Chromium扩展(Extension)通信机制分析

2657次阅读  |  发布于5年以前

Chromium的Extension由Page和Content Script组成。如果将Extension看作是一个App,那么Page和Content Script就是Extension的Module。既然是Module,就避免不了需要相互通信。也正是由于相互通信,使得它们形成一个完整的App。本文接下来就分析Extension的Page之间以及Page与Content Script之间的通信机制。

从前面Chromium扩展(Extension)的页面(Page)加载过程分析Chromium扩展(Extension)的Content Script加载过程分析这两篇文章可以知道,Extension的Page,实际上就是Web Page,它们加载在同一个Extension Process中,而Extension的Content Script,实际上是JavaScript,它们加载在宿主网页所在的Render Process中。这意味着Extension的Page之间,可以进行进程内通信,但是Page与Content Script之间,需要进行进程间通信。

Chromium的Extension模块提供了接口,让Extension的Page与Page之间,以及Page与Content Script之间,可以方便地通信,如图1所示:

图1 Extension的通信机制

Extension的Page之间的通信,表现为可以访问各自定义的JS变量和函数。例如,我们在前面Chromium扩展(Extension)机制简要介绍和学习计划一文中提到的Page action example,定义了一个Background Page和一个Popup Page。其中,Background Page包含了的一个background.js,它的内容如下所示:

chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {   
      ...... 

      var views = chrome.extension.getViews({type: "tab"});  
      if (views.length > 0) {  
        console.log(views[0].whoiam);  
      } else {  
        console.log("No tab");  
      }  
    });    

    ......  

    var whoiam = "background.html"

它定义了一个变量whoiam,同时它又会通过Extension模块提供的API接口chrome.extension.getViews,获得在浏览器窗口的Tab中加载的所有Extension Page的window对象。假设此时Page action example在Tab中加载了一个Extension Page,并且这个Page也像Background Page一样定义了变量whoiam,那么Background Page就可以通过它的window对象直接访问它的变量whoiam。

API接口chrome.extension.getViews除了可以获得在Tab中加载的Page的window对象,还可以获得以其它方式加载的Page的window对象。例如,在弹窗口中加载的Popup Page的window对象,以及在浏览器的Info Bar(信息栏)和Notification(通知面板)中加载的Page的window对象。可以通过type参数指定要获取哪一种类型的Page的window对象。如果没有指定,那么就会获得所有类型的Page的window对象。

注意,API接口chrome.extension.getViews获得的是非Background Page的window对象。如果需要获得Background Page的window对象,可以使用另外一个API接口chrome.extension.getBackgroundPage。例如,我们在前面Chromium扩展(Extension)机制简要介绍和学习计划一文中提到的Page action example的Popup Page,包含有一个popup.js,它的内容如下所示:

document.addEventListener('DOMContentLoaded', function() {  
      getImageUrl(function(imageUrl, width, height) {  
        var imageResult = document.getElementById('image-result');  
        imageResult.width = width;  
        imageResult.height = height;  
        imageResult.src = imageUrl;  
        imageResult.hidden = false;  

        console.log(chrome.extension.getBackgroundPage().whoiam);  
      }, function(errorMessage) {  
        renderStatus('Cannot display image. ' + errorMessage);  
      });  

      ......   
    });  

    var whoiam = "popup.html" 

它在Popup Page中显示图片时,就可以通过调用API接口chrome.extension.getBackgroundPage获得Background Page的window对象,然后通过这个window对象访问在Background Page中定义的变量whoiam。从前面的定义可以知道,这个变量的值等于"background.html"。

总结来说,就是Extension的Page之间,可以通过chrome.extension.getViews和chrome.extension.getBackgroundPage这两个API接口获得对方的window对象。有了对方的window对象之后,就可以直接进行通信了。

由于Extension的Page和Content Script不在同一个进程,它们的通信过程就会复杂一些。总体来说,是通过消息进行通信的。接下来我们以Popup Page与Content Script的通信为例,说明Extension的Page和Content Script的通信过程。

在前面Chromium扩展(Extension)机制简要介绍和学习计划一文中提到的Page action example,它的Popup Page可以通过API接口chrome.tabs.sendRequest向Content Script发送请求,如下所示:

function testRequest() {    
      chrome.tabs.getSelected(null, function(tab) {     
        chrome.tabs.sendRequest(tab.id, {counter: counter}, function handler(response) {    
          counter = response.counter;  
          document.querySelector('#resultsRequest').innerHTML = "<font color='gray'> response: " + counter + "</font>";  
          document.querySelector('#testRequest').innerText = "send " + (counter -1) + " to tab page";  
        });    
      });    
    }    

这个请求会被封装在一个类型为ExtensionHostMsg_PostMessage的IPC消息,并且发送给Browser进程。Browser进程会找到Page action example的Content Script的宿主网页所在的Render进程,并且将请求封装成另外一个类型为ExtensionMsg_DeliverMessage的IPC消息发送给它。

Render进程收到类型为ExtensionMsg_DeliverMessage的IPC消息后,就会将封装在里面的请求提取出来,并且交给Content Script处理,如下所示:

chrome.extension.onRequest.addListener(    
      function(request, sender, sendResponse) {    
        sendResponse({counter: request.counter + 1 });    
      }  
    );  

Content Script需要通过API接口chrome.extension.onRequest.addListener注册一个函数,用来接收来自Extension Page的请求。这个函数的第三个参数是一个Callback函数。通过这个Callback函数,Content Script可以向Background Page发送Response。

Extension的Content Script同样也可以向Extension的Page发送请求。不过,它是通过另外一个API接口chrome.runtime.sendMessage进行发送的。例如,上述Page action example的Content Script就是通过这个接口向Background Page发送请求的,如下所示:

function testRequest() {    
      chrome.runtime.sendMessage({counter: counter}, function(response) {  
        counter = response.counter;  
        document.querySelector('#resultsRequest').innerText = "response: " + counter;  
        document.querySelector('#testRequest').innerText = "send " + (counter -1) + " to background page";  
      });  
    }  

这个请求同样是先通过一个类型为ExtensionHostMsg_PostMessage的IPC消息传递到Browser进程,然后再由Browser进程通过另外一个类型为ExtensionMsg_DeliverMessage的IPC消息传递给Extension进程中的Background Page。

Background Page需要通过API接口chrome.runtime.onMessage.addListener注册一个函数,用来接收来自Content Script的请求,如下所示:

chrome.runtime.onMessage.addListener(  
      function(request, sender, sendResponse) {  
        sendResponse({counter: request.counter + 1 });   
      }  
    );  

这个函数的第三个参数同样是一个Callback函数。通过这个Callback函数,Background Page可以向Content Script发送Response。

总结来说,就是Extension的Page可以通过API接口chrome.tabs.sendRequest和chrome.extension.onRequest.addListener与Content Script通信,而Content Script可以通过API接口chrome.runtime.sendMessage和chrome.runtime.onMessage.addListener与Page通信。

接下来,我们结合源代码分析chrome.extension.getViews、chrome.extension.getBackgroundPage和chrome.runtime.sendMessage这三个API接口的实现。了解这三个API接口的实现之后,我们就会对上述的Extension通信机制,有更深刻的认识。

在分析上述三个API接口之前,我们首先简单介绍一下JS Binding。JS Binding类型于Java里面的JNI,用来在JS与C/C++之间建立桥梁,也就是用来将一个JS接口绑定到一个C/C++函数中去。

Chromium使用的JS引擎是V8。V8引擎在创建完成Script Context之后,会向WebKit发出通知。WebKit再向Chromium的Content模块发出通知。Content模块又会向Extension模块发出通知。Extension模块获得通知后,就会将chrome.extension.getViews、chrome.extension.getBackgroundPage和chrome.runtime.sendMessage接口绑定到Chromium内部定义的函数中去,从而使得它们可以通过Chromium的基础设施来实现Extension的通信机制。

V8引擎的初始化发生在V8WindowShell类的成员函数initialize中,它的实现如下所示:

bool V8WindowShell::initialize()
    {
        TRACE_EVENT0("v8", "V8WindowShell::initialize");
        ......

        createContext();
        ......

        ScriptState::Scope scope(m_scriptState.get());
        v8::Handle<v8::Context> context = m_scriptState->context();
        ......

        m_frame->loader().client()->didCreateScriptContext(context, m_world->extensionGroup(), m_world->worldId());
        return true;
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/bindings/v8/V8WindowShell.cpp中。

V8WindowShell类的成员函数initialize在初始化完成V8引擎之后,会通过调用成员变量m_frame指向的一个LocalFrame对象的成员函数loader获得一个FrameLoader对象。有了这个FrameLoader对象之后,再调用它的成员函数client就可以获得一个FrameLoaderClientImpl对象。有了这个FrameLoaderClientImpl对象,就可以调用它的成员函数didCreateScriptContext向WebKit发出通知,V8引擎的Script Context创建好了。

FrameLoaderClientImpl类的成员函数didCreateScriptContext的实现如下所示:

void FrameLoaderClientImpl::didCreateScriptContext(v8::Handle<v8::Context> context, int extensionGroup, int worldId)
    {
        WebViewImpl* webview = m_webFrame->viewImpl();
        ......
        if (m_webFrame->client())
            m_webFrame->client()->didCreateScriptContext(m_webFrame, context, extensionGroup, worldId);
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/FrameLoaderClientImpl.cpp中。

FrameLoaderClientImpl类的成员变量m_webFrame指向的是一个WebLocalFrameImpl对象。FrameLoaderClientImpl类的成员函数didCreateScriptContext首先调用这个WebLocalFrameImpl对象的成员函数viewImpl获得一个WebViewImpl对象。有了这个WebViewImpl对象之后,再调用它的成员函数client可以获得一个RenderFrameImpl对象。这个RenderFrameImpl对象实现了WebViewClient接口,它是从Chromium的Content层设置进来的,作为WebKit回调Content的接口。因此,有了这个RenderFrameImpl对象之后,FrameLoaderClientImpl类的成员函数didCreateScriptContext就可以调用它的成员函数didCreateScriptContext,用来通知它V8引擎的Script Context创建好了。

RenderFrameImpl类的成员函数didCreateScriptContext的实现如下所示:

void RenderFrameImpl::didCreateScriptContext(blink::WebLocalFrame* frame,
                                                 v8::Handle<v8::Context> context,
                                                 int extension_group,
                                                 int world_id) {
      DCHECK(!frame_ || frame_ == frame);
      GetContentClient()->renderer()->DidCreateScriptContext(
          frame, context, extension_group, world_id);
    }

这个函数定义在文件external/chromium_org/content/renderer/render_frame_impl.cc中。

我们假设当前基于Chromium实现的浏览器为Chrome。这时候RenderFrameImpl类的成员函数didCreateScriptContext调用函数GetContentClient获得的是一个ChromeContentClient对象。有了这个ChromeContentClient对象之后,调用它的成员函数renderer可以获得一个ChromeContentRendererClient对象。有了这个ChromeContentRendererClient对象之后,RenderFrameImpl类的成员函数didCreateScriptContext就可以调用它的成员函数DidCreateScriptContext,用来通知它V8引擎的Script Context创建好了。

ChromeContentRendererClient类的成员函数DidCreateScriptContext的实现如下所示:

void ChromeContentRendererClient::DidCreateScriptContext(
        WebFrame* frame, v8::Handle<v8::Context> context, int extension_group,
        int world_id) {
      extension_dispatcher_->DidCreateScriptContext(
          frame, context, extension_group, world_id);
    }

这个函数定义在文件external/chromium_org/chrome/renderer/chrome_content_renderer_client.cc中。

ChromeContentRendererClient类的成员变量extension_dispatcher_指向的是一个Dispatcher对象。这个Dispatcher对象就是我们在前面Chromium扩展(Extension)的Content Script加载过程分析一文中提到的那个用来接收Extension相关的IPC消息的Dispatcher对象,ChromeContentRendererClient类的成员函数DidCreateScriptContext调用所做的事情就是调用它的成员函数DidCreateScriptContext,用来通知它V8引擎的Script Context创建好了。

Dispatcher类的成员函数DidCreateScriptContext的实现如下所示:

void Dispatcher::DidCreateScriptContext(
        WebFrame* frame,
        const v8::Handle<v8::Context>& v8_context,
        int extension_group,
        int world_id) {
      ......

      std::string extension_id = GetExtensionID(frame, world_id);

      const Extension* extension = extensions_.GetByID(extension_id);
      ......

      Feature::Context context_type =
          ClassifyJavaScriptContext(extension,
                                    extension_group,
                                    ScriptContext::GetDataSourceURLForFrame(frame),
                                    frame->document().securityOrigin());

      ScriptContext* context =
          delegate_->CreateScriptContext(v8_context, frame, extension, context_type)
              .release();
      ......

      {
        scoped_ptr<ModuleSystem> module_system(
            new ModuleSystem(context, &source_map_));
        context->set_module_system(module_system.Pass());
      }
      ModuleSystem* module_system = context->module_system();

      ......

      RegisterNativeHandlers(module_system, context);

      ......
    }

这个函数定义在文件external/chromium_org/extensions/renderer/dispatcher.cc中。

参数frame描述的是当前加载的网页,另外一个参数world_id描述的是V8引擎中的一个Isolated World ID。从前面Chromium扩展(Extension)的Content Script加载过程分析一文可以知道,Isolated World是用来执行Extension的Content Script的,并且每一个Extension在其宿主网页中都有一个唯一的Isolated World。这意味着根据这个Isolated World ID可以获得它所对应的Extension。这可以通过调用Dispatcher类的成员函数GetExtensionID获得。

知道了参数world_id描述的Isolated World对应的Extension之后,Dispatcher类的成员函数DidCreateScriptContext就可以为这个Extension创建一个Script Context。这个Script Context实际上只是对参数v8_context描述的V8 Script Context进行封装。

创建上述Script Context的目的是创建一个Module System。通过这个Module System,可以向参数world_id描述的Isolated World注册Native Handler。Native Handler的作用就是创建JS Binding。有了这些JS Binding之后,我们就可以在Content Script中调用Extension相关的API接口了。

Dispatcher类的成员函数DidCreateScriptContext最后是通过调用另外一个成员函数RegisterNativeHandlers向参数world_id描述的Isolated World注册Native Handler的,它的实现如下所示:

void Dispatcher::RegisterNativeHandlers(ModuleSystem* module_system,
                                            ScriptContext* context) {
      ......

      module_system->RegisterNativeHandler(
          "messaging_natives",
          scoped_ptr<NativeHandler>(MessagingBindings::Get(this, context)));

      ......

      module_system->RegisterNativeHandler(
          "runtime", scoped_ptr<NativeHandler>(new RuntimeCustomBindings(context)));

      ......
    }

这个函数定义在文件external/chromium_org/extensions/renderer/dispatcher.cc中。

Dispatcher类的成员函数RegisterNativeHandlers注册了一系列的Native Handler。每一个Native Handler都对应有一个名称。这个名称在JS中称为Module,可以通过JS函数require进行引用。这一点后面我们就会看到它的用法。

这里我们只关注与前面提到的API接口chrome.extension.getViews、chrome.extension.getBackgroundPage和chrome.runtime.sendMessage相关的两个Module:"message_natives"和"runtime"。

其中,名称为"message_natives"的Module使用的Native Handler是一个ExtensionImpl对象。这个ExtensionImpl对象是通过调用MessagingBindings类的静态成员函数Get创建的,如下所示:

ObjectBackedNativeHandler* MessagingBindings::Get(Dispatcher* dispatcher,
                                                      ScriptContext* context) {
      return new ExtensionImpl(dispatcher, context);
    }

这个函数定义在文件external/chromium_org/extensions/renderer/messaging_bindings.cc中。

这个ExtensionImpl对象在创建的时候,就会为名称为"message_natives"的Module导出的JS函数创建Binding,如下所示:

class ExtensionImpl : public ObjectBackedNativeHandler {
     public:
      ExtensionImpl(Dispatcher* dispatcher, ScriptContext* context)
          : ObjectBackedNativeHandler(context), dispatcher_(dispatcher) {
        ......
        RouteFunction(
            "PostMessage",
            base::Bind(&ExtensionImpl::PostMessage, base::Unretained(this)));
        ......
      }

      ......
    };

这个类定义在文件external/chromium_org/extensions/renderer/messaging_bindings.cc中。

从这里可以看到,名称为"message_natives"的Module导出了一个名称为"PostMessage"的JS函数。这个JS函数绑定了ExtensionImpl类的成员函数PostMessage。这意味着以后我们在JS中调用了messaging_natives.PostMessage函数时,ExtensionImpl类的成员函数PostMessage就会被调用。

回到Dispatcher类的成员函数RegisterNativeHandlers中,它注册的另外一个名称为"runtime"的Module使用的Native Handler是一个RuntimeCustomBindings对象。这个RuntimeCustomBindings对象在创建的过程中,就会为名称为"runtime"的Module导出的JS函数创建Binding,如下所示:

RuntimeCustomBindings::RuntimeCustomBindings(ScriptContext* context)
        : ObjectBackedNativeHandler(context) {
      ......
      RouteFunction("GetExtensionViews",
                    base::Bind(&RuntimeCustomBindings::GetExtensionViews,
                               base::Unretained(this)));
    }

这个类定义在文件external/chromium_org/extensions/renderer/runtime_custom_bindings.cc中。

这里可以看到,名称为"runtime"的Module导出了一个名称为"GetExtensionViews"的JS函数。这个JS函数绑定了RuntimeCustomBindings类的成员函数GetExtensionViews。这意味着以后我们在JS中调用了runtime.GetExtensionViews函数时,RuntimeCustomBindings类的成员函数GetExtensionViews就会被调用。

有了以上JS Binding相关的背景知识之后,接下来我们就开始分析Chromium的Extension提供的API接口chrome.extension.getViews、chrome.extension.getBackgroundPage和chrome.runtime.sendMessage的实现了。

我们首先分析chrome.extension.getViews和chrome.extension.getBackgroundPage这两个API接口的实现,如下所示:

var binding = require('binding').Binding.create('extension');
    .....
    var runtimeNatives = requireNative('runtime');
    var GetExtensionViews = runtimeNatives.GetExtensionViews;

    binding.registerCustomHook(function(bindingsAPI, extensionId) {
      ......

      var apiFunctions = bindingsAPI.apiFunctions;

      apiFunctions.setHandleRequest('getViews', function(properties) {
        var windowId = WINDOW_ID_NONE;
        var type = 'ALL';
        if (properties) {
          if (properties.type != null) {
            type = properties.type;
          }
          if (properties.windowId != null) {
            windowId = properties.windowId;
          }
        }
        return GetExtensionViews(windowId, type);
      });

      ......

      apiFunctions.setHandleRequest('getBackgroundPage', function() {
        return GetExtensionViews(-1, 'BACKGROUND')[0] || null;
      });

      ......
    });

这两个JS接口定义在文件external/chromium_org/extensions/renderer/resources/extension_custom_bindings.js中。

从这里可以看到,chrome.extension.getViews和chrome.extension.getBackgroundPage这两个API接口都是通过调用名称为"runtime"的Module导出的函数GetExtensionViews(即runtime.GetExtensionViews)实现的。不过,后者在调用函数runtime.GetExtensionViews时,两个参数被固定为-1和"BACKGROUND",表示要获取的是Background Page的window对象。

从前面的分析可以知道,函数runtime.GetExtensionViews绑定到了RuntimeCustomBindings类的成员函数GetExtensionViews。这意味着,当我们在JS中调用chrome.extension.getViews和chrome.extension.getBackgroundPage这两个API接口时,最后会调用到C++层的RuntimeCustomBindings类的成员函数GetExtensionViews。它的实现如下所示:

void RuntimeCustomBindings::GetExtensionViews(
        const v8::FunctionCallbackInfo<v8::Value>& args) {
      if (args.Length() != 2)
        return;

      if (!args[0]->IsInt32() || !args[1]->IsString())
        return;

      // |browser_window_id| == extension_misc::kUnknownWindowId means getting
      // all views for the current extension.
      int browser_window_id = args[0]->Int32Value();

      std::string view_type_string = *v8::String::Utf8Value(args[1]->ToString());
      StringToUpperASCII(&view_type_string);
      // |view_type| == VIEW_TYPE_INVALID means getting any type of
      // views.
      ViewType view_type = VIEW_TYPE_INVALID;
      if (view_type_string == kViewTypeBackgroundPage) {
        view_type = VIEW_TYPE_EXTENSION_BACKGROUND_PAGE;
      } else if (view_type_string == kViewTypeInfobar) {
        view_type = VIEW_TYPE_EXTENSION_INFOBAR;
      } else if (view_type_string == kViewTypeTabContents) {
        view_type = VIEW_TYPE_TAB_CONTENTS;
      } else if (view_type_string == kViewTypePopup) {
        view_type = VIEW_TYPE_EXTENSION_POPUP;
      } else if (view_type_string == kViewTypeExtensionDialog) {
        view_type = VIEW_TYPE_EXTENSION_DIALOG;
      } else if (view_type_string == kViewTypeAppWindow) {
        view_type = VIEW_TYPE_APP_WINDOW;
      } else if (view_type_string == kViewTypePanel) {
        view_type = VIEW_TYPE_PANEL;
      } else if (view_type_string != kViewTypeAll) {
        return;
      }

      std::string extension_id = context()->GetExtensionID();
      if (extension_id.empty())
        return;

      std::vector<content::RenderView*> views = ExtensionHelper::GetExtensionViews(
          extension_id, browser_window_id, view_type);
      v8::Local<v8::Array> v8_views = v8::Array::New(args.GetIsolate());
      int v8_index = 0;
      for (size_t i = 0; i < views.size(); ++i) {
        v8::Local<v8::Context> context =
            views[i]->GetWebView()->mainFrame()->mainWorldScriptContext();
        if (!context.IsEmpty()) {
          v8::Local<v8::Value> window = context->Global();
          DCHECK(!window.IsEmpty());
          v8_views->Set(v8::Integer::New(args.GetIsolate(), v8_index++), window);
        }
      }

      args.GetReturnValue().Set(v8_views);
    }

这个函数定义在文件external/chromium_org/extensions/renderer/runtime_custom_bindings.cc中。

RuntimeCustomBindings类的成员函数GetExtensionViews首先将从JS传递过来的参数(Page Window ID和Page Type)提取出来,并且获得当前正在调用的Extension的ID,然后就调用ExtensionHelper类的静态成员函数GetExtensionViews获得指定的Page所加载在的Render View。

在Render进程中,每一个网页都是加载在一个Render View中的。有了Render View之后,就可以获得它在WebKit层为网页创建的V8 Script Context。有了V8 Script Context之后,就可以获得网页的window对象了。这些window对象最后会返回到JS层中去给调用者。

接下来,我们继续分析ExtensionHelper类的静态成员函数GetExtensionViews的实现,以便了解Extension Page对应的Render View的获取过程,如下所示:

std::vector<content::RenderView*> ExtensionHelper::GetExtensionViews(
        const std::string& extension_id,
        int browser_window_id,
        ViewType view_type) {
      ViewAccumulator accumulator(extension_id, browser_window_id, view_type);
      content::RenderView::ForEach(&accumulator);
      return accumulator.views();
    }

这个函数定义在文件external/chromium_org/extensions/renderer/extension_helper.cc中。

ExtensionHelper类的静态成员函数GetExtensionViews通过调用RenderView类的静态成员函数ForEach遍历在当前Render进程创建的所有Render View,并且通过一个ViewAccumulator对象挑选出那些符合条件的Render View返回给调用者。

RenderView类的静态成员函数ForEach的实现如下所示:

void RenderView::ForEach(RenderViewVisitor* visitor) {
      ViewMap* views = g_view_map.Pointer();
      for (ViewMap::iterator it = views->begin(); it != views->end(); ++it) {
        if (!visitor->Visit(it->second))
          return;
      }
    }

这个函数定义在文件external/chromium_org/content/renderer/render_view_impl.cc中。

RenderView类的静态成员函数ForEach通过遍历全局变量g_view_map描述的一个Map可以获得当前Render进程创建的所有Render View。这些Render View将会进一步交给参数visitor描述的一个ViewAccumulator对象进行处理。

从前面Chromium网页Frame Tree创建过程分析一文可以知道,Render进程为网页创建的Render View实际上是一个RenderViewImpl对象。每一个RenderViewImpl对象在创建完成后,它们的成员函数Initialize都会被调用,用来执行初始化工作。在初始化的过程,RenderViewImpl对象就会将自己保存在上述全局变量g_view_map描述的一个Map中,如下所示:

void RenderViewImpl::Initialize(RenderViewImplParams* params) {
      ......

      g_view_map.Get().insert(std::make_pair(webview(), this));

      ......
    }

这个函数定义在文件external/chromium_org/content/renderer/render_view_impl.cc中。

因此,前面分析的RenderView类的静态成员函数ForEach,可以通过遍历全局变量g_view_map描述的Map获得当前Render进程创建的所有Render View。

这样,我们就分析完成了chrome.extension.getViews和chrome.extension.getBackgroundPage这两个API接口的实现。接下来,我们继续分析chrome.runtime.sendMessage这个API接口的实现,如下所示:

var messaging = require('messaging');
    ......

    binding.registerCustomHook(function(binding, id, contextType) {
      ......

      apiFunctions.setHandleRequest('sendMessage',
          function(targetId, message, options, responseCallback) {
        var connectOptions = {name: messaging.kMessageChannel};
        forEach(options, function(k, v) {
          connectOptions[k] = v;
        });
        var port = runtime.connect(targetId || runtime.id, connectOptions);
        messaging.sendMessageImpl(port, message, responseCallback);
      });

      ......
    });

这个JS接口定义在文件external/chromium_org/extensions/renderer/resources/runtime_custom_bindings.js中。

从这里可以看到,chrome.runtime.sendMessage这个API接口是通过调用名称为"messaging"的Module导出的函数sendMessageImpl(即messaging.sendMessageImpl)实现的。

在调用函数messaging.sendMessageImpl的时候,需要指定的一个Port。在Extension中,所有的消息都是通过通道进行传输的。这个通道就称为Port。我们可以通过调用另外一个API接口runtime.connect获得一个连接到目标通信对象的Port。有了这个Port之后,就可以向目标通信对象发送消息了。目标通信对象可以通过参数targetId描述。如果没有指定targetId,则使用默认的Port进行发送消息。这个默认的Port由runtime.id描述。

函数messaging.sendMessageImpl的实现如下所示:

function sendMessageImpl(port, request, responseCallback) {
        if (port.name != kNativeMessageChannel)
          port.postMessage(request);

        ......

        function messageListener(response) {
          try {
            responseCallback(response);
          } finally {
            port.disconnect();
          }
        }

        ......

        port.onMessage.addListener(messageListener);
    };

这个函数定义在文件external/chromium_org/extensions/renderer/resources/messaging.js中。

从前面的调用过程可以知道,参数port描述的Port的名称被设置为kMessageChannel,它的值不等于kNativeMessageChannel。在这种情况下,函数messaging.sendMessageImpl将会调用参数port描述的Port的成员函数postMessage发送参数request描述的消息给目标通信对象,并且它会将参数responseCallback描述的一个Callback封装在一个Listener中。当目标通信对象处理完成参数request描述的消息进行Reponse时,上述Listener就会调用它内部封装的Callback,这样消息的发送方就可以得到接收方的回复了。

参数port描述的Port的成员函数postMessage的实现如下所示:

var messagingNatives = requireNative('messaging_natives');
    ......

    PortImpl.prototype.postMessage = function(msg) {
        .....
        messagingNatives.PostMessage(this.portId_, msg);
    };

这个函数定义在文件external/chromium_org/extensions/renderer/resources/messaging.js中。

从这里可以看到,Port类的成员函数postMessage是通过调用名称为"messaging_natives"的Module导出的函数PostMessage(即messaging_natives.PostMessage)实现的。

从前面的分析可以知道,函数messaging_natives.PostMessage绑定到了ExtensionImpl类的成员函数PostMessage。这意味中,当我们在JS中调用chrome.runtime.sendMessage这个API接口时,最后会调用到C++层的ExtensionImpl类的成员函数PostMessage。它的实现如下所示:

class ExtensionImpl : public ObjectBackedNativeHandler {
     ......

     private:
      ......

      // Sends a message along the given channel.
      void PostMessage(const v8::FunctionCallbackInfo<v8::Value>& args) {
        content::RenderView* renderview = context()->GetRenderView();
        ......

        int port_id = args[0]->Int32Value();
        ......

        renderview->Send(new ExtensionHostMsg_PostMessage(
            renderview->GetRoutingID(), port_id,
            Message(*v8::String::Utf8Value(args[1]),
                    blink::WebUserGestureIndicator::isProcessingUserGesture())));
      }

      ......
    };

这个函数定义在文件external/chromium_org/extensions/renderer/messaging_bindings.cc中。

ExtensionImpl类的成员函数PostMessage首先获得一个Render View。这个Render View描述的是消息发送方所属的网页。获得这个Render View的目的,是为了调用它的成员函数Send向Browser进程发送一个类型为ExtensionHostMsg_PostMessage的IPC消息。这个IPC消息封装了JS层所要发送的消息。

Browser进程通过ChromeExtensionWebContentsObserver类的成员函数OnMessageReceived接收类型为ExtensionHostMsg_PostMessage的IPC消息,如下所示:

bool ChromeExtensionWebContentsObserver::OnMessageReceived(
        const IPC::Message& message) {
      bool handled = true;
      IPC_BEGIN_MESSAGE_MAP(ChromeExtensionWebContentsObserver, message)
        IPC_MESSAGE_HANDLER(ExtensionHostMsg_PostMessage, OnPostMessage)
        IPC_MESSAGE_UNHANDLED(handled = false)
      IPC_END_MESSAGE_MAP()
      return handled;
    }

这个函数定义在文件external/chromium_org/chrome/browser/extensions/chrome_extension_web_contents_observer.cc中。

从这里可以看到,ChromeExtensionWebContentsObserver类的成员函数OnMessageReceived将类型为ExtensionHostMsg_PostMessage的IPC消息分发给另外一个成员函数OnPostMessage处理,如下所示:

void ChromeExtensionWebContentsObserver::OnPostMessage(int port_id,
                                                           const Message& message) {
      MessageService* message_service = MessageService::Get(browser_context());
      if (message_service) {
        message_service->PostMessage(port_id, message);
      }
    }

这个函数定义在文件external/chromium_org/chrome/browser/extensions/chrome_extension_web_contents_observer.cc中。

ChromeExtensionWebContentsObserver类的成员函数OnPostMessage首先通过MessageService类的静态成员函数Get获得一个MessageService对象。这个MessageService对象负责管理在当前Render进程创建的所有Port。因此,有了这个MessageService对象之后,就可以调用它的成员函数PostMessage将参数message描述的消息分发给参数port_id描述的Port处理,如下所示:

void MessageService::PostMessage(int source_port_id, const Message& message) {
      int channel_id = GET_CHANNEL_ID(source_port_id);
      MessageChannelMap::iterator iter = channels_.find(channel_id);
      ......

      DispatchMessage(source_port_id, iter->second, message);
    }

这个函数定义在external/chromium_org/chrome/browser/extensions/api/messaging/message_service.cc中。

MessageService类的成员函数PostMessage首先根据参数source_port_id获得一个Channel ID。有了这个Channel ID之后,就可以在成员变量channels_描述的一个Map中获得一个对应的MessageChannel对象。这个MessageChannel描述的就是一个Port。因此,有了这个MessageChannel对象之后,MessageService类的成员函数PostMessage就可以调用另外一个成员函数DispatchMessage将参数message描述的消息分发给它处理,如下所示:

void MessageService::DispatchMessage(int source_port_id,
                                         MessageChannel* channel,
                                         const Message& message) {
      // Figure out which port the ID corresponds to.
      int dest_port_id = GET_OPPOSITE_PORT_ID(source_port_id);
      MessagePort* port = IS_OPENER_PORT_ID(dest_port_id) ?
          channel->opener.get() : channel->receiver.get();

      port->DispatchOnMessage(message, dest_port_id);
    }

这个函数定义在external/chromium_org/chrome/browser/extensions/api/messaging/message_service.cc中。

MessageService类的成员函数DispatchMessage首先根据参数source_port_id获得目标通信对象用来接收消息的Port的ID。这个ID就称为Dest Port ID。有了这个Dest Port ID之后,就可以通过参数channel描述的MessageChannel对象获得一个MessagePort对象。通过调用这个MessagePort对象的成员函数DispatchOnMessage,就可以将参数message描述的消息发送给目标通信对象。

上述获得的MessagePort对象的实际类型是ExtensionMessagePort。ExtensionMessagePort类重写了父类MessagePort的成员函数DispatchOnMessage。因此,MessageService类的成员函数DispatchMessage实际上是通过调用ExtensionMessagePort类的成员函数DispatchOnMessage向目标通信对象发送消息,如下所示:

void ExtensionMessagePort::DispatchOnMessage(const Message& message,
                                                 int target_port_id) {
      process_->Send(new ExtensionMsg_DeliverMessage(
          routing_id_, target_port_id, message));
    }

这个函数定义在文件external/chromium_org/chrome/browser/extensions/api/messaging/extension_message_port.cc中。

ExtensionMessagePort类的成员变量process_指向的是一个RenderProcessHost对象。这个RenderProcessHost对象描述的是目标通信对象所在的Render进程,ExtensionMessagePort类的成员函数DispatchOnMessage通过调用它的成员函数Send可以向目标通信对象所在的Render进程发送一个类型为ExtensionMsg_DeliverMessage的IPC消息。这个IPC消息封装了参数message描述的消息。

Render进程通过ExtensionHelper类的成员函数OnMessageReceived接收类型为ExtensionMsg_DeliverMessage的IPC消息,如下所示:

bool ExtensionHelper::OnMessageReceived(const IPC::Message& message) {
      bool handled = true;
      IPC_BEGIN_MESSAGE_MAP(ExtensionHelper, message)
        ......
        IPC_MESSAGE_HANDLER(ExtensionMsg_DeliverMessage, OnExtensionDeliverMessage)
        ......
        IPC_MESSAGE_UNHANDLED(handled = false)
      IPC_END_MESSAGE_MAP()
      return handled;
    }

这个函数定义在文件external/chromium_org/extensions/renderer/extension_helper.cc中。

从这里可以看到,ExtensionHelper类的成员函数OnMessageReceived将类型为ExtensionMsg_DeliverMessage的IPC消息分发给另外一个成员函数OnExtensionDeliverMessage处理,如下所示:

void ExtensionHelper::OnExtensionDeliverMessage(int target_id,
                                                    const Message& message) {
      MessagingBindings::DeliverMessage(
          dispatcher_->script_context_set(), target_id, message, render_view());
    }

这个函数定义在文件external/chromium_org/extensions/renderer/extension_helper.cc中。

ExtensionHelper类的成员变量dispatcher_指向的是一个Dispatcher对象。这个Dispatcher对象在当前Render进程中是唯一的。调用这个Dispatcher对象的成员函数script_context_set可以获得一个V8 Script Context集合,其中的每一个V8 Script Context都对应有一个Page。由于当前Render进程可能加载有多个Page,每一个Page也可能会创建多个V8 Script Context,因此这里获得的V8 Script Context的个数可能大于1。

此外,在当前Render进程加载的每一个Page都对应有一个ExtensionHelper对象。对于当前正在处理的ExtensionHelper对象来说,它对应的Page可以通过它的调用成员函数render_view获得。ExtensionHelper类的成员函数OnExtensionDeliverMessage所要做的事情就是将参数message描述的消息分发给当前正在处理的ExtensionHelper对象对应的Page处理。确切地说,是将分发给该Page创建的所有V8 Script Context进行处理。这是通过调用MessagingBindings类的静态成员函数DeliverMessage实现的,如下所示:

void MessagingBindings::DeliverMessage(
        const ScriptContextSet& context_set,
        int target_port_id,
        const Message& message,
        content::RenderView* restrict_to_render_view) {
      ......

      context_set.ForEach(
          restrict_to_render_view,
          base::Bind(&DeliverMessageToScriptContext, message.data, target_port_id));
    }

这个函数定义在文件external/chromium_org/extensions/renderer/messaging_bindings.cc中。

MessagingBindings类的静态成员函数DeliverMessage所做的事情就是遍历参数context_set描述的V8 Script Context集合中的所有V8 Script Context。对于每一个V8 Script Context,都检查它们对应的Page是否就是参数restrict_to_render_view描述的Page。如果是的话,那么就会调用函数DeliverMessageToScriptContext将参数message描述的消息分给它处理,如下所示:

void DeliverMessageToScriptContext(const std::string& message_data,
                                       int target_port_id,
                                       ScriptContext* script_context) {
      v8::Isolate* isolate = v8::Isolate::GetCurrent();
      v8::HandleScope handle_scope(isolate);

      // Check to see whether the context has this port before bothering to create
      // the message.
      v8::Handle<v8::Value> port_id_handle =
          v8::Integer::New(isolate, target_port_id);
      v8::Handle<v8::Value> has_port =
          script_context->module_system()->CallModuleMethod(
              "messaging", "hasPort", 1, &port_id_handle);

      CHECK(!has_port.IsEmpty());
      if (!has_port->BooleanValue())
        return;

      std::vector<v8::Handle<v8::Value> > arguments;
      arguments.push_back(v8::String::NewFromUtf8(isolate,
                                                  message_data.c_str(),
                                                  v8::String::kNormalString,
                                                  message_data.size()));
      arguments.push_back(port_id_handle);
      script_context->module_system()->CallModuleMethod(
          "messaging", "dispatchOnMessage", &arguments);
    }

这个函数定义在文件external/chromium_org/extensions/renderer/messaging_bindings.cc中。

函数DeliverMessageToScriptContext首先是检查在参数script_context描述的V8 Script Context中,是否存在一个ID等于参数target_port_id的Port。如果存在,那么就会将参数message_data描述的消息分发给它处理。这是通过调用名称为"messaging"的Module导出的JS函数dispatchOnMessage(即messaging.dispatchOnMessage)实现的。

JS函数messaging.dispatchOnMessage的定义如下所示:

  // Called by native code when a message has been sent to the given port.
      function dispatchOnMessage(msg, portId) {
        var port = ports[portId];
        if (port) {
          if (msg)
            msg = $JSON.parse(msg);
          port.onMessage.dispatch(msg, port);
        }
      };

这个函数定义在文件external/chromium_org/extensions/renderer/resources/messaging.js中。

JS函数messaging.dispatchOnMessage首先根据参数portId描述的Port ID找到对应的Port。每一个Port都有一个onMessage属性。这个onMessage属性描述的是一个Event对象。这个Event对象内部维护有一个Listener列表。列表中的每一个Listener就是参数msg描述的消息的接收者。通过调用这个Event对象的成员函数dispatch即可以将参数msg描述的消息分发给它内部维护的Listener对象处理。

这样,我们就分析完成了chrome.runtime.sendMessage这个API接口的实现。与此同时,我们也分析完成了Extension的Page与Page之间,以及Page与Content Script之间的通信机制。概括来说,就是Page与Page之间通过直接访问对方定义的变量或者函数完成通信过程,而Page与Content Script之间通过消息完成通信过程。

至此,我们就分析完成了Chromium的Extension机制。从分析的过程可以知道,Extension相当于是运行在Chromium环境中的一种App。这种App由Page和Content Script组成。Page与Page之间,以及Page与Content Script之间,可以相互通信。其中,Content Script可以注入在Chromium加载的网页中执行,从而可以增加网页的功能。此外,这种App的开发语言是JavaScript,UI是HTML页面,并且通过Chromium提供的API完成自身的功能。重新学习Chromium的Extension机制,可以参考前面Chromium扩展(Extension)机制简要介绍和学习计划一文。更多的信息,也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8