Chromium的Extension由Page和Content Script组成。Page有UI和JS,它们加载在自己的Extension Process中渲染和执行。Content Script只有JS,这些JS是注入在宿主网页中执行的。Content Script可以访问宿主网页的DOM Tree,从而可以增强宿主网页的功能。本文接下来分析Content Script注入到宿主网页执行的过程。
我们可以在Extension的清单文件中指定Content Script对哪些网页有兴趣,如前面Chromium扩展(Extension)机制简要介绍和学习计划一文的Page action example所示:
{
......
"content_scripts": [
{
"matches": ["https://fast.com/"],
"js": ["content.js"],
"run_at": "document_start",
"all_frames": true
}
]
}
这个清单文件仅对URL为"https://fast.com/"的网页感兴趣。当这个网页在Chromium中加载的时候,Chromium就会往里面注入脚本content.js。注入过程如图1所示:
图1 Content Script注入到宿主网页执行的过程
首先,在前面Chromium扩展(Extension)加载过程分析一文提到,Browser进程在加载Extension之前,会创建一个UserScriptMaster对象。此后每当加载一个Extension,这个UserScriptMaster对象的成员函数OnExtensionLoaded都会被调用,用来收集当前正在加载的Extension的Content Script。
此后,每当Browser进程启动一个Render进程时,代表该Render进程的一个RenderProcessHostImpl对象的成员函数OnProcessLaunched都会被调用,用来通知Browser进程新的Render进程已经启动起来的。这时候这个RenderProcessHostImpl对象会到上述UserScriptMaster对象中获取当前收集到的所有Content Script。这些Content Script接下来会通过一个类型为ExtensionMsg_UpdateUserScript的IPC消息传递给新启动的Render进程。新启动的Render进程通过一个Dispatcher对象接收这个IPC消息,并且会将它传递过来的Content Script保存在一个UserScriptSlave对象中。
接下来,每当Render进程加载一个网页时,都会在三个时机检查是否需要在该网页中注入Content Script。从前面Chromium扩展(Extension)机制简要介绍和学习计划一文可以知道,这三个时机分别为document_start、document_end和document_idle,分别表示网页的Document对象开始创建、结束创建以及空闲时。接下来我们以document_start这个时机为例,说明Content Script注入到宿主网页的过程。
网页的Document对象是在WebKit中创建的。WebKit为网页创建了Document对象之后,会调用Content层的一个RenderFrameImpl对象的成员函数didCreateDocumentElement,用来通知后者,它描述的网页的Document对象已经创建好了。这时候这个RenderFrameImpl对象将会调用前面提到的UserScriptSlave对象的成员函数InjectScripts,用来通知后者,现在可以将Content Script注入当前正在加载的网页中去执行。前面提到的UserScriptSlave对象会调用另外一个WebLocalFrameImpl对象的成员函数executeScriptInIsolatedWorld,用来注入符合条件的Content Script到当前正在加载的网页中去,并且在JS引擎的一个Isolated World中执行。Content Script在Isolated World中执行,意味着它不可以访问在宿主网页中定义的JavaScript,包括不能调用在宿主网页中定义的JavaScript函数,以及访问宿主网页中定义的变量。
以上就是Content Script注入到宿主网页中执行的大概流程。接下来我们结合源代码进行详细的分析,以便对这个注入流程有更深刻的认识。
我们首先分析UserScriptMaster类收集Content Script的过程。这要从UserScriptMaster类的构造函数说起,如下所示:
UserScriptMaster::UserScriptMaster(Profile* profile)
: ......,
extension_registry_observer_(this) {
extension_registry_observer_.Add(ExtensionRegistry::Get(profile_));
registrar_.Add(this, chrome::NOTIFICATION_EXTENSIONS_READY,
content::Source<profile>(profile_));
registrar_.Add(this, content::NOTIFICATION_RENDERER_PROCESS_CREATED,
content::NotificationService::AllBrowserContextsAndSources());
}
这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。
UserScriptMaster类的成员变量extension_registry_observer_描述的是一个ExtensionRegistryObserver对象。这个ExtensionRegistryObserver对象接下来会注册到与参数profile描述的一个Profile对象关联的一个Extension Registry对象中去,也就是注入到与当前使用的Profile关联的一个Extension Registry对象中去。这个Extension Registry对象的创建过程可以参考前面Chromium扩展(Extension)加载过程分析一文。
与此同时,UserScriptMaster类的构造函数还会通过成员变量registrar_描述的一个NotificationRegistrar对象监控chrome::NOTIFICATION_EXTENSIONS_READY和content::NOTIFICATION_RENDERER_PROCESS_CREATED事件。其中,事件chrome::NOTIFICATION_EXTENSIONS_READY用来通知Chromium的Extension Service已经启动了,而事件content::NOTIFICATION_RENDERER_PROCESS_CREATED用来通知有一个新的Render进程启动起来。如前面所述,当新的Render进程启动起来的时候,UserScriptMaster类会将当前加载的Extension定义的Content Script传递给它处理。这个过程我们在后面会进行详细分析。
从前面Chromium扩展(Extension)加载过程分析一文还可以知道,接下来加载的每一个Extension,都会保存在上述Extension Registry对象内部的一个Enabled List中,并且都会调用注册在上述Extension Registry对象中的每一个ExtensionRegistryObserver对象的成员函数OnExtensionLoaded,通知它们有一个新的Extension被加载。
当UserScriptMaster类的成员变量extension_registry_observer_描述的ExtensionRegistryObserver对象的成员函数OnExtensionLoaded被调用时,它又会调用UserScriptMaster类的成员函数OnExtensionLoaded,以便UserScriptMaster类可以收集新加载的Extension定义的Content Script,如下所示:
void UserScriptMaster::OnExtensionLoaded(
content::BrowserContext* browser_context,
const Extension* extension) {
// Add any content scripts inside the extension.
extensions_info_[extension->id()] =
ExtensionSet::ExtensionPathAndDefaultLocale(
extension->path(), LocaleInfo::GetDefaultLocale(extension));
......
const UserScriptList& scripts =
ContentScriptsInfo::GetContentScripts(extension);
for (UserScriptList::const_iterator iter = scripts.begin();
iter != scripts.end();
++iter) {
user_scripts_.push_back(*iter);
.....
}
if (extensions_service_ready_) {
changed_extensions_.insert(extension->id());
if (script_reloader_.get()) {
pending_load_ = true;
} else {
StartLoad();
}
}
}
这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。
参数extension描述的就是当前正在加载的Extension。UserScriptMaster类的成员函数OnExtensionLoaded首先会调用ExtensionSet类的静态成员函数ExtensionPathAndDefaultLocale将该Extension的Path和Locale信息封装在一个ExtensionPathAndDefaultLocale对象中,并且以该Extension的ID为键值,将上述ExtensionPathAndDefaultLocale对象保存在成员变量extensions_info_描述的一个std::map中。
UserScriptMaster类的成员函数OnExtensionLoaded接下来将当前正在加载的Extension定义的所有Content Script保存在成员变量user_scripts_描述的一个std::vector中。
UserScriptMaster类有一个类型为bool的成员变量extensions_serviceready。当它的值等于true的时候,表示Chromium的Extension Service已经启动起来了。这时候extensions_service_ready_就会将当前正在加载的Extension的ID插入到UserScriptMaster类的成员变量changed_extensions_描述的一个std::set中去,表示有一个新的Extension需要处理。这里说的处理,就是将新加载的Extension定义的Content Script的内容读取出来,并且保存在一个共享内存中。
将Extension定义的Content Script的内容读取出来,并且保存在一个共享内存中,是通过UserScriptMaster类的成员变量script_reloader_指向的一个ScriptReloader对象实现的。如果这个ScriptReloader已经创建出来,那么就表示它现在正在读取Content Script的过程中。这时候UserScriptMaster类的成员变量pending_load_的值会被设置为true,表示当前需要读取的Content Script发生了变化,因此需要重新进行读取。
如果UserScriptMaster类的成员变量script_reloader_指向的ScriptReloader对象还没有创建出来,那么UserScriptMaster类的成员函数OnExtensionLoaded就会调用另外一个成员函数StartLoad创建该ScriptReloader对象,并且通过该ScriptReloader对象读取当前已经加载的Extension定义的Content Script,如下所示:
void UserScriptMaster::StartLoad() {
if (!script_reloader_.get())
script_reloader_ = new ScriptReloader(this);
script_reloader_->StartLoad(user_scripts_, extensions_info_);
}
这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。
从这里可以看到,如果UserScriptMaster类的成员变量script_reloader_指向的ScriptReloader对象还没有创建出来,那么UserScriptMaster类的成员函数StartLoad就会创建,并且在创建之后,调用它的成员函数StartLoad读取当前已经加载的Extension定义的Content Script,如下所示:
void UserScriptMaster::ScriptReloader::StartLoad(
const UserScriptList& user_scripts,
const ExtensionsInfo& extensions_info) {
// Add a reference to ourselves to keep ourselves alive while we're running.
// Balanced by NotifyMaster().
AddRef();
......
this->extensions_info_ = extensions_info;
BrowserThread::PostTask(
BrowserThread::FILE, FROM_HERE,
base::Bind(
&UserScriptMaster::ScriptReloader::RunLoad, this, user_scripts));
}
这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。
ScriptReloader类的成员函数StartLoad首先调用成员函数AddRef增加当前正在处理的ScriptReloader对象的引用计数,避免它在读取Content Script的过程中被销毁。
从前面的调用过程可以知道,参数user_scripts描述的是一个std::vector。这个std::vector保存在当前已经加载的Extension定义的Content Script。另外一个参数extension_info指向的是一个std::map。这个std::map描述了当前加载的所有Extension。
ScriptReloader类的成员函数StartLoad将参数extension_info指向的std::map保存在自己的成员变量extension_info_之后,就向Browser进程中专门用来执行文件读写操作的BrowserThread::FILE线程的消息队列发送一个Task。这个Task绑定了ScriptReloader类的成员函数RunLoad。
这意味着ScriptReloader类的成员函数RunLoad接下来会在BrowserThread::FILE线程被调用,用来读取保存在参数user_scripts描述的std::vector中的Content Script,如下所示:
// This method will be called on the file thread.
void UserScriptMaster::ScriptReloader::RunLoad(
const UserScriptList& user_scripts) {
LoadUserScripts(const_cast<UserScriptList*>(&user_scripts));
// Scripts now contains list of up-to-date scripts. Load the content in the
// shared memory and let the master know it's ready. We need to post the task
// back even if no scripts ware found to balance the AddRef/Release calls.
BrowserThread::PostTask(master_thread_id_,
FROM_HERE,
base::Bind(&ScriptReloader::NotifyMaster,
this,
base::Passed(Serialize(user_scripts))));
}
这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。
ScriptReloader类的成员函数RunLoad首先调用成员函数LoadUserScripts读取当前已经加载的Extension定义的Content Script,如下所示:
void UserScriptMaster::ScriptReloader::LoadUserScripts(
UserScriptList* user_scripts) {
for (size_t i = 0; i < user_scripts->size(); ++i) {
UserScript& script = user_scripts->at(i);
scoped_ptr<SubstitutionMap> localization_messages(
GetLocalizationMessages(script.extension_id()));
for (size_t k = 0; k < script.js_scripts().size(); ++k) {
UserScript::File& script_file = script.js_scripts()[k];
if (script_file.GetContent().empty())
LoadScriptContent(
script.extension_id(), &script_file, NULL, verifier_.get());
}
for (size_t k = 0; k < script.css_scripts().size(); ++k) {
UserScript::File& script_file = script.css_scripts()[k];
if (script_file.GetContent().empty())
LoadScriptContent(script.extension_id(),
&script_file,
localization_messages.get(),
verifier_.get());
}
}
}
这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。
参数user_srcipts描述的std::vector里面保存的是一系列的UserScript对象。每一个UserScript对象里面包含若干个Content Script文件。每一个Content Script文件都是通过一个UserScript::File对象描述。注意,这些Content Script有可能是JavaScript,也有可能是CSS Script。这意味着Extension不仅可以注入JavaScript到宿主网页中,还可以注入CSS Script。
ScriptReloader类的成员函数LoadUserScripts依次调用函数LoadScriptContent读取这些Content Script文件的内容,如下所示:
static bool LoadScriptContent(const std::string& extension_id,
UserScript::File* script_file,
const SubstitutionMap* localization_messages,
ContentVerifier* verifier) {
std::string content;
const base::FilePath& path = ExtensionResource::GetFilePath(
script_file->extension_root(), script_file->relative_path(),
ExtensionResource::SYMLINKS_MUST_RESOLVE_WITHIN_ROOT);
if (path.empty()) {
......
} else {
if (!base::ReadFileToString(path, &content)) {
LOG(WARNING) << "Failed to load user script file: " << path.value();
return false;
}
......
}
......
// Remove BOM from the content.
std::string::size_type index = content.find(base::kUtf8ByteOrderMark);
if (index == 0) {
script_file->set_content(content.substr(strlen(base::kUtf8ByteOrderMark)));
} else {
script_file->set_content(content);
}
return true;
}
这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。
函数LoadScriptContent首先调用ExtensionResource类的静态成员函数GetFilePath获得要读取的Content Script的文件路径,然后再调用函数base::ReadFileToString读取该文件的内容。这样就可以获得要读取的Content Script的内容,这些内容最终又会保存在参数script_file描述的一个UserScript::File对象的内部。
这一步执行完成后,Chromium就获得了当前已经加载的Extension所定义的Content Script的内容。这些内容保存在每一个Content Script对应的UserScript::File对象中。回到前面分析的ScriptReloader类的成员函数RunLoad中,接下来它调用函数Serialize将前面读取的Content Script的内容保存在一块共享内存中,如下所示:
// Pickle user scripts and return pointer to the shared memory.
static scoped_ptr<base::SharedMemory> Serialize(const UserScriptList& scripts) {
Pickle pickle;
pickle.WriteUInt64(scripts.size());
for (size_t i = 0; i < scripts.size(); i++) {
const UserScript& script = scripts[i];
// TODO(aa): This can be replaced by sending content script metadata to
// renderers along with other extension data in ExtensionMsg_Loaded.
// See crbug.com/70516.
script.Pickle(&pickle);
// Write scripts as 'data' so that we can read it out in the slave without
// allocating a new string.
for (size_t j = 0; j < script.js_scripts().size(); j++) {
base::StringPiece contents = script.js_scripts()[j].GetContent();
pickle.WriteData(contents.data(), contents.length());
}
for (size_t j = 0; j < script.css_scripts().size(); j++) {
base::StringPiece contents = script.css_scripts()[j].GetContent();
pickle.WriteData(contents.data(), contents.length());
}
}
// Create the shared memory object.
base::SharedMemory shared_memory;
base::SharedMemoryCreateOptions options;
options.size = pickle.size();
options.share_read_only = true;
if (!shared_memory.Create(options))
return scoped_ptr<base::SharedMemory>();
if (!shared_memory.Map(pickle.size()))
return scoped_ptr<base::SharedMemory>();
// Copy the pickle to shared memory.
memcpy(shared_memory.memory(), pickle.data(), pickle.size());
base::SharedMemoryHandle readonly_handle;
if (!shared_memory.ShareReadOnlyToProcess(base::GetCurrentProcessHandle(),
&readonly_handle))
return scoped_ptr<base::SharedMemory>();
return make_scoped_ptr(new base::SharedMemory(readonly_handle,
/*read_only=*/true));
}
这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。
函数Serialize依次遍历保存在参数scripts描述的一个std::vector中的每一个UserScript对象,并且将这些UserScript对象包含的Content Script写入到本地变量pickle描述一个Pickle对象中。从前面Chromium的IPC消息发送、接收和分发机制分析一文可以知道,Pickle类是Chromium定义的一种IPC消息格式,它将数据按照一定的格式打包在一块内存中。
函数Serialize将Content Script写入到本地变量pickle描述的Pickle对象中去之后,接下来又会创建一块共享内存。这块共享内存通过本地变量shared_memory描述的一个SharedMemory对象描述。有了这块共享内存之后,函数Serialize就会将Content Script的内容从本地变量pickle描述的Pickle对象中拷贝到它里面去。
函数Serialize最后获得已经写入了Content Script的共享内存的只读版本,并且将这个只读版本封装在另外一个SharedMemory对象中返回给调用者,以便调用者以后可以将它传递给宿主网页所在的Render进程进行只读访问。
这一步执行完成后,Chromium就获得了一块只读的共享内存,这块共享内存保存了当前已经加载的Extension所定义的Content Script的内容。回到前面分析的ScriptReloader类的成员函数RunLoad中,接下来它又会向成员变量master_thread_id_描述的线程的消息队列发送一个Task。这个Task绑定了ScriptReloader类的成员函数NotifyMaster,用来通知其内部引用的一个UserScriptMaster对象,当前已经加载的Extension所定义的Content Script的内容已经读取完毕。
ScriptReloader类的成员函数NotifyMaster的实现如下所示:
void UserScriptMaster::ScriptReloader::NotifyMaster(
scoped_ptr<base::SharedMemory> memory) {
// The master could go away
if (master_)
master_->NewScriptsAvailable(memory.Pass());
// Drop our self-reference.
// Balances StartLoad().
Release();
}
这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。
ScriptReloader类内部引用的UserScriptMaster对象保存在成员变量master_中。ScriptReloader类的成员函数NotifyMaster首先调用这个UserScriptMaster对象的成员函数NewScriptsAvailable,通知它已经加载的Extension所定义的Content Script的内容已经读取完毕。
通知完成后,ScriptReloader类的成员函数NotifyMaster还会调用成员函数Release减少当前正在处理的ScriptReloader对象的引用计数,用来平衡在读取Content Script之前,对该ScriptReloader对象的引用计数的增加。
接下来我们继续分析UserScriptMaster类的成员函数NewScriptsAvailable的实现,以便了解它是如何处理当前已经加载的Extension的Content Script的内容的,如下所示:
void UserScriptMaster::NewScriptsAvailable(
scoped_ptr<base::SharedMemory> handle) {
if (pending_load_) {
// While we were loading, there were further changes. Don't bother
// notifying about these scripts and instead just immediately reload.
pending_load_ = false;
StartLoad();
} else {
......
// We've got scripts ready to go.
shared_memory_ = handle.Pass();
for (content::RenderProcessHost::iterator i(
content::RenderProcessHost::AllHostsIterator());
!i.IsAtEnd(); i.Advance()) {
SendUpdate(i.GetCurrentValue(),
shared_memory_.get(),
changed_extensions_);
}
......
}
}
这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。
UserScriptMaster类的成员函数NewScriptsAvailable首先判断成员变量pending_load_的值是否等于true。如果等于true,那么就说明前面在读取Content Script的过程中,又有新的Extension被加载。这时候UserScriptMaster类的成员函数会调用前面分析过的成员函数StartLoad对Content Script进行重新读取。
我们假设这时候UserScriptMaster类的成员变量pending_load_的值等于false。在这种情况下,UserScriptMaster类的成员函数NewScriptsAvailable首先将参数handle描述的共享内存保存在成员变量shared_memory_中,接下来又会将这块共享内存传递给当前已经启动的Render进程,这是通过调用成员函数SendUpdate实现的。
我们假设当前还没有Render进程启动起来。前面分析UserScriptMaster类的构造函数的实现时提到,UserScriptMaster类会监控Render进程启动事件,也就是content::NOTIFICATION_RENDERER_PROCESS_CREATED事件。以后每当有一个Render进程启动完成,UserScriptMaster类的成员函数Observe就会被调用。UserScriptMaster类的成员函数Observe在调用的过程中,就会将前面已经加载的Extension的Content Script发送给新启动的Render进程,以便后者可以将Content Script注入到它后续加载的网页中去。
接下来我们就从Render进程启动完成后开始,分析UserScriptMaster类将Content Script传递给Render进程的过程。从前面Chromium的Render进程启动过程分析一文可以知道,Render进程是由Browser进程启动的。Browser进程又是通过ChildProcessLauncher::Context类启动Render进程的。当Render进程启动完成后,ChildProcessLauncher::Context类的静态成员函数OnChildProcessStarted就会被调用,它的实现如下所示:
class ChildProcessLauncher::Context
: public base::RefCountedThreadSafe<ChildProcessLauncher::Context> {
public:
......
static void OnChildProcessStarted(
// |this_object| is NOT thread safe. Only use it to post a task back.
scoped_refptr<Context> this_object,
BrowserThread::ID client_thread_id,
const base::TimeTicks begin_launch_time,
base::ProcessHandle handle) {
RecordHistograms(begin_launch_time);
if (BrowserThread::CurrentlyOn(client_thread_id)) {
// This is always invoked on the UI thread which is commonly the
// |client_thread_id| so we can shortcut one PostTask.
this_object->Notify(handle);
} else {
BrowserThread::PostTask(
client_thread_id, FROM_HERE,
base::Bind(
&ChildProcessLauncher::Context::Notify,
this_object,
handle));
}
}
......
};
这个函数定义在文件external/chromium_org/content/browser/child_process_launcher.cc中。
参数client_thread_id描述的是当初请求启动Render进程的线程的ID。另外一个参数this_object描述的是用来启动Render进程的一个ChildProcessLauncher::Context对象。这个ChildProcessLauncher::Context对象就是在请求启动Render进程的线程中创建的。
ChildProcessLauncher::Context类的静态成员函数OnChildProcessStarted首先检查当前线程是否就是当初请求启动Render进程的线程。如果是的话,那么就直接调用参数this_object描述的ChildProcessLauncher::Context对象的成员函数Notify,用来通知它请求的Render进程已经启动完成了。否则的话,会向当初请求启动Render进程的线程的消息队列发送一个Task,然后在这个Task执行的时候,再调用参数this_object描述的ChildProcessLauncher::Context对象的成员函数Notify。通过这种方式,保证参数this_object描述的ChildProcessLauncher::Context对象总是在创建它的线程中使用。这样可以避免在多线程环境下,访问对象(调用对象的成员函数)需要加锁的问题(加锁会引入竞争,竞争会带来不确定的延时)。这是Chromium多线程编程哲学所要遵循的原则之一。关于Chromium多线程编程哲学,可以参考前面Chromium多线程模型设计和实现分析一文。
接下来,我们就继续分析ChildProcessLauncher::Context类的成员函数Notify的实现,如下所示:
class ChildProcessLauncher::Context
: public base::RefCountedThreadSafe<ChildProcessLauncher::Context> {
......
private:
......
void Notify(
#if defined(OS_POSIX) && !defined(OS_MACOSX) && !defined(OS_ANDROID)
bool zygote,
#endif
base::ProcessHandle handle) {
......
if (client_) {
if (handle) {
client_->OnProcessLaunched();
} else {
client_->OnProcessLaunchFailed();
}
}
......
}
......
};
这个函数定义在文件external/chromium_org/content/browser/child_process_launcher.cc中。
ChildProcessLauncher::Context类的成员变量client_指向的是一个RenderProcessHostImpl对象。这个RenderProcessHostImpl对象是在Browser进程中描述它启动的一个Render进程的。通过这个RenderProcessHostImpl对象,可以与Render进程进行IPC。
参数handle描述的就是前面请求启动的Render进程的句柄。当这个句柄值不等于0时,就表示请求的Render进程已经成功地启动起来了。这时候ChildProcessLauncher::Context类的成员函数就会调用成员变量client_指向的RenderProcessHostImpl对象的成员函数OnProcessLaunched,以便它可以发出一个content::NOTIFICATION_RENDERER_PROCESS_CREATED事件通知,如下所示:
void RenderProcessHostImpl::OnProcessLaunched() {
......
NotificationService::current()->Notify(
NOTIFICATION_RENDERER_PROCESS_CREATED,
Source<RenderProcessHost>(this),
NotificationService::NoDetails());
......
}
这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_process_host_impl.cc中。
从前面的分析可以知道,一旦RenderProcessHostImpl对象的成员函数OnProcessLaunched发出content::NOTIFICATION_RENDERER_PROCESS_CREATED事件通知,UserScriptMaster类的成员函数Observe就会被调用,如下所示:
void UserScriptMaster::Observe(int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
......
switch (type) {
......
case content::NOTIFICATION_RENDERER_PROCESS_CREATED: {
content::RenderProcessHost* process =
content::Source<content::RenderProcessHost>(source).ptr();
......
if (ScriptsReady()) {
SendUpdate(process,
GetSharedMemory(),
std::set<std::string>()); // Include all extensions.
}
break;
}
......
}
......
}
这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。
UserScriptMaster类的成员函数Observe在处理content::NOTIFICATION_RENDERER_PROCESS_CREATED事件通知的时候,首先会调用成员函数ScriptsReady检查当前加载的Extension的Content Script是否已经读取出来,并且保存在内部维护的一块共享内存中去了。如果是的话,那么就会继续调用另外一个成员函数SendUpdate将这块共享内存传递给当前启动完成的Render进程。
UserScriptMaster类的成员函数SendUpdate的实现如下所示:
void UserScriptMaster::SendUpdate(
content::RenderProcessHost* process,
base::SharedMemory* shared_memory,
const std::set<std::string>& changed_extensions) {
......
base::SharedMemoryHandle handle_for_process;
if (!shared_memory->ShareToProcess(handle, &handle_for_process))
return; // This can legitimately fail if the renderer asserts at startup.
if (base::SharedMemory::IsHandleValid(handle_for_process)) {
process->Send(new ExtensionMsg_UpdateUserScripts(handle_for_process,
changed_extensions));
}
}
这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。
参数shared_memory描述的共享内存就是要发送给参数process描述的Render进程的,它里面包含了当前加载的Extension的Content Script。UserScriptMaster类的成员函数SendUpdate将这块共享封装在一个类型为ExtensionMsg_UpdateUserScripts的IPC消息中,并且发送给参数process描述的Render进程。
Render进程在启动的时候会创建一个Dispatcher对象。这个Dispatcher对象会通过成员函数OnControlMessageReceived接收类型为ExtensionMsg_UpdateUserScripts的IPC消息,如下所示:
bool Dispatcher::OnControlMessageReceived(const IPC::Message& message) {
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(Dispatcher, message)
......
IPC_MESSAGE_HANDLER(ExtensionMsg_UpdateUserScripts, OnUpdateUserScripts)
......
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
return handled;
}
这个函数定义在文件external/chromium_org/extensions/renderer/dispatcher.cc中。
Dispatcher类的成员函数OnControlMessageReceived将类型为ExtensionMsg_UpdateUserScripts的IPC消息分发给另外一个成员函数OnUpdateUserScripts处理,如下所示:
void Dispatcher::OnUpdateUserScripts(
base::SharedMemoryHandle scripts,
const std::set<std::string>& extension_ids) {
......
user_script_slave_->UpdateScripts(scripts, extension_ids);
......
}
这个函数定义在文件external/chromium_org/extensions/renderer/dispatcher.cc中。
Dispatcher类的成员变量user_script_slave_指向的是一个UserScriptSlave对象。Dispatcher类的成员函数OnUpdateUserScripts调用这个UserScriptSlave对象的成员函数UpdateScripts将参数scripts描述的共享内存包含的Content Script交给它处理。处理过程如下所示:
bool UserScriptSlave::UpdateScripts(
base::SharedMemoryHandle shared_memory,
const std::set<std::string>& changed_extensions) {
......
// Unpickle scripts.
uint64 num_scripts = 0;
Pickle pickle(reinterpret_cast<char*>(shared_memory_->memory()), pickle_size);
PickleIterator iter(pickle);
CHECK(pickle.ReadUInt64(&iter, &num_scripts));
......
// If we pass no explicit extension ids, we should refresh all extensions.
bool include_all_extensions = changed_extensions.empty();
......
if (include_all_extensions) {
script_injections_.clear();
}
......
for (uint64 i = 0; i < num_scripts; ++i) {
scoped_ptr<UserScript> script(new UserScript());
script->Unpickle(pickle, &iter);
......
for (size_t j = 0; j < script->js_scripts().size(); ++j) {
const char* body = NULL;
int body_length = 0;
CHECK(pickle.ReadData(&iter, &body, &body_length));
script->js_scripts()[j].set_external_content(
base::StringPiece(body, body_length));
}
for (size_t j = 0; j < script->css_scripts().size(); ++j) {
const char* body = NULL;
int body_length = 0;
CHECK(pickle.ReadData(&iter, &body, &body_length));
script->css_scripts()[j].set_external_content(
base::StringPiece(body, body_length));
}
// If we include all extensions or the given extension changed, we add a
// new script injection.
if (include_all_extensions ||
changed_extensions.count(script->extension_id()) > 0) {
script_injections_.push_back(new ScriptInjection(script.Pass(), this));
}
......
}
return true;
}
这个函数定义在文件external/chromium_org/extensions/renderer/user_script_slave.cc中。
UserScriptSlave类的成员函数UpdateScripts会通过本地变量pickle描述的Pickle对象解析和读取包含在参数shared_memory中的Content Script。这些Content Script将会保存在UserScriptSlave类的成员变量script_injections_描述的一个Vector中。
当参数changed_extensions描述的字符串不等于空时,它的值就表示Content Script内容发生过变化的Extension。这时候UserScriptSlave类可以对内部维护的Content Script进行增量更新。从前面的调用过程可以知道,在我们这种情景中,参数changed_extensions描述的字符串等于空。在这种情况下,UserScriptSlave类将会对内部维护的Content Script进行全部更新。也就是先清空成员变量script_injections_描述的Vector,然后再将包含在参数shared_memory中的Content Script增加到这个Vector中去。
从前面分析的函数Serialize可以知道,包含在参数shared_memory中的Content Script是按照Extension进行组织的。一个Extension对应一个UserScript对象。一个UserScript对象又可以包含若干个Content Script。这些Content Script又可能同时包含有JavaScript和CSS Script。UserScriptSlave类的成员函数UpdateScripts通过本地变量pickle描述的Pickle对象依次获得每一个Extension的Content Script,并且封装在一个ScriptInjection对象中。这些ScriptInjection对象会保存在UserScriptSlave类的成员变量script_injections_描述的一个Vector中。
这一步执行完成后,每一个Render进程就会获得当前加载的所有Extension定义的Content Script。这些Content Script由一个UserScriptSlave对象维护。以后每当Render进程加载一个网页,就会询问这个UserScriptSlave对象,是否需要往里面注入相应的Content Script。
接下来,我们以前面提到的Page action example的Content Script注入到URL为"https://fast.com/"的网页为例,分析Content Script注入宿主网页执行的过程。从Page action example的Content Script的定义可以知道,它是在URL为"https://fast.com/"的网页的Document对象创建时注入的。
前面提到,网页的Document对象是在WebKit中创建的。WebKit为网页创建了Document对象之后,会调用Content层的一个RenderFrameImpl对象的成员函数didCreateDocumentElement,用来通知后者,它描述的网页的Document对象已经创建好了,如下所示:
void RenderFrameImpl::didCreateDocumentElement(blink::WebLocalFrame* frame) {
......
FOR_EACH_OBSERVER(RenderViewObserver, render_view_->observers(),
DidCreateDocumentElement(frame));
}
这个函数定义在文件external/chromium_org/content/renderer/render_frame_impl.cc中。
RenderFrameImpl类的成员变量render_view_指向的是一个RenderViewImpl对象。这个RenderViewImpl对象的内部维护有一系列的Render View Observer。这些Render View Observer用来监听WebKit事件。这样,如果一个模块要监听某一个网页的WebKit事件,那么就往与该网页对应的RenderViewImpl对象内部注册一个Render View Observer即可。
Chromium的Extension模块会监听所有网页的WebKit事件,这是通过向与这些网页对应的RenderViewImpl对象内部注册一个类型为ExtensionHelper的Render View Observer实现的。这意味着当WebKit发出事件通知时,ExtensionHelper类的相应成员函数会被调用。在我们这个情景中,就是当网页的Document对象已经创建出来时,ExtensionHelper类的成员函数DidCreateDocumentElement会被调用。在调用的过程中,它就会往当前加载的网页注入那些运行时机为"document_start"的Content Script,如下所示:
void ExtensionHelper::DidCreateDocumentElement(WebLocalFrame* frame) {
dispatcher_->user_script_slave()->InjectScripts(
frame, UserScript::DOCUMENT_START);
......
}
这个函数定义在文件external/chromium_org/extensions/renderer/extension_helper.cc中。
ExtensionHelper类的成员变量dispatcher_指向的是一个Dispatcher对象。这个Dispatcher对象就是前面分析的用来处理从Browser进程发送过来的类型为ExtensionMsg_UpdateUserScripts的IPC消息的Dispatcher对象。ExtensionHelper类的成员函数DidCreateDocumentElement首先调用这个Dispatcher对象的成员函数user_script_slave获得它内部维护的一个UserScriptSlave对象。有了这个UserScriptSlave对象之后,就可以调用它的成员函数InjectScripts向当前加载的网页注入那些运行时机为"document_start"的Content Script,如下所示:
void UserScriptSlave::InjectScripts(WebFrame* frame,
UserScript::RunLocation location) {
GURL document_url = ScriptInjection::GetDocumentUrlForFrame(frame);
......
ScriptInjection::ScriptsRunInfo scripts_run_info;
for (ScopedVector<ScriptInjection>::const_iterator iter =
script_injections_.begin();
iter != script_injections_.end();
++iter) {
(*iter)->InjectIfAllowed(frame, location, document_url, &scripts_run_info);
}
......
}
这个函数定义在文件external/chromium_org/extensions/renderer/user_script_slave.cc中。
参数frame描述的就是当前加载的网页。UserScriptSlave类的成员函数InjectScripts首先通过调用ScriptInjection类的静态成员函数GetDocumentUrlForFrame获得这个网页的URL。有了这个URL之后,UserScriptSlave类的成员函数InjectScripts接下来就会遍历保存在成员变量script_injections_描述的Vector中的每一个ScriptInjection对象,并且调用这些ScriptInjection对象的成员函数InjectIfAllowed。
从前面的分析可以知道,保存在UserScriptSlave类的成员变量script_injections_中的ScriptInjection对象描述的就是当前加载的Extension的Content Script。这些ScriptInjection对象的成员函数InjectIfAllowed在执行期间,就会判断是否需要将自己描述的Content Script注入到当前加载的网页中去执行,也就是前面获得的URL对应的网页。
接下来我们就继续分析ScriptInjection类的成员函数InjectIfAllowed的实现,以便了解Content Script注入到宿主网页执行的过程,如下所示:
void ScriptInjection::InjectIfAllowed(blink::WebFrame* frame,
UserScript::RunLocation run_location,
const GURL& document_url,
ScriptsRunInfo* scripts_run_info) {
if (!WantsToRun(frame, run_location, document_url))
return;
const Extension* extension = user_script_slave_->GetExtension(extension_id_);
DCHECK(extension); // WantsToRun() should be false if there's no extension.
// We use the top render view here (instead of the render view for the
// frame), because script injection on any frame requires permission for
// the top frame. Additionally, if we have to show any UI for permissions,
// it should only be done on the top frame.
content::RenderView* top_render_view =
content::RenderView::FromWebView(frame->top()->view());
int tab_id = ExtensionHelper::Get(top_render_view)->tab_id();
// By default, we allow injection.
bool should_inject = true;
// Check if the extension requires user consent for injection *and* we have a
// valid tab id (if we don't have a tab id, we have no UI surface to ask for
// user consent).
if (tab_id != -1 &&
extension->permissions_data()->RequiresActionForScriptExecution(
extension, tab_id, frame->top()->document().url())) {
int64 request_id = kInvalidRequestId;
int page_id = top_render_view->GetPageId();
// We only delay the injection if the feature is enabled.
// Otherwise, we simply treat this as a notification by passing an invalid
// id.
if (FeatureSwitch::scripts_require_action()->IsEnabled()) {
should_inject = false;
ScopedVector<PendingInjection>::iterator pending_injection =
pending_injections_.insert(
pending_injections_.end(),
new PendingInjection(frame, run_location, page_id));
request_id = (*pending_injection)->id;
}
top_render_view->Send(
new ExtensionHostMsg_RequestContentScriptPermission(
top_render_view->GetRoutingID(),
extension->id(),
page_id,
request_id));
}
if (should_inject)
Inject(frame, run_location, scripts_run_info);
}
这个函数定义在文件external/chromium_org/extensions/renderer/script_injection.cc中。
ScriptInjection类的成员函数InjectIfAllowed首先调用成员函数WantsToRun判断当前加载的网页是否是当前正在处理的Content Script感兴趣的网页,也就是判断当前加载的网页的URL是否匹配Content Script在其Extension的清单文件设置的URL规则。如果不匹配,那么就说明不需要将当前正在处理的Content Script注入到当前加载的网页中执行。
如果匹配,ScriptInjection类的成员函数InjectIfAllowed还会进一步检查当前正在处理的Content Script所在的Extension是否对当前正在加载的网页申请了Permission。如果没有申请,并且Chromium启用了Scripts Require Action Feature,那么就需要用户同意后,才能将当前正在处理的Content Script注入到当前加载的网页中执行。这个需要用户同意的操作是通过向Browser进程发出一个类型ExtensionHostMsg_RequestContentScriptPermission的IPC消息触发的。
我们假设当前加载的网页是当前正在处理的Content Script感兴趣的网页,并且Chromium没有开启Scripts Require Action Feature。这时候ScriptInjection类的成员函数InjectIfAllowed就会马上调用成员函数Inject将当前正在处理的Content Script注入到当前加载的网页中执行,如下所示:
void ScriptInjection::Inject(blink::WebFrame* frame,
UserScript::RunLocation run_location,
ScriptsRunInfo* scripts_run_info) const {
......
if (ShouldInjectCSS(run_location))
InjectCSS(frame, scripts_run_info);
if (ShouldInjectJS(run_location))
InjectJS(frame, scripts_run_info);
}
这个函数定义在文件external/chromium_org/extensions/renderer/script_injection.cc中。
ScriptInjection类的成员函数Inject首先调用成员函数ShouldInjectsCSS判断当前正在处理的Content Script是否包含有CSS Script,并且参数run_location描述的Content Script运行时机是否为"document_start"。如果都是的话,那么当前正在处理的Content Script包含的CSS Script就会通过调用另外一个成员函数InjectCSS注入到当前加载的网页中去。这意味着CSS Script只可以在宿主网页的Document对象创建时注入。
ScriptInjection类的成员函数Inject接下来又调用成员函数ShouldInjectJS判断当前正在处理的Content Script是否包含有JavaScript,并且参数run_location描述的Content Script运行时机是否为包含的JavaScript在清单文件中指定的运行时机。如果都是的话,那么当前正在处理的Content Script包含的JavaScript就会通过调用另外一个成员函数InjectJS注入到当前加载的网页中去执行。
接下来我们只关注JavaScript注入到宿主网页执行的过程,因此我们继续分析ScriptInjection类的成员函数InjectJS的实现,如下所示:
void ScriptInjection::InjectJS(blink::WebFrame* frame,
ScriptsRunInfo* scripts_run_info) const {
const UserScript::FileList& js_scripts = script_->js_scripts();
std::vector<blink::WebScriptSource> sources;
scripts_run_info->num_js += js_scripts.size();
for (UserScript::FileList::const_iterator iter = js_scripts.begin();
iter != js_scripts.end();
++iter) {
std::string content = iter->GetContent().as_string();
.......
sources.push_back(blink::WebScriptSource(
blink::WebString::fromUTF8(content), iter->url()));
}
......
int isolated_world_id =
user_script_slave_->GetIsolatedWorldIdForExtension(
user_script_slave_->GetExtension(extension_id_), frame);
......
DOMActivityLogger::AttachToWorld(isolated_world_id, extension_id_);
frame->executeScriptInIsolatedWorld(isolated_world_id,
&sources.front(),
sources.size(),
EXTENSION_GROUP_CONTENT_SCRIPTS);
......
}
这个函数定义在文件external/chromium_org/extensions/renderer/script_injection.cc中。
ScriptInjection类的成员函数InjectJS首先遍历将要执行的每一个JavaScript文件。在遍历的过程中,每一个JavaScript文件的内容都会被读取出来,并且封装在一个blink::WebScriptSource对象中。这些blink::WebScriptSource对象最后又会保存在本地变量sources描述的一个blink::WebScriptSource向量中。这个blink::WebScriptSource向量接下来会传递给WebKit中的JS引擎。JS引擎获得了这个向量之后,就会执行保存在里面的JavaScript。
Extension的Content Script是在一个Isolated World中执行的,这意味着它们不能访问在宿主网页中定义的JS变量和函数,也不能访问在宿主网页中注入的其它Extension的Content Script定义的JS变量和函数。为此,Chromium会为每一个Extension分配一个不同的Isolated World ID,使得它们的Content Script注入到宿主网页时,既不能互相访问各自定义的JS变量和函数,也不能访问宿主网页定义的JS变量和函数。
按照上述规则,ScriptInjection类的成员函数InjectJS在注入指定的JavaScript到宿主网页中执行之前,首先会调用成员变量user_script_slave_指向的一个UserScriptSlave对象的成员函数GetIsolatedWorldIdForExtension获得一个Isolated World ID。有了这个Isolated World ID之后,就可以调用参数frame指向的一个WebLocalFrameImpl对象的成员函数executeScriptInIsolatedWorld了。
WebLocalFrameImpl类是WebKit向Chromium提供的一个API接口。这个API接口的成员函数executeScriptInIsolatedWorld会将指定的JavaScript交给WebKit内部使用的JS引擎在指定的Isolated World中执行。在Chromium中,这个JS引擎就是V8引擎。V8引擎执行JavaScript的过程,以后我们有机会再分析。
至此,我们就分析完成Extension的Content Script注入到宿主网页中执行的过程了。这些Content Script虽然是在宿主网页中执行,但是它们是不能访问宿主网页定义的JS变量和函数的,也不能访问注入在宿主网页中的其它Extension的Content Script定义的JS变量和函数。不过,它们却可以操作宿主网页的DOM Tree,这样就可以修改宿主网页的UI和行为了。
如果我们将Extension看作是一个App,那么它的Page和Content Script就可以看作是它的Module。既然是Module,它们之间就避免不了互相通信,以完成一个App的功能。Chromium为Extension的Page与Page之间,以及Page和Content Script之间,均提供了通信接口。不过,Page与Page之间的通信方式,与Page和Content Script之间的通信方式,是不一样的。这是因为Page运行在所属Extension加载在的Extension Process中,而Content Script运行在宿主网页所加载在的Render Process中。在接下来一篇文章中,我们就继续分析Extension的Page与Page之间,以及Page与Content Script之间的通信机制,敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo。
扫一扫
在手机上阅读