Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Chromium擴展(Extension)通信機制分析

Chromium擴展(Extension)通信機制分析

編輯:關於Android編程

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與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 = " response: " + counter + "";  
      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 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 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 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 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,
    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 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(MessagingBindings::Get(this, context)));

  ......

  module_system->RegisterNativeHandler(
      "runtime", scoped_ptr(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,如下所示:

 

lass 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& 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 views = ExtensionHelper::GetExtensionViews(
      extension_id, browser_window_id, view_type);
  v8::Local v8_views = v8::Array::New(args.GetIsolate());
  int v8_index = 0;
  for (size_t i = 0; i < views.size(); ++i) {
    v8::Local context =
        views[i]->GetWebView()->mainFrame()->mainWorldScriptContext();
    if (!context.IsEmpty()) {
      v8::Local 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 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& 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 port_id_handle =
      v8::Integer::New(isolate, target_port_id);
  v8::Handle has_port =
      script_context->module_system()->CallModuleMethod(
          "messaging", "hasPort", 1, &port_id_handle);

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

  std::vector > 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與Content Script之間,可以相互通信。其中,Content Script可以注入在Chromium加載的網頁中執行,從而可以增加網頁的功能。此外,這種App的開發語言是JavaScript,UI是HTML頁面,並且通過Chromium提供的API完成自身的功能。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved