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

发表于 5年以前  | 总阅读数:2592 次

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

《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

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 目录