Chromium扩展(Extension)的Content Script加载过程分析

Posted 罗升阳

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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,欢迎关注!

android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

       我们可以在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_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类的成员函数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

以上是关于Chromium扩展(Extension)的Content Script加载过程分析的主要内容,如果未能解决你的问题,请参考以下文章

Chromium扩展(Extension)的Content Script加载过程分析

Chromium扩展(Extension)的页面(Page)加载过程分析

如何在WebView2上启用扩展名

从安装程序安装 Opera Extension (Opera Blink)

chromium 内核浏览器刷新扩展列表

Chrome/Chromium的实验性功能+扩展推荐,让你的Chrome/Chromium起飞!