整个 Weex 的工作原理大致可以用一张图来表述:
这次分享的重点在 Native Module 如何注册并切可以被 JS 调用的。
初始化
首先我们通过 [WXSDKEngine initSDKEnvironment]
初始化 SDK 环境
+ (void)initSDKEnvironment
{
……
// 获取 main.js 文件路径
NSString *filePath = [[NSBundle bundleForClass:self] pathForResource:@"main" ofType:@"js"];
// 读出 main.js 文件内容
NSString *script = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
[WXSDKEngine initSDKEnvironment:script];
……
}
+ (void)initSDKEnvironment:(NSString *)script
{
……
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 注册默认内容 这个稍后会说
[self registerDefaults];
// BridgeManager 执行 JS
[[WXSDKManager bridgeMgr] executeJsFramework:script];
});
}
让我们忽略掉一些边界处理、监控、日志的代码,只提取其中的核心代码(以下同)。Weex 将 Bundle 中的 main.js 加载,交给 Bridge Manager 执行。我们可以查看 main.js 是一个压缩后的 js 文件,其实它就是 Weex JS 的 Framework,我们调用的 Weex JS API 就是在这里定义的。那么刚刚执行它的 Bridge Manager 又是做什么的呢?
Bridge 模块
Bridge 模块就是负责 Native 与 JS 通讯的,有下面这些类组成。
WXBridgeManager
Bridge 模块还有一个非常重要的类,是在 Manager 路径下的 WXBridgeManager,它是整个模块的对外API,单例。它维护了一个线程来 Loop 处理事件,还关联了 Bridge Context,比如 register module、execute JS 等 JS 的交互操作实际都是在 Bridge Context 中执行的。下面来介绍这个 Bridge Context。
WXBridgeContext
Bridge Context 的主要功能如上文说,是给 Bridge Manager 提供与 JS 交互的能力,所以除了注册 Module、Service 等之外,就是调用 JS 方法和服务,所以它维护了 sendQueue、serviceQueue 两个队列来分别管理方法和服务的调用消息,另外还有一个 methodQueue,是来存储在 Framework 加载之前就被发送的消息,在 Framework 加载完成之后再执行。它还维持了一个 weex instance id stack,在 WXSDKManager 中有一个 id -> WXInstance 的哈希表,可以通过 id 获取到对应的 WXInstance。
WXBridgeProtocol
WXBridgeContext 真正的 JS Native 交互能力依赖于它关联的 id<WXBridgeProtocol>,这个 Protocol 的声明如下:
@protocol WXBridgeProtocol <NSObject>
@property (nonatomic, readonly) JSValue* exception;
/**
* Executes the js framework code in javascript engine
* You can do some setup in this method
*/
- (void)executeJSFramework:(NSString *)frameworkScript;
/**
* Executes the js code in javascript engine
* You can do some setup in this method
*/
- (void)executeJavascript:(NSString *)script;
/**
* Executes global js method with specific arguments
*/
- (JSValue *)callJSMethod:(NSString *)method args:(NSArray*)args;
/**
* Register callback when call native tasks occur
*/
- (void)registerCallNative:(WXJSCallNative)callNative;
/**
* Reset js engine environment, called when any environment variable is changed.
*/
- (void)resetEnvironment;
@optional
/**
* Called when garbage collection is wanted by sdk.
*/
- (void)garbageCollect;
/**
* Register callback when addElement tasks occur
*/
- (void)registerCallAddElement:(WXJSCallAddElement)callAddElement;
/**
* Register callback for global js function `callNativeModule`
*/
- (void)registerCallNativeModule:(WXJSCallNativeModule)callNativeModuleBlock;
/**
* Register callback for global js function `callNativeComponent`
*/
- (void)registerCallNativeComponent:(WXJSCallNativeComponent)callNativeComponentBlock;
@end
很显然,它提供了 Native 和 JS 交互的最基本功能,包括执行 JS 代码,调用方法,注册 Native 回调等。这个协议有两个实现类:WXJSCoreBridge 和 WXDebugLoggerBridge。其中 WXJSCoreBridge 是使用苹果的 JavaScriptCore 这个引擎来实现的。而 WXDebugLoggerBridge 则是用于调试,将 JS Bridge 对接到一个远程的 websocket 服务器,而不是本地的 JS 引擎。本地准备一个网页,运行了完整的 JS 引擎的代码,并且也可以链接到一个远程的 websocket 服务器,这样客户端的 native 层和本地网页里面的 JS 引擎就串联起来了,原本通过 JS Bridge 的双向通信内容就可以被 websocket 连接记录下来,JS 引擎中里的所有代码都可以通过本地浏览器的开发者工具进行 debug 和 console 控制。
其他
WXBridgeMethod 是其他各种 Method 的基类,他们是封装的方法模型,包括调用对象的 Ref、方法名、参数数组、weex instance 等,可以通过获取 NSInvocation 调用相应的 Native 方法。
JSValue Category 是个工具,可以将 NSInvocation 的返回值转换成 JS 对应的类型。
WXPolyfillSet 是一个 JSExport,封装了一个 NSMutableSet,供 JS 调用。
总结
整个 Weex Bridge 模块的主要类图如下所示。
Native Module 的注册和调用
以 Native Module 为例,学习一下 Weex 是如何通过 JS 调用 Native 代码的。
注册
我们通过[WXSDKEngin registerModule:withClass:]
方法来在 Weex 中注册 Module。
+ (void)registerModule:(NSString *)name withClass:(Class)clazz
{
NSString *moduleName = [WXModuleFactory registerModule:name withClass:clazz];
NSDictionary *dict = [WXModuleFactory moduleMethodMapsWithName:moduleName];
[[WXSDKManager bridgeMgr] registerModules:dict];
}
代码中先在 ModuleFactory 中注册 Module 并获取 moduleName,再从在 ModuleFactory 中获取到 moduleMethod 的哈希表,并将这个哈希表注册在 Bridge Manager 中。
下面来具体看看每一步注册具体是如何做的。
WXModuleFactory 中的注册
- (NSString *)_registerModule:(NSString *)name withClass:(Class)clazz
{
[_moduleLock lock];
//allow to register module with the same name;
WXModuleConfig *config = [[WXModuleConfig alloc] init];
config.name = name;
config.clazz = NSStringFromClass(clazz);
[config registerMethods];
[_moduleMap setValue:config forKey:name];
[_moduleLock unlock];
return name;
}
在 ModuleFactory 的 moduleMap 中,加入了一个 WXModuleConfig,WXModuleConfig 记录了 name 和 class,调用 [WXModuleConfig registerMethods]
注册了方法。
- (void)registerMethods
{
Class currentClass = NSClassFromString(_clazz);
if (!currentClass) {
WXLogWarning(@"The module class [%@] doesn't exit!", _clazz);
return;
}
while (currentClass != [NSObject class]) {
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList(object_getClass(currentClass), &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
NSString *selStr = [NSString stringWithCString:sel_getName(method_getName(methodList[i])) encoding:NSUTF8StringEncoding];
BOOL isSyncMethod = NO;
if ([selStr hasPrefix:@"wx_export_method_sync_"]) {
isSyncMethod = YES;
} else if ([selStr hasPrefix:@"wx_export_method_"]) {
isSyncMethod = NO;
} else {
continue;
}
NSString *name = nil, *method = nil;
SEL selector = NSSelectorFromString(selStr);
if ([currentClass respondsToSelector:selector]) {
method = ((NSString* (*)(id, SEL))[currentClass methodForSelector:selector])(currentClass, selector);
}
if (method.length <= 0) {
WXLogWarning(@"The module class [%@] doesn't has any method!", _clazz);
continue;
}
NSRange range = [method rangeOfString:@":"];
if (range.location != NSNotFound) {
name = [method substringToIndex:range.location];
} else {
name = method;
}
NSMutableDictionary *methods = isSyncMethod ? _syncMethods : _asyncMethods;
[methods setObject:method forKey:name];
}
free(methodList);
currentClass = class_getSuperclass(currentClass);
}
}
[WXModuleConfig registerMethods]
方法非常长,简单来说就是:先通过 while 循环遍历自己和父类的方法列表中的每一个方法,如果方法名以 wx_export_method_sync_ 或 wx_export_method_ 开头,则通过 OC runtime 获取到方法所对应的函数指针,并调用这个函数,得到 method 字符串,最后将 method 字符串放入同步或异步的方法表里。
这里面有几个疑问:wx_export_method_sync、wx_export_method 开头的方法是在哪里声明的?调用它得到的 method 字符串又是什么?
在 Weex Module 中,我们需要用 WX_EXPORT_METHOD、WX_EXPORT_METHOD_SYNC 这两个宏来声名 JS 异步和同步方法,问题应该就是在这里,所以看看这两个宏的定义。
/*
* Concatenate preprocessor tokens a and b without expanding macro definitions
* (however, if invoked from a macro, macro arguments are expanded).
*/
#define WX_CONCAT(a, b) a ## b
/*
* Concatenate preprocessor tokens a and b after macro-expanding them.
*/
#define WX_CONCAT_WRAPPER(a, b) WX_CONCAT(a, b)
#define WX_EXPORT_METHOD_INTERNAL(method, token) \
+ (NSString *)WX_CONCAT_WRAPPER(token, __LINE__) { \
return NSStringFromSelector(method); \
}
/**
* @abstract export public method
*/
#define WX_EXPORT_METHOD(method) WX_EXPORT_METHOD_INTERNAL(method,wx_export_method_)
/**
* @abstract export public method, support sync return value
* @warning the method can only be called on js thread
*/
#define WX_EXPORT_METHOD_SYNC(method) WX_EXPORT_METHOD_INTERNAL(method,wx_export_method_sync_)
这两个宏的作用就是定义了一个类方法,以 wx_export_method、wx_export_method_sync_ 加上所在代码的行数组成方法名,返回 selector 的字符串,上面的疑问得以解决。
回到刚才的步骤, 从 ModuleFactory 中获取到 moduleMethod 的哈希表,它是将 config 中的同步和异步方法都装入一个数组中,以 moduleName 为 key,数组为 value 生成一个单项哈希表。
至此,在 WXModuleFactory 中的注册已经基本完成了:moduleMap 哈希表中用 moduleName 作为 key,moduleConfig 作为 value;moduleConfig 中记录着 className 和 moduleName,同时还有两个哈希表,存储同步和异步方法。
WXBridgeManager 中的注册
Bridge Manager 是调用 Bridge Context 执行的注册。
- (void)registerModules:(NSDictionary *)modules
{
WXAssertBridgeThread();
if(!modules) return;
[self callJSMethod:@"registerModules" args:@[modules]];
}
- (void)callJSMethod:(NSString *)method args:(NSArray *)args
{
if (self.frameworkLoadFinished) {
[self.jsBridge callJSMethod:method args:args];
} else {
[_methodQueue addObject:@{@"method":method, @"args":args}];
}
}
它是调用了 JSBridge 的 callJSMethod 方法,method 为 "registerModules",args 参数是这个 Module 提供给 JS 调用的所有的同步、异步方法列表。
之前介绍了 Bridge 模块下的 JSBridgeProtocol 的两个实现类,那么以 WXJSCoreBridge 为例,callJSMethod 的方法实现如下:
- (JSValue *)callJSMethod:(NSString *)method args:(NSArray *)args
{
return [[_jsContext globalObject] invokeMethod:method withArguments:args];
}
直接调用了 JS 全局对象的registerModules
方法。那么我们就看看 JS 中的 registerModules 方法是如何定义的。
/**
* Register the name and methods of each module.
* @param {object} modules a object of modules
*/
export function registerModules (modules) {
/* istanbul ignore else */
if (typeof modules === 'object') {
initModules(modules)
}
}
/**
* init modules for an app instance
* the second param determines whether to replace an existed method
*/
export function initModules (modules, ifReplace) {
for (const moduleName in modules) {
// init `modules[moduleName][]`
let methods = nativeModules[moduleName]
if (!methods) {
methods = {}
nativeModules[moduleName] = methods
}
// push each non-existed new method
modules[moduleName].forEach(function (method) {
if (typeof method === 'string') {
method = {
name: method
}
}
if (!methods[method.name] || ifReplace) {
methods[method.name] = method
}
})
}
}
简而言之就是将 name 和方法哈希中的方法都写入了 nativeModules 这个对象中。
调用
我们在 JS 中使用weex.requireModule('xxx')
来获取 module,再调用它的方法。那么 weex 这个全局变量是哪里定义的呢?它是怎么能够被获取到的?requireModule()
又做了什么?
weex 对象
每个 weex 页面都是一个 WXSDKInstance,weex instance 在加载到 JS bundle 的代码之后,会调用renderWithMainBundleString:
方法。
- (void)_renderWithMainBundleString:(NSString *)mainBundleString
{
NSMutableDictionary *dictionary = [_options mutableCopy];
WXPerformBlockOnMainThread(^{
_rootView = [[WXRootView alloc] initWithFrame:self.frame];
_rootView.instance = self;
if(self.onCreate) {
self.onCreate(_rootView);
}
});
// ensure default modules/components/handlers are ready before create instance
[WXSDKEngine registerDefaults];
[[WXSDKManager bridgeMgr] createInstance:self.instanceId template:mainBundleString options:dictionary data:_jsData];
}
先在主线程中创建了 WXRootView,并调用了 onCreate 回调,最后调用了[WXBridgeManager createInstance:template:options:data]
,将 mainBundleString 传给了 BridgeManager。
在 BridgeManager 中切换了执行线程,将任务交给 Bridge Context 来执行。
- (void)createInstance:(NSString *)instance
template:(NSString *)temp
options:(NSDictionary *)options
data:(id)data
{
……
[self callJSMethod:@"createInstance" args:args];
}
最后 Bridge Context 调用了createInstance()
这个 JS 方法,将 instanceId、mainBundleString 等参数都传了过去。
我们看看 JS 中createInstance()
这个方法的实现。
/*
* @param {string} id
* @param {string} code
* @param {object} options
* option `HAS_LOG` enable print log
* @param {object} data
* @param {object} info { created, ... services }
*/
export function createInstance (id, code, options, data, info) {
const { services } = info || {}
resetTarget()
let instance = instanceMap[id]
/* istanbul ignore else */
options = options || {}
let result
/* istanbul ignore else */
if (!instance) {
instance = new App(id, options)
instanceMap[id] = instance
result = initApp(instance, code, data, services)
}
else {
result = new Error(`invalid instance id "${id}"`)
}
return result
}
export function init (app, code, data, services) {
//……
//……
/* istanbul ignore next */
const bundleRequireModule = name => app.requireModule(removeWeexPrefix(name))
const weexGlobalObject = {
config: app.options,
define: bundleDefine,
bootstrap: bundleBootstrap,
requireModule: bundleRequireModule,
document: bundleDocument,
Vm: bundleVm
}
Object.freeze(weexGlobalObject)
// prepare code
let functionBody
/* istanbul ignore if */
if (typeof code === 'function') {
// `function () {...}` -> `{...}`
// not very strict
functionBody = code.toString().substr(12)
}
/* istanbul ignore next */
else if (code) {
functionBody = code.toString()
}
// wrap IFFE and use strict mode
functionBody = `(function(global){\n\n"use strict";\n\n ${functionBody} \n\n})(Object.create(this))`
// ……
// run code and get result
const globalObjects = Object.assign({
define: bundleDefine,
require: bundleRequire,
bootstrap: bundleBootstrap,
register: bundleRegister,
render: bundleRender,
__weex_define__: bundleDefine, // alias for define
__weex_bootstrap__: bundleBootstrap, // alias for bootstrap
__weex_document__: bundleDocument,
__weex_require__: bundleRequireModule,
__weex_viewmodel__: bundleVm,
weex: weexGlobalObject
}, timerAPIs, services)
callFunction(globalObjects, functionBody)
return result
}
/**
* Call a new function body with some global objects.
* @param {object} globalObjects
* @param {string} code
* @return {any}
*/
function callFunction (globalObjects, body) {
const globalKeys = []
const globalValues = []
for (const key in globalObjects) {
globalKeys.push(key)
globalValues.push(globalObjects[key])
}
globalKeys.push(body)
const result = new Function(...globalKeys)
return result(...globalValues)
}
代码非常多,所以只截取了我们今天要关注的部分。
首先在createInstance()
函数中,创建了 App 对象,并将它存储在 instanceMap 中。
在init()
函数中,先定义了 weexGlobalObject,这也就是我们 JS bundle 代码中要使用的 weex 对象,其中也声名了我们需要的requireModule()
方法。而后又将传入的 mainBundleString 封装在了 IIFE 中。然后又定义了 globalObjects 对象,里面除了包含了 weex、require、bootstrap、render 等多个成员外,也包含了 timerAPIs 和 services 中的成员。
最后在callFunction()
函数中,将 globalObjects 中的所有成员都作为参数,调用了之前封装的 IIFE。至此我们的 JS bundle 代码中直接使用的 weex 实例就被这样传递过来了。
requireModule()
后面的问题就简单多了,在 App 中查看requireModule()
的实现。
/**
* @deprecated
*/
App.prototype.requireModule = function (name) {
return requireModule(this, name)
}
/**
* get a module of methods for an app instance
*/
export function requireModule (app, name) {
const methods = nativeModules[name]
const target = {}
for (const methodName in methods) {
Object.defineProperty(target, methodName, {
configurable: true,
enumerable: true,
get: function moduleGetter () {
return (...args) => app.callTasks({
module: name,
method: methodName,
args: args
})
},
set: function moduleSetter (value) {
if (typeof value === 'function') {
return app.callTasks({
module: name,
method: methodName,
args: [value]
})
}
}
})
}
return target
}
从 nativeModules 中取到之前注册时候存入的 methods 遍历,将方法的相关信息设为 target 的成员,最后将 target 返回。调用的话是直接将 module name、方法名和参数封装,传递给app.callTasks()
。
调用 native
先看看app.callTasks()
的实现。
/**
* @deprecated
*/
App.prototype.callTasks = function (tasks) {
return callTasks(this, tasks)
}
/**
* Call all tasks from an app to renderer (native).
* @param {object} app
* @param {array} tasks
*/
export function callTasks (app, tasks) {
let result
/* istanbul ignore next */
if (typof(tasks) !== 'array') {
tasks = [tasks]
}
tasks.forEach(task => {
result = app.doc.taskCenter.send(
'module',
{
module: task.module,
method: task.method
},
task.args
)
})
return result
}
对 taskCenter 发送消息,将 module、method 和 args 都传递过去。
export class TaskCenter {
……
send (type, options, args) {
const { action, component, ref, module, method } = options
args = args.map(arg => this.normalize(arg))
switch (type) {
case 'dom':
return this[action](this.instanceId, args)
case 'component':
return this.componentHandler(this.instanceId, ref, method, args, { component })
default:
return this.moduleHandler(this.instanceId, module, method, args, {})
}
}
……
}
export function init () {
const DOM_METHODS = {
createFinish: global.callCreateFinish,
updateFinish: global.callUpdateFinish,
refreshFinish: global.callRefreshFinish,
createBody: global.callCreateBody,
addElement: global.callAddElement,
removeElement: global.callRemoveElement,
moveElement: global.callMoveElement,
updateAttrs: global.callUpdateAttrs,
updateStyle: global.callUpdateStyle,
addEvent: global.callAddEvent,
removeEvent: global.callRemoveEvent
}
const proto = TaskCenter.prototype
for (const name in DOM_METHODS) {
const method = DOM_METHODS[name]
proto[name] = method ?
(id, args) => method(id, ...args) :
(id, args) => fallback(id, [{ module: 'dom', method: name, args }], '-1')
}
proto.componentHandler = global.callNativeComponent ||
((id, ref, method, args, options) =>
fallback(id, [{ component: options.component, ref, method, args }]))
proto.moduleHandler = global.callNativeModule ||
((id, module, method, args) =>
fallback(id, [{ module, method, args }]))
}
taskCenter 这个类是 JS 到 native 的桥梁,其中的 send 方法,会根据 type 对发送来的消息做不同的处理,现在的参数是 'module' 则调用moduleHandler()
。在 init() 函数中定义了moduleHandler = global.callNativeModule
,感觉就快找到目标了。
最后终于到callNativeModule
函数定义在了这里:
@implementation WXJSCoreBridge
……
- (void)registerCallNativeModule:(WXJSCallNativeModule)callNativeModuleBlock
{
_jsContext[@"callNativeModule"] = ^JSValue *(JSValue *instanceId, JSValue *moduleName, JSValue *methodName, JSValue *args, JSValue *options) {
NSString *instanceIdString = [instanceId toString];
NSString *moduleNameString = [moduleName toString];
NSString *methodNameString = [methodName toString];
NSArray *argsArray = [args toArray];
NSDictionary *optionsDic = [options toDictionary];
NSInvocation *invocation = callNativeModuleBlock(instanceIdString, moduleNameString, methodNameString, argsArray, optionsDic);
JSValue *returnValue = [JSValue wx_valueWithReturnValueFromInvocation:invocation inContext:[JSContext currentContext]];
return returnValue;
};
}
……
@end
@implementation WXBridgeContext
……
- (void)registerGlobalFunctions
{
……
[_jsBridge registerCallNativeModule:^NSInvocation*(NSString *instanceId, NSString *moduleName, NSString *methodName, NSArray *arguments, NSDictionary *options) {
WXSDKInstance *instance = [WXSDKManager instanceForID:instanceId];
WXModuleMethod *method = [[WXModuleMethod alloc] initWithModuleName:moduleName methodName:methodName arguments:arguments instance:instance];
return [method invoke];
}];
……
}
……
@end
在 WXJSCoreBridge 中定义了注册调用原生模块的方法,在 WXBridgeContext 中的注册全局函数中,对这个函数进行了注册。被调用的处理很简单,根据 moduleName、methodName 和 arguments 创建出 WXModuleMethod 并进行调用。
最后看看 WXModuleMethod 中 invoke 的具体实现。
- (NSInvocation *)invoke
{
Class moduleClass = [WXModuleFactory classWithModuleName:_moduleName];
id<WXModuleProtocol> moduleInstance = [self.instance moduleForClass:moduleClass];
BOOL isSync = NO;
SEL selector = [WXModuleFactory selectorWithModuleName:self.moduleName methodName:self.methodName isSync:&isSync];
if (![moduleInstance respondsToSelector:selector]) {
// if not implement the selector, then dispatch default module method
if ([self.methodName isEqualToString:@"addEventListener"]) {
[self.instance _addModuleEventObserversWithModuleMethod:self];
} else if ([self.methodName isEqualToString:@"removeAllEventListeners"]) {
[self.instance _removeModuleEventObserverWithModuleMethod:self];
}
return nil;
}
NSInvocation *invocation = [self invocationWithTarget:moduleInstance selector:selector];
if (isSync) {
[invocation invoke];
return invocation;
} else {
[self _dispatchInvocation:invocation moduleInstance:moduleInstance];
return nil;
}
}
先从 WXModuleFactory 中获取到 moduleClass,再在 weex instance 中获取到 moduleInstance,最后得到 invocation 对象并根据同步异步来做不同的调用,整个 module 的注册和调用流程就总算完成了。
网友评论