首页 > 代码库 > Chromium扩展(Extension)的Content Script加载过程分析
Chromium扩展(Extension)的Content Script加载过程分析
Chromium的Extension由Page和Content Script组成。Page有UI和JS,它们加载在自己的Extension Process中渲染和执行。Content Script只有JS,这些JS是注入在宿主网页中执行的。Content Script可以访问宿主网页的DOM Tree,从而可以增强宿主网页的功能。本文接下来分析Content Script注入到宿主网页执行的过程。
老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!
我们可以在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这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。(profile_)); registrar_.Add(this, content::NOTIFICATION_RENDERER_PROCESS_CREATED, content::NotificationService::AllBrowserContextsAndSources()); }
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_service_ready_。当它的值等于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类的成员函数OnExtensionLoaded就会创建,并且在创建之后,调用它的成员函数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有可能是Java Script,也有可能是CSS Script。这意味着Extension不仅可以注入Java Script到宿主网页中,还可以注入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又可能同时包含有Java Script和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是否包含有Java Script,并且参数run_location描述的Content Script运行时机是否为包含的Java Script在清单文件中指定的运行时机。如果都是的话,那么当前正在处理的Content Script包含的Java Script就会通过调用另外一个成员函数InjectJS注入到当前加载的网页中去执行。
接下来我们只关注Java Script注入到宿主网页执行的过程,因此我们继续分析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首先遍历将要执行的每一个Java Script文件。在遍历的过程中,每一个Java Script文件的内容都会被读取出来,并且封装在一个blink::WebScriptSource对象中。这些blink::WebScriptSource对象最后又会保存在本地变量sources描述的一个blink::WebScriptSource向量中。这个blink::WebScriptSource向量接下来会传递给WebKit中的JS引擎。JS引擎获得了这个向量之后,就会执行保存在里面的Java Script。
Extension的Content Script是在一个Isolated World中执行的,这意味着它们不能访问在宿主网页中定义的JS变量和函数,也不能访问在宿主网页中注入的其它Extension的Content Script定义的JS变量和函数。为此,Chromium会为每一个Extension分配一个不同的Isolated World ID,使得它们的Content Script注入到宿主网页时,既不能互相访问各自定义的JS变量和函数,也不能访问宿主网页定义的JS变量和函数。
按照上述规则,ScriptInjection类的成员函数InjectJS在注入指定的Java Script到宿主网页中执行之前,首先会调用成员变量user_script_slave_指向的一个UserScriptSlave对象的成员函数GetIsolatedWorldIdForExtension获得一个Isolated World ID。有了这个Isolated World ID之后,就可以调用参数frame指向的一个WebLocalFrameImpl对象的成员函数executeScriptInIsolatedWorld了。
WebLocalFrameImpl类是WebKit向Chromium提供的一个API接口。这个API接口的成员函数executeScriptInIsolatedWorld会将指定的Java Script交给WebKit内部使用的JS引擎在指定的Isolated World中执行。在Chromium中,这个JS引擎就是V8引擎。V8引擎执行Java Script的过程,以后我们有机会再分析。
至此,我们就分析完成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和Content Script之间,均提供了通信接口。不过,Page之间的通信方式,与Page和Content Script之间的通信方式,是不一样的。这源于Page运行在所属Extension所加载在的Extension Process中,而Content Script运行在宿主网页所加载在的Render Process中。在接下来一篇文章中,我们就继续分析Extension的Page之间,以及Page与Content Script之间的通信机制,敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo。
Chromium扩展(Extension)的Content Script加载过程分析