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

Android社区 收藏文章

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

相关标签

扫一扫

在手机上阅读