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

Posted 罗升阳

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Chromium扩展(Extension)通信机制分析相关的知识,希望对你有一定的参考价值。

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

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

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

       从前面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

以上是关于Chromium扩展(Extension)通信机制分析的主要内容,如果未能解决你的问题,请参考以下文章

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

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

Chromium多线程通信的Closure机制分析

如何在WebView2上启用扩展名

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

Electron中页面之间的通信