Chromium的Extension由Page和Content Script组成。如果将Extension看作是一个App,那么Page和Content Script就是Extension的Module。既然是Module,就避免不了需要相互通信。也正是由于相互通信,使得它们形成一个完整的App。本文接下来就分析Extension的Page之间以及Page与Content Script之间的通信机制。
从前面Chromium扩展(Extension)的页面(Page)加载过程分析和Chromium扩展(Extension)的Content Script加载过程分析这两篇文章可以知道,Extension的Page,实际上就是Web Page,它们加载在同一个Extension Process中,而Extension的Content Script,实际上是JavaScript,它们加载在宿主网页所在的Render Process中。这意味着Extension的Page之间,可以进行进程内通信,但是Page与Content Script之间,需要进行进程间通信。
Chromium的Extension模块提供了接口,让Extension的Page与Page之间,以及Page与Content Script之间,可以方便地通信,如图1所示:
图1 Extension的通信机制
Extension的Page之间的通信,表现为可以访问各自定义的JS变量和函数。例如,我们在前面Chromium扩展(Extension)机制简要介绍和学习计划一文中提到的Page action example,定义了一个Background Page和一个Popup Page。其中,Background Page包含了的一个background.js,它的内容如下所示:
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
......
var views = chrome.extension.getViews({type: "tab"});
if (views.length > 0) {
console.log(views[0].whoiam);
} else {
console.log("No tab");
}
});
......
var whoiam = "background.html"
它定义了一个变量whoiam,同时它又会通过Extension模块提供的API接口chrome.extension.getViews,获得在浏览器窗口的Tab中加载的所有Extension Page的window对象。假设此时Page action example在Tab中加载了一个Extension Page,并且这个Page也像Background Page一样定义了变量whoiam,那么Background Page就可以通过它的window对象直接访问它的变量whoiam。
API接口chrome.extension.getViews除了可以获得在Tab中加载的Page的window对象,还可以获得以其它方式加载的Page的window对象。例如,在弹窗口中加载的Popup Page的window对象,以及在浏览器的Info Bar(信息栏)和Notification(通知面板)中加载的Page的window对象。可以通过type参数指定要获取哪一种类型的Page的window对象。如果没有指定,那么就会获得所有类型的Page的window对象。
注意,API接口chrome.extension.getViews获得的是非Background Page的window对象。如果需要获得Background Page的window对象,可以使用另外一个API接口chrome.extension.getBackgroundPage。例如,我们在前面Chromium扩展(Extension)机制简要介绍和学习计划一文中提到的Page action example的Popup Page,包含有一个popup.js,它的内容如下所示:
document.addEventListener('DOMContentLoaded', function() {
getImageUrl(function(imageUrl, width, height) {
var imageResult = document.getElementById('image-result');
imageResult.width = width;
imageResult.height = height;
imageResult.src = imageUrl;
imageResult.hidden = false;
console.log(chrome.extension.getBackgroundPage().whoiam);
}, function(errorMessage) {
renderStatus('Cannot display image. ' + errorMessage);
});
......
});
var whoiam = "popup.html"
它在Popup Page中显示图片时,就可以通过调用API接口chrome.extension.getBackgroundPage获得Background Page的window对象,然后通过这个window对象访问在Background Page中定义的变量whoiam。从前面的定义可以知道,这个变量的值等于"background.html"。
总结来说,就是Extension的Page之间,可以通过chrome.extension.getViews和chrome.extension.getBackgroundPage这两个API接口获得对方的window对象。有了对方的window对象之后,就可以直接进行通信了。
由于Extension的Page和Content Script不在同一个进程,它们的通信过程就会复杂一些。总体来说,是通过消息进行通信的。接下来我们以Popup Page与Content Script的通信为例,说明Extension的Page和Content Script的通信过程。
在前面Chromium扩展(Extension)机制简要介绍和学习计划一文中提到的Page action example,它的Popup Page可以通过API接口chrome.tabs.sendRequest向Content Script发送请求,如下所示:
function testRequest() {
chrome.tabs.getSelected(null, function(tab) {
chrome.tabs.sendRequest(tab.id, {counter: counter}, function handler(response) {
counter = response.counter;
document.querySelector('#resultsRequest').innerHTML = "<font color='gray'> response: " + counter + "</font>";
document.querySelector('#testRequest').innerText = "send " + (counter -1) + " to tab page";
});
});
}
这个请求会被封装在一个类型为ExtensionHostMsg_PostMessage的IPC消息,并且发送给Browser进程。Browser进程会找到Page action example的Content Script的宿主网页所在的Render进程,并且将请求封装成另外一个类型为ExtensionMsg_DeliverMessage的IPC消息发送给它。
Render进程收到类型为ExtensionMsg_DeliverMessage的IPC消息后,就会将封装在里面的请求提取出来,并且交给Content Script处理,如下所示:
chrome.extension.onRequest.addListener(
function(request, sender, sendResponse) {
sendResponse({counter: request.counter + 1 });
}
);
Content Script需要通过API接口chrome.extension.onRequest.addListener注册一个函数,用来接收来自Extension Page的请求。这个函数的第三个参数是一个Callback函数。通过这个Callback函数,Content Script可以向Background Page发送Response。
Extension的Content Script同样也可以向Extension的Page发送请求。不过,它是通过另外一个API接口chrome.runtime.sendMessage进行发送的。例如,上述Page action example的Content Script就是通过这个接口向Background Page发送请求的,如下所示:
function testRequest() {
chrome.runtime.sendMessage({counter: counter}, function(response) {
counter = response.counter;
document.querySelector('#resultsRequest').innerText = "response: " + counter;
document.querySelector('#testRequest').innerText = "send " + (counter -1) + " to background page";
});
}
这个请求同样是先通过一个类型为ExtensionHostMsg_PostMessage的IPC消息传递到Browser进程,然后再由Browser进程通过另外一个类型为ExtensionMsg_DeliverMessage的IPC消息传递给Extension进程中的Background Page。
Background Page需要通过API接口chrome.runtime.onMessage.addListener注册一个函数,用来接收来自Content Script的请求,如下所示:
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
sendResponse({counter: request.counter + 1 });
}
);
这个函数的第三个参数同样是一个Callback函数。通过这个Callback函数,Background Page可以向Content Script发送Response。
总结来说,就是Extension的Page可以通过API接口chrome.tabs.sendRequest和chrome.extension.onRequest.addListener与Content Script通信,而Content Script可以通过API接口chrome.runtime.sendMessage和chrome.runtime.onMessage.addListener与Page通信。
接下来,我们结合源代码分析chrome.extension.getViews、chrome.extension.getBackgroundPage和chrome.runtime.sendMessage这三个API接口的实现。了解这三个API接口的实现之后,我们就会对上述的Extension通信机制,有更深刻的认识。
在分析上述三个API接口之前,我们首先简单介绍一下JS Binding。JS Binding类型于Java里面的JNI,用来在JS与C/C++之间建立桥梁,也就是用来将一个JS接口绑定到一个C/C++函数中去。
Chromium使用的JS引擎是V8。V8引擎在创建完成Script Context之后,会向WebKit发出通知。WebKit再向Chromium的Content模块发出通知。Content模块又会向Extension模块发出通知。Extension模块获得通知后,就会将chrome.extension.getViews、chrome.extension.getBackgroundPage和chrome.runtime.sendMessage接口绑定到Chromium内部定义的函数中去,从而使得它们可以通过Chromium的基础设施来实现Extension的通信机制。
V8引擎的初始化发生在V8WindowShell类的成员函数initialize中,它的实现如下所示:
bool V8WindowShell::initialize()
{
TRACE_EVENT0("v8", "V8WindowShell::initialize");
......
createContext();
......
ScriptState::Scope scope(m_scriptState.get());
v8::Handle<v8::Context> context = m_scriptState->context();
......
m_frame->loader().client()->didCreateScriptContext(context, m_world->extensionGroup(), m_world->worldId());
return true;
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/bindings/v8/V8WindowShell.cpp中。
V8WindowShell类的成员函数initialize在初始化完成V8引擎之后,会通过调用成员变量m_frame指向的一个LocalFrame对象的成员函数loader获得一个FrameLoader对象。有了这个FrameLoader对象之后,再调用它的成员函数client就可以获得一个FrameLoaderClientImpl对象。有了这个FrameLoaderClientImpl对象,就可以调用它的成员函数didCreateScriptContext向WebKit发出通知,V8引擎的Script Context创建好了。
FrameLoaderClientImpl类的成员函数didCreateScriptContext的实现如下所示:
void FrameLoaderClientImpl::didCreateScriptContext(v8::Handle<v8::Context> context, int extensionGroup, int worldId)
{
WebViewImpl* webview = m_webFrame->viewImpl();
......
if (m_webFrame->client())
m_webFrame->client()->didCreateScriptContext(m_webFrame, context, extensionGroup, worldId);
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/FrameLoaderClientImpl.cpp中。
FrameLoaderClientImpl类的成员变量m_webFrame指向的是一个WebLocalFrameImpl对象。FrameLoaderClientImpl类的成员函数didCreateScriptContext首先调用这个WebLocalFrameImpl对象的成员函数viewImpl获得一个WebViewImpl对象。有了这个WebViewImpl对象之后,再调用它的成员函数client可以获得一个RenderFrameImpl对象。这个RenderFrameImpl对象实现了WebViewClient接口,它是从Chromium的Content层设置进来的,作为WebKit回调Content的接口。因此,有了这个RenderFrameImpl对象之后,FrameLoaderClientImpl类的成员函数didCreateScriptContext就可以调用它的成员函数didCreateScriptContext,用来通知它V8引擎的Script Context创建好了。
RenderFrameImpl类的成员函数didCreateScriptContext的实现如下所示:
void RenderFrameImpl::didCreateScriptContext(blink::WebLocalFrame* frame,
v8::Handle<v8::Context> context,
int extension_group,
int world_id) {
DCHECK(!frame_ || frame_ == frame);
GetContentClient()->renderer()->DidCreateScriptContext(
frame, context, extension_group, world_id);
}
这个函数定义在文件external/chromium_org/content/renderer/render_frame_impl.cc中。
我们假设当前基于Chromium实现的浏览器为Chrome。这时候RenderFrameImpl类的成员函数didCreateScriptContext调用函数GetContentClient获得的是一个ChromeContentClient对象。有了这个ChromeContentClient对象之后,调用它的成员函数renderer可以获得一个ChromeContentRendererClient对象。有了这个ChromeContentRendererClient对象之后,RenderFrameImpl类的成员函数didCreateScriptContext就可以调用它的成员函数DidCreateScriptContext,用来通知它V8引擎的Script Context创建好了。
ChromeContentRendererClient类的成员函数DidCreateScriptContext的实现如下所示:
void ChromeContentRendererClient::DidCreateScriptContext(
WebFrame* frame, v8::Handle<v8::Context> context, int extension_group,
int world_id) {
extension_dispatcher_->DidCreateScriptContext(
frame, context, extension_group, world_id);
}
这个函数定义在文件external/chromium_org/chrome/renderer/chrome_content_renderer_client.cc中。
ChromeContentRendererClient类的成员变量extension_dispatcher_指向的是一个Dispatcher对象。这个Dispatcher对象就是我们在前面Chromium扩展(Extension)的Content Script加载过程分析一文中提到的那个用来接收Extension相关的IPC消息的Dispatcher对象,ChromeContentRendererClient类的成员函数DidCreateScriptContext调用所做的事情就是调用它的成员函数DidCreateScriptContext,用来通知它V8引擎的Script Context创建好了。
Dispatcher类的成员函数DidCreateScriptContext的实现如下所示:
void Dispatcher::DidCreateScriptContext(
WebFrame* frame,
const v8::Handle<v8::Context>& v8_context,
int extension_group,
int world_id) {
......
std::string extension_id = GetExtensionID(frame, world_id);
const Extension* extension = extensions_.GetByID(extension_id);
......
Feature::Context context_type =
ClassifyJavaScriptContext(extension,
extension_group,
ScriptContext::GetDataSourceURLForFrame(frame),
frame->document().securityOrigin());
ScriptContext* context =
delegate_->CreateScriptContext(v8_context, frame, extension, context_type)
.release();
......
{
scoped_ptr<ModuleSystem> module_system(
new ModuleSystem(context, &source_map_));
context->set_module_system(module_system.Pass());
}
ModuleSystem* module_system = context->module_system();
......
RegisterNativeHandlers(module_system, context);
......
}
这个函数定义在文件external/chromium_org/extensions/renderer/dispatcher.cc中。
参数frame描述的是当前加载的网页,另外一个参数world_id描述的是V8引擎中的一个Isolated World ID。从前面Chromium扩展(Extension)的Content Script加载过程分析一文可以知道,Isolated World是用来执行Extension的Content Script的,并且每一个Extension在其宿主网页中都有一个唯一的Isolated World。这意味着根据这个Isolated World ID可以获得它所对应的Extension。这可以通过调用Dispatcher类的成员函数GetExtensionID获得。
知道了参数world_id描述的Isolated World对应的Extension之后,Dispatcher类的成员函数DidCreateScriptContext就可以为这个Extension创建一个Script Context。这个Script Context实际上只是对参数v8_context描述的V8 Script Context进行封装。
创建上述Script Context的目的是创建一个Module System。通过这个Module System,可以向参数world_id描述的Isolated World注册Native Handler。Native Handler的作用就是创建JS Binding。有了这些JS Binding之后,我们就可以在Content Script中调用Extension相关的API接口了。
Dispatcher类的成员函数DidCreateScriptContext最后是通过调用另外一个成员函数RegisterNativeHandlers向参数world_id描述的Isolated World注册Native Handler的,它的实现如下所示:
void Dispatcher::RegisterNativeHandlers(ModuleSystem* module_system,
ScriptContext* context) {
......
module_system->RegisterNativeHandler(
"messaging_natives",
scoped_ptr<NativeHandler>(MessagingBindings::Get(this, context)));
......
module_system->RegisterNativeHandler(
"runtime", scoped_ptr<NativeHandler>(new RuntimeCustomBindings(context)));
......
}
这个函数定义在文件external/chromium_org/extensions/renderer/dispatcher.cc中。
Dispatcher类的成员函数RegisterNativeHandlers注册了一系列的Native Handler。每一个Native Handler都对应有一个名称。这个名称在JS中称为Module,可以通过JS函数require进行引用。这一点后面我们就会看到它的用法。
这里我们只关注与前面提到的API接口chrome.extension.getViews、chrome.extension.getBackgroundPage和chrome.runtime.sendMessage相关的两个Module:"message_natives"和"runtime"。
其中,名称为"message_natives"的Module使用的Native Handler是一个ExtensionImpl对象。这个ExtensionImpl对象是通过调用MessagingBindings类的静态成员函数Get创建的,如下所示:
ObjectBackedNativeHandler* MessagingBindings::Get(Dispatcher* dispatcher,
ScriptContext* context) {
return new ExtensionImpl(dispatcher, context);
}
这个函数定义在文件external/chromium_org/extensions/renderer/messaging_bindings.cc中。
这个ExtensionImpl对象在创建的时候,就会为名称为"message_natives"的Module导出的JS函数创建Binding,如下所示:
class ExtensionImpl : public ObjectBackedNativeHandler {
public:
ExtensionImpl(Dispatcher* dispatcher, ScriptContext* context)
: ObjectBackedNativeHandler(context), dispatcher_(dispatcher) {
......
RouteFunction(
"PostMessage",
base::Bind(&ExtensionImpl::PostMessage, base::Unretained(this)));
......
}
......
};
这个类定义在文件external/chromium_org/extensions/renderer/messaging_bindings.cc中。
从这里可以看到,名称为"message_natives"的Module导出了一个名称为"PostMessage"的JS函数。这个JS函数绑定了ExtensionImpl类的成员函数PostMessage。这意味着以后我们在JS中调用了messaging_natives.PostMessage函数时,ExtensionImpl类的成员函数PostMessage就会被调用。
回到Dispatcher类的成员函数RegisterNativeHandlers中,它注册的另外一个名称为"runtime"的Module使用的Native Handler是一个RuntimeCustomBindings对象。这个RuntimeCustomBindings对象在创建的过程中,就会为名称为"runtime"的Module导出的JS函数创建Binding,如下所示:
RuntimeCustomBindings::RuntimeCustomBindings(ScriptContext* context)
: ObjectBackedNativeHandler(context) {
......
RouteFunction("GetExtensionViews",
base::Bind(&RuntimeCustomBindings::GetExtensionViews,
base::Unretained(this)));
}
这个类定义在文件external/chromium_org/extensions/renderer/runtime_custom_bindings.cc中。
这里可以看到,名称为"runtime"的Module导出了一个名称为"GetExtensionViews"的JS函数。这个JS函数绑定了RuntimeCustomBindings类的成员函数GetExtensionViews。这意味着以后我们在JS中调用了runtime.GetExtensionViews函数时,RuntimeCustomBindings类的成员函数GetExtensionViews就会被调用。
有了以上JS Binding相关的背景知识之后,接下来我们就开始分析Chromium的Extension提供的API接口chrome.extension.getViews、chrome.extension.getBackgroundPage和chrome.runtime.sendMessage的实现了。
我们首先分析chrome.extension.getViews和chrome.extension.getBackgroundPage这两个API接口的实现,如下所示:
var binding = require('binding').Binding.create('extension');
.....
var runtimeNatives = requireNative('runtime');
var GetExtensionViews = runtimeNatives.GetExtensionViews;
binding.registerCustomHook(function(bindingsAPI, extensionId) {
......
var apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setHandleRequest('getViews', function(properties) {
var windowId = WINDOW_ID_NONE;
var type = 'ALL';
if (properties) {
if (properties.type != null) {
type = properties.type;
}
if (properties.windowId != null) {
windowId = properties.windowId;
}
}
return GetExtensionViews(windowId, type);
});
......
apiFunctions.setHandleRequest('getBackgroundPage', function() {
return GetExtensionViews(-1, 'BACKGROUND')[0] || null;
});
......
});
这两个JS接口定义在文件external/chromium_org/extensions/renderer/resources/extension_custom_bindings.js中。
从这里可以看到,chrome.extension.getViews和chrome.extension.getBackgroundPage这两个API接口都是通过调用名称为"runtime"的Module导出的函数GetExtensionViews(即runtime.GetExtensionViews)实现的。不过,后者在调用函数runtime.GetExtensionViews时,两个参数被固定为-1和"BACKGROUND",表示要获取的是Background Page的window对象。
从前面的分析可以知道,函数runtime.GetExtensionViews绑定到了RuntimeCustomBindings类的成员函数GetExtensionViews。这意味着,当我们在JS中调用chrome.extension.getViews和chrome.extension.getBackgroundPage这两个API接口时,最后会调用到C++层的RuntimeCustomBindings类的成员函数GetExtensionViews。它的实现如下所示:
void RuntimeCustomBindings::GetExtensionViews(
const v8::FunctionCallbackInfo<v8::Value>& args) {
if (args.Length() != 2)
return;
if (!args[0]->IsInt32() || !args[1]->IsString())
return;
// |browser_window_id| == extension_misc::kUnknownWindowId means getting
// all views for the current extension.
int browser_window_id = args[0]->Int32Value();
std::string view_type_string = *v8::String::Utf8Value(args[1]->ToString());
StringToUpperASCII(&view_type_string);
// |view_type| == VIEW_TYPE_INVALID means getting any type of
// views.
ViewType view_type = VIEW_TYPE_INVALID;
if (view_type_string == kViewTypeBackgroundPage) {
view_type = VIEW_TYPE_EXTENSION_BACKGROUND_PAGE;
} else if (view_type_string == kViewTypeInfobar) {
view_type = VIEW_TYPE_EXTENSION_INFOBAR;
} else if (view_type_string == kViewTypeTabContents) {
view_type = VIEW_TYPE_TAB_CONTENTS;
} else if (view_type_string == kViewTypePopup) {
view_type = VIEW_TYPE_EXTENSION_POPUP;
} else if (view_type_string == kViewTypeExtensionDialog) {
view_type = VIEW_TYPE_EXTENSION_DIALOG;
} else if (view_type_string == kViewTypeAppWindow) {
view_type = VIEW_TYPE_APP_WINDOW;
} else if (view_type_string == kViewTypePanel) {
view_type = VIEW_TYPE_PANEL;
} else if (view_type_string != kViewTypeAll) {
return;
}
std::string extension_id = context()->GetExtensionID();
if (extension_id.empty())
return;
std::vector<content::RenderView*> views = ExtensionHelper::GetExtensionViews(
extension_id, browser_window_id, view_type);
v8::Local<v8::Array> v8_views = v8::Array::New(args.GetIsolate());
int v8_index = 0;
for (size_t i = 0; i < views.size(); ++i) {
v8::Local<v8::Context> context =
views[i]->GetWebView()->mainFrame()->mainWorldScriptContext();
if (!context.IsEmpty()) {
v8::Local<v8::Value> window = context->Global();
DCHECK(!window.IsEmpty());
v8_views->Set(v8::Integer::New(args.GetIsolate(), v8_index++), window);
}
}
args.GetReturnValue().Set(v8_views);
}
这个函数定义在文件external/chromium_org/extensions/renderer/runtime_custom_bindings.cc中。
RuntimeCustomBindings类的成员函数GetExtensionViews首先将从JS传递过来的参数(Page Window ID和Page Type)提取出来,并且获得当前正在调用的Extension的ID,然后就调用ExtensionHelper类的静态成员函数GetExtensionViews获得指定的Page所加载在的Render View。
在Render进程中,每一个网页都是加载在一个Render View中的。有了Render View之后,就可以获得它在WebKit层为网页创建的V8 Script Context。有了V8 Script Context之后,就可以获得网页的window对象了。这些window对象最后会返回到JS层中去给调用者。
接下来,我们继续分析ExtensionHelper类的静态成员函数GetExtensionViews的实现,以便了解Extension Page对应的Render View的获取过程,如下所示:
std::vector<content::RenderView*> ExtensionHelper::GetExtensionViews(
const std::string& extension_id,
int browser_window_id,
ViewType view_type) {
ViewAccumulator accumulator(extension_id, browser_window_id, view_type);
content::RenderView::ForEach(&accumulator);
return accumulator.views();
}
这个函数定义在文件external/chromium_org/extensions/renderer/extension_helper.cc中。
ExtensionHelper类的静态成员函数GetExtensionViews通过调用RenderView类的静态成员函数ForEach遍历在当前Render进程创建的所有Render View,并且通过一个ViewAccumulator对象挑选出那些符合条件的Render View返回给调用者。
RenderView类的静态成员函数ForEach的实现如下所示:
void RenderView::ForEach(RenderViewVisitor* visitor) {
ViewMap* views = g_view_map.Pointer();
for (ViewMap::iterator it = views->begin(); it != views->end(); ++it) {
if (!visitor->Visit(it->second))
return;
}
}
这个函数定义在文件external/chromium_org/content/renderer/render_view_impl.cc中。
RenderView类的静态成员函数ForEach通过遍历全局变量g_view_map描述的一个Map可以获得当前Render进程创建的所有Render View。这些Render View将会进一步交给参数visitor描述的一个ViewAccumulator对象进行处理。
从前面Chromium网页Frame Tree创建过程分析一文可以知道,Render进程为网页创建的Render View实际上是一个RenderViewImpl对象。每一个RenderViewImpl对象在创建完成后,它们的成员函数Initialize都会被调用,用来执行初始化工作。在初始化的过程,RenderViewImpl对象就会将自己保存在上述全局变量g_view_map描述的一个Map中,如下所示:
void RenderViewImpl::Initialize(RenderViewImplParams* params) {
......
g_view_map.Get().insert(std::make_pair(webview(), this));
......
}
这个函数定义在文件external/chromium_org/content/renderer/render_view_impl.cc中。
因此,前面分析的RenderView类的静态成员函数ForEach,可以通过遍历全局变量g_view_map描述的Map获得当前Render进程创建的所有Render View。
这样,我们就分析完成了chrome.extension.getViews和chrome.extension.getBackgroundPage这两个API接口的实现。接下来,我们继续分析chrome.runtime.sendMessage这个API接口的实现,如下所示:
var messaging = require('messaging');
......
binding.registerCustomHook(function(binding, id, contextType) {
......
apiFunctions.setHandleRequest('sendMessage',
function(targetId, message, options, responseCallback) {
var connectOptions = {name: messaging.kMessageChannel};
forEach(options, function(k, v) {
connectOptions[k] = v;
});
var port = runtime.connect(targetId || runtime.id, connectOptions);
messaging.sendMessageImpl(port, message, responseCallback);
});
......
});
这个JS接口定义在文件external/chromium_org/extensions/renderer/resources/runtime_custom_bindings.js中。
从这里可以看到,chrome.runtime.sendMessage这个API接口是通过调用名称为"messaging"的Module导出的函数sendMessageImpl(即messaging.sendMessageImpl)实现的。
在调用函数messaging.sendMessageImpl的时候,需要指定的一个Port。在Extension中,所有的消息都是通过通道进行传输的。这个通道就称为Port。我们可以通过调用另外一个API接口runtime.connect获得一个连接到目标通信对象的Port。有了这个Port之后,就可以向目标通信对象发送消息了。目标通信对象可以通过参数targetId描述。如果没有指定targetId,则使用默认的Port进行发送消息。这个默认的Port由runtime.id描述。
函数messaging.sendMessageImpl的实现如下所示:
function sendMessageImpl(port, request, responseCallback) {
if (port.name != kNativeMessageChannel)
port.postMessage(request);
......
function messageListener(response) {
try {
responseCallback(response);
} finally {
port.disconnect();
}
}
......
port.onMessage.addListener(messageListener);
};
这个函数定义在文件external/chromium_org/extensions/renderer/resources/messaging.js中。
从前面的调用过程可以知道,参数port描述的Port的名称被设置为kMessageChannel,它的值不等于kNativeMessageChannel。在这种情况下,函数messaging.sendMessageImpl将会调用参数port描述的Port的成员函数postMessage发送参数request描述的消息给目标通信对象,并且它会将参数responseCallback描述的一个Callback封装在一个Listener中。当目标通信对象处理完成参数request描述的消息进行Reponse时,上述Listener就会调用它内部封装的Callback,这样消息的发送方就可以得到接收方的回复了。
参数port描述的Port的成员函数postMessage的实现如下所示:
var messagingNatives = requireNative('messaging_natives');
......
PortImpl.prototype.postMessage = function(msg) {
.....
messagingNatives.PostMessage(this.portId_, msg);
};
这个函数定义在文件external/chromium_org/extensions/renderer/resources/messaging.js中。
从这里可以看到,Port类的成员函数postMessage是通过调用名称为"messaging_natives"的Module导出的函数PostMessage(即messaging_natives.PostMessage)实现的。
从前面的分析可以知道,函数messaging_natives.PostMessage绑定到了ExtensionImpl类的成员函数PostMessage。这意味中,当我们在JS中调用chrome.runtime.sendMessage这个API接口时,最后会调用到C++层的ExtensionImpl类的成员函数PostMessage。它的实现如下所示:
class ExtensionImpl : public ObjectBackedNativeHandler {
......
private:
......
// Sends a message along the given channel.
void PostMessage(const v8::FunctionCallbackInfo<v8::Value>& args) {
content::RenderView* renderview = context()->GetRenderView();
......
int port_id = args[0]->Int32Value();
......
renderview->Send(new ExtensionHostMsg_PostMessage(
renderview->GetRoutingID(), port_id,
Message(*v8::String::Utf8Value(args[1]),
blink::WebUserGestureIndicator::isProcessingUserGesture())));
}
......
};
这个函数定义在文件external/chromium_org/extensions/renderer/messaging_bindings.cc中。
ExtensionImpl类的成员函数PostMessage首先获得一个Render View。这个Render View描述的是消息发送方所属的网页。获得这个Render View的目的,是为了调用它的成员函数Send向Browser进程发送一个类型为ExtensionHostMsg_PostMessage的IPC消息。这个IPC消息封装了JS层所要发送的消息。
Browser进程通过ChromeExtensionWebContentsObserver类的成员函数OnMessageReceived接收类型为ExtensionHostMsg_PostMessage的IPC消息,如下所示:
bool ChromeExtensionWebContentsObserver::OnMessageReceived(
const IPC::Message& message) {
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(ChromeExtensionWebContentsObserver, message)
IPC_MESSAGE_HANDLER(ExtensionHostMsg_PostMessage, OnPostMessage)
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
return handled;
}
这个函数定义在文件external/chromium_org/chrome/browser/extensions/chrome_extension_web_contents_observer.cc中。
从这里可以看到,ChromeExtensionWebContentsObserver类的成员函数OnMessageReceived将类型为ExtensionHostMsg_PostMessage的IPC消息分发给另外一个成员函数OnPostMessage处理,如下所示:
void ChromeExtensionWebContentsObserver::OnPostMessage(int port_id,
const Message& message) {
MessageService* message_service = MessageService::Get(browser_context());
if (message_service) {
message_service->PostMessage(port_id, message);
}
}
这个函数定义在文件external/chromium_org/chrome/browser/extensions/chrome_extension_web_contents_observer.cc中。
ChromeExtensionWebContentsObserver类的成员函数OnPostMessage首先通过MessageService类的静态成员函数Get获得一个MessageService对象。这个MessageService对象负责管理在当前Render进程创建的所有Port。因此,有了这个MessageService对象之后,就可以调用它的成员函数PostMessage将参数message描述的消息分发给参数port_id描述的Port处理,如下所示:
void MessageService::PostMessage(int source_port_id, const Message& message) {
int channel_id = GET_CHANNEL_ID(source_port_id);
MessageChannelMap::iterator iter = channels_.find(channel_id);
......
DispatchMessage(source_port_id, iter->second, message);
}
这个函数定义在external/chromium_org/chrome/browser/extensions/api/messaging/message_service.cc中。
MessageService类的成员函数PostMessage首先根据参数source_port_id获得一个Channel ID。有了这个Channel ID之后,就可以在成员变量channels_描述的一个Map中获得一个对应的MessageChannel对象。这个MessageChannel描述的就是一个Port。因此,有了这个MessageChannel对象之后,MessageService类的成员函数PostMessage就可以调用另外一个成员函数DispatchMessage将参数message描述的消息分发给它处理,如下所示:
void MessageService::DispatchMessage(int source_port_id,
MessageChannel* channel,
const Message& message) {
// Figure out which port the ID corresponds to.
int dest_port_id = GET_OPPOSITE_PORT_ID(source_port_id);
MessagePort* port = IS_OPENER_PORT_ID(dest_port_id) ?
channel->opener.get() : channel->receiver.get();
port->DispatchOnMessage(message, dest_port_id);
}
这个函数定义在external/chromium_org/chrome/browser/extensions/api/messaging/message_service.cc中。
MessageService类的成员函数DispatchMessage首先根据参数source_port_id获得目标通信对象用来接收消息的Port的ID。这个ID就称为Dest Port ID。有了这个Dest Port ID之后,就可以通过参数channel描述的MessageChannel对象获得一个MessagePort对象。通过调用这个MessagePort对象的成员函数DispatchOnMessage,就可以将参数message描述的消息发送给目标通信对象。
上述获得的MessagePort对象的实际类型是ExtensionMessagePort。ExtensionMessagePort类重写了父类MessagePort的成员函数DispatchOnMessage。因此,MessageService类的成员函数DispatchMessage实际上是通过调用ExtensionMessagePort类的成员函数DispatchOnMessage向目标通信对象发送消息,如下所示:
void ExtensionMessagePort::DispatchOnMessage(const Message& message,
int target_port_id) {
process_->Send(new ExtensionMsg_DeliverMessage(
routing_id_, target_port_id, message));
}
这个函数定义在文件external/chromium_org/chrome/browser/extensions/api/messaging/extension_message_port.cc中。
ExtensionMessagePort类的成员变量process_指向的是一个RenderProcessHost对象。这个RenderProcessHost对象描述的是目标通信对象所在的Render进程,ExtensionMessagePort类的成员函数DispatchOnMessage通过调用它的成员函数Send可以向目标通信对象所在的Render进程发送一个类型为ExtensionMsg_DeliverMessage的IPC消息。这个IPC消息封装了参数message描述的消息。
Render进程通过ExtensionHelper类的成员函数OnMessageReceived接收类型为ExtensionMsg_DeliverMessage的IPC消息,如下所示:
bool ExtensionHelper::OnMessageReceived(const IPC::Message& message) {
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(ExtensionHelper, message)
......
IPC_MESSAGE_HANDLER(ExtensionMsg_DeliverMessage, OnExtensionDeliverMessage)
......
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
return handled;
}
这个函数定义在文件external/chromium_org/extensions/renderer/extension_helper.cc中。
从这里可以看到,ExtensionHelper类的成员函数OnMessageReceived将类型为ExtensionMsg_DeliverMessage的IPC消息分发给另外一个成员函数OnExtensionDeliverMessage处理,如下所示:
void ExtensionHelper::OnExtensionDeliverMessage(int target_id,
const Message& message) {
MessagingBindings::DeliverMessage(
dispatcher_->script_context_set(), target_id, message, render_view());
}
这个函数定义在文件external/chromium_org/extensions/renderer/extension_helper.cc中。
ExtensionHelper类的成员变量dispatcher_指向的是一个Dispatcher对象。这个Dispatcher对象在当前Render进程中是唯一的。调用这个Dispatcher对象的成员函数script_context_set可以获得一个V8 Script Context集合,其中的每一个V8 Script Context都对应有一个Page。由于当前Render进程可能加载有多个Page,每一个Page也可能会创建多个V8 Script Context,因此这里获得的V8 Script Context的个数可能大于1。
此外,在当前Render进程加载的每一个Page都对应有一个ExtensionHelper对象。对于当前正在处理的ExtensionHelper对象来说,它对应的Page可以通过它的调用成员函数render_view获得。ExtensionHelper类的成员函数OnExtensionDeliverMessage所要做的事情就是将参数message描述的消息分发给当前正在处理的ExtensionHelper对象对应的Page处理。确切地说,是将分发给该Page创建的所有V8 Script Context进行处理。这是通过调用MessagingBindings类的静态成员函数DeliverMessage实现的,如下所示:
void MessagingBindings::DeliverMessage(
const ScriptContextSet& context_set,
int target_port_id,
const Message& message,
content::RenderView* restrict_to_render_view) {
......
context_set.ForEach(
restrict_to_render_view,
base::Bind(&DeliverMessageToScriptContext, message.data, target_port_id));
}
这个函数定义在文件external/chromium_org/extensions/renderer/messaging_bindings.cc中。
MessagingBindings类的静态成员函数DeliverMessage所做的事情就是遍历参数context_set描述的V8 Script Context集合中的所有V8 Script Context。对于每一个V8 Script Context,都检查它们对应的Page是否就是参数restrict_to_render_view描述的Page。如果是的话,那么就会调用函数DeliverMessageToScriptContext将参数message描述的消息分给它处理,如下所示:
void DeliverMessageToScriptContext(const std::string& message_data,
int target_port_id,
ScriptContext* script_context) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::HandleScope handle_scope(isolate);
// Check to see whether the context has this port before bothering to create
// the message.
v8::Handle<v8::Value> port_id_handle =
v8::Integer::New(isolate, target_port_id);
v8::Handle<v8::Value> has_port =
script_context->module_system()->CallModuleMethod(
"messaging", "hasPort", 1, &port_id_handle);
CHECK(!has_port.IsEmpty());
if (!has_port->BooleanValue())
return;
std::vector<v8::Handle<v8::Value> > arguments;
arguments.push_back(v8::String::NewFromUtf8(isolate,
message_data.c_str(),
v8::String::kNormalString,
message_data.size()));
arguments.push_back(port_id_handle);
script_context->module_system()->CallModuleMethod(
"messaging", "dispatchOnMessage", &arguments);
}
这个函数定义在文件external/chromium_org/extensions/renderer/messaging_bindings.cc中。
函数DeliverMessageToScriptContext首先是检查在参数script_context描述的V8 Script Context中,是否存在一个ID等于参数target_port_id的Port。如果存在,那么就会将参数message_data描述的消息分发给它处理。这是通过调用名称为"messaging"的Module导出的JS函数dispatchOnMessage(即messaging.dispatchOnMessage)实现的。
JS函数messaging.dispatchOnMessage的定义如下所示:
// Called by native code when a message has been sent to the given port.
function dispatchOnMessage(msg, portId) {
var port = ports[portId];
if (port) {
if (msg)
msg = $JSON.parse(msg);
port.onMessage.dispatch(msg, port);
}
};
这个函数定义在文件external/chromium_org/extensions/renderer/resources/messaging.js中。
JS函数messaging.dispatchOnMessage首先根据参数portId描述的Port ID找到对应的Port。每一个Port都有一个onMessage属性。这个onMessage属性描述的是一个Event对象。这个Event对象内部维护有一个Listener列表。列表中的每一个Listener就是参数msg描述的消息的接收者。通过调用这个Event对象的成员函数dispatch即可以将参数msg描述的消息分发给它内部维护的Listener对象处理。
这样,我们就分析完成了chrome.runtime.sendMessage这个API接口的实现。与此同时,我们也分析完成了Extension的Page与Page之间,以及Page与Content Script之间的通信机制。概括来说,就是Page与Page之间通过直接访问对方定义的变量或者函数完成通信过程,而Page与Content Script之间通过消息完成通信过程。
至此,我们就分析完成了Chromium的Extension机制。从分析的过程可以知道,Extension相当于是运行在Chromium环境中的一种App。这种App由Page和Content Script组成。Page与Page之间,以及Page与Content Script之间,可以相互通信。其中,Content Script可以注入在Chromium加载的网页中执行,从而可以增加网页的功能。此外,这种App的开发语言是JavaScript,UI是HTML页面,并且通过Chromium提供的API完成自身的功能。重新学习Chromium的Extension机制,可以参考前面Chromium扩展(Extension)机制简要介绍和学习计划一文。更多的信息,也可以关注老罗的新浪微博:http://weibo.com/shengyangluo。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8