iOS 开源库源码分析之react-native
此文基于 react-native-0.53.0
react-native 是 facebook 于 2015.3.27 发布的一款构建移动 native app 的框架,使用的 JavaScript 和 React。
本文主要讲述 react-native 在 iOS 端的内容,包括 react-native 的加载流程,JS 与 native 互调的过程,UI 渲染的步骤以及如何传递 native 事件。
加载流程
- RCTBridge 的初始化
- batchedBridge(RCTCxxBridge) 调用 start
- 通过 RCTJavaScriptLoader 的 loadBundleAtURL 方法把 url 解析成 data
- 获取到数据后,调用 RCTCxxBridge 的 executeApplicationScript
- 调用 JSCExecutor 的 loadApplicationScript 方法
- 在 loadApplicationScript 里面会调用 JSCHelplers 的 evaluateScript 方法,传入 JSContextRef 和前面的 data 转化成的脚本字符串 script,以及 sourceURL,这里其实最终调用的就是 JavaScriptCore 的 JSBase 类的 JSEvaluateScript 函数
补充RCTBridge的几个通知的时机
- RCTJavaScriptWillStartLoadingNotification
RCTCxxBridge 调用start的时候
- RCTJavaScriptWillStartExecutingNotification ,表示bridge开始执行JS bundle
RCTCxxBridge调用executeApplicationScript的时候
- RCTJavaScriptDidLoadNotification , 表示bridge 完成load JS bundle
RCTJavaScriptLoader loadBundleAtURL 成功之后调用
- RCTJavaScriptDidFailToLoadNotification,表示bridge load JS bundle 失败
RCTJavaScriptLoader loadBundleAtURL 失败之后调用
- RCTBridgeWillDownloadScriptNotification ,表示bridge开始下载一个script
RCTJavaScriptLoader loadBundleAtURL 调用前发出该通知
- RCTBridgeDidDownloadScriptNotification
RCTJavaScriptLoader loadBundleAtURL完成之后调用,在RCTJavaScriptDidLoadNotification之前
顺序是 RCTJavaScriptWillStartLoadingNotification -> RCTBridgeWillDownloadScriptNotification -> RCTBridgeDidDownloadScriptNotification -> RCTJavaScriptWillStartExecutingNotification -> RCTJavaScriptDidLoadNotification
在调用start方法里面,在loadBundleAtURL之前
[self _initModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO];
这里会注册所有的bridgemodule
JS调用native方法
native 方法能够被 JS 调用
- 一个 native 的类能够被访问首先需要注册进入 JS,是通过一个宏 RCT_EXPORT_MODULE(js_name),会把 js_name(类名)加入在 RCTBridge 的 RCTModuleClasses(数组,模块配置列表)中
- 然后会把 RCTModuleClasses 转化为 RCTCxxBridge 的 _moduleDataByID (_moduleDataByID 的元素是RCTModuleData类,里面有methods数组)
接着在 RCTCxxBridge 的 buildModuleRegistry 中会构建出 ModuleRegistry 对象,传入 _moduleDataByID 作为其成员变量 modules
Objective-C 的对象能够被 JS 访问得益于此,
installGlobalProxy(m_context, "nativeModuleProxy", exceptionWrapMethod<&jscexecutor::getnativemodule>()); &jscexecutor::getnativemodule>最终这里会调用 JSObjectSetProperty(),传入类名和访问的 callback。 这样 JS 端在通过
var RCTAlertManager = require('NativeModules').AlertManager;
访问的时候就会回调 JSCExecutor 的 getNativeModule 方法,最终调用 JSCNativeModules 的 getModule,在这里会从 mmoduleRegistry->getConfig(name) 获取对应的结果,ModuleRegistry 的查找就是根据前面modules模块配置列表找到对应的 RCTAlertManager
ObjC 的方法能够被 JS 调用则是这样,JSCHelpers 的 installGlobalFunction(),可以设置 JS 的 function callback,JS 可以同步(在 JS 线程里面)或者异步(切换到指定线程)调用 native 方法,分别是 nativeCallSyncHook 和 nativeFlushQueueImmediate(MessageQueue.js 的 enqueueNativeCall 调用 callback 对象),这两个方法定义在NativeModules.js,callback 实现在 native 端
- nativeFlushQueueImmediate 这个方法主要是将js传过来的队列里面的需要调用的方法一起调用了。nativeCallSyncHook 这个方式主要是给JS直接同步调用ObjC方法来用的。
installNativeHook 最终是把 exceptionWrapMethod(nativeCallSyncHook) callback设置到对应的 jsName(nativeCallSyncHook) 上去,这里通过的是 JSC_JSObjectMakeFunctionWithCallback 方法(也就是 JSObjectRef 的JSObjectMakeFunctionWithCallback())
installNativeHook<&JSCExecutor::nativeFlushQueueImmediate>("nativeFlushQueueImmediate");
void JSCExecutor::installNativeHook(const char* name) {
installGlobalFunction(m_context, name, exceptionWrapMethod<method>());
}
void installGlobalFunction(
JSGlobalContextRef ctx,
const char* name,
JSObjectCallAsFunctionCallback callback) {
String jsName(ctx, name);
JSObjectRef functionObj = JSC_JSObjectMakeFunctionWithCallback(
ctx, jsName, callback);
Object::getGlobalObject(ctx).setProperty(jsName, Value(ctx, functionObj));
}
对于 exceptionWrapMethod 里面会把 installNativeHook 传进来的执行者和方法来调用 (executor->*method)(argumentCount, arguments),这样调用 callback 的时候就会调用 JSCExecutor 的 nativeCallSyncHook 或者 nativeFlushQueueImmediate 方法。
template<JSValueRef (JSCExecutor::*method)(size_t, const JSValueRef[])>
inline JSObjectCallAsFunctionCallback exceptionWrapMethod() {
struct funcWrapper {
static JSValueRef call(
JSContextRef ctx,
JSObjectRef function,
JSObjectRef thisObject,
size_t argumentCount,
const JSValueRef arguments[],
JSValueRef *exception) {
try {
auto executor = Object::getGlobalObject(ctx).getPrivate<JSCExecutor>();
if (executor && executor->getJavaScriptContext()) { // Executor not invalidated
return (executor->*method)(argumentCount, arguments);
}
} catch (...) {
*exception = translatePendingCppExceptionToJSError(ctx, function);
}
return Value::makeUndefined(ctx);
}
};
return &funcWrapper::call;
}
nativeFlushQueueImmediate 或者 nativeCallSyncHook 会调用 ModuleRegistry 的 callNativeMethod/callSerializableNativeHook 方法,在这里会去匹配模块列表,然后进行正确调用,之后调用 RCTNativeModule 的 invokeInner 方法
最后调用 RCTModuleMethod 类的 [method invokeWithBridge:bridge module:moduleData.instance arguments:objcParams]; 这里其实调用的是 runtime 的 [_invocation invokeWithTarget:module];
native调用JS方法
- native 可以调用 RCTBridge的enqueueJSCall:args: 方法来调用 JS 环境的代码
- 然后会调用到 NativeToJsBridge.cpp 的 callFunction 方法
- 然后调用 JSCExecutor 的 callFunction 方法
- 最后在 Value.cpp 中调用下面
Value Object::callAsFunction(JSObjectRef thisObj, int nArgs, const JSValueRef args[]) const { JSValueRef exn; JSValueRef result = JSC_JSObjectCallAsFunction(m_context, m_obj, thisObj, nArgs, args, &exn); if (!result) { throw JSException(m_context, exn, "Exception calling object as function"); } return Value(m_context, result); }
JSC_JSObjectCallAsFunction这里其实是通过__jsc_wrapper最终调用了JSObjectRef的JSObjectCallAsFunction函数。
RCTMessageThread
JS 是单线程的,所以在 native 里面调用 JS 或者 JS 调用 native 的回调都是在JS 线程的。
- 在 RCTBridge 初始化的时候会使用 NSThread 初始化出_jsThread实例。
- 之后通过 ensureOnJavaScriptThread 方法,在_jsThread中初始化_jsMessageThread(RCTMessageThread)
在通过 RCTCxxBridge 的 executeApplicationScript 加载 js 文件的时候,最终也会在 NativeToJsBridge 的 loadApplication 方法里面调用 m_executorMessageQueueThread->runOnQueue,来切换到_jsMessageThread。
native 调用 JS 时,在 enqueueJSCall 里面 ensureOnJavaScriptThread 来进行切换线程
- JS 通过 native 定义的 CallSyncHook 和 nativeFlushQueueImmediate 同步或者异步调用 native 方法时候都是会在 RCTMessageThread 线程回调,如果同步则直接调用,如果异步(RCTNativeModule::invoke)则会切换到 moduledata 指定的线程
moduledata是可以指定执行线程的,详见其类的setUpMethodQueue方法
if (!_methodQueue && _bridge.valid) { // Create new queue (store queueName, as it isn't retained by dispatch_queue) _queueName = [NSString stringWithFormat:@"com.facebook.react.%@Queue", self.name]; _methodQueue = dispatch_queue_create(_queueName.UTF8String, DISPATCH_QUEUE_SERIAL); }
UI渲染
在最开始 RCTUImanager 设置 bridge 的时候会给_componentDataByName赋值,这是一个以 componentData.name 为 key 的字典,保存了所有为 RCTViewManager 子类的 moduleClasses
RCTRootView 在初始化的时候,这里会把 RCTRootContentView addSubView,然后会调用 JS 端 AppRegistry 的 runApplication 方法
这个时候会收到 RootView 的 batchDidComplete 消息进行 RootView 的布局相关操作
- 之后 JS 会调用 native RCTUIManager 的 createView: 方法进行子视图的创建
在 create 方法里面会形成一个_shadowViewRegistry字典,key 为所要创建 view 的 reactTag,值为 view 对应的 RCTShadowView
然后在主线程形成一个_viewRegistry字典,key 同样为 reactTag,值则是对应的 UIView
当收到 batchDidComplete 的消息的时候,会进行子视图的 UI 布局的工作,在这里会通过 addUIBlock 把 UI 工作加入_pendingUIBlocks,最后通过 flushUIBlocksWithCompletion 把所有_pendingUIBlocks异步到主线程来做
在 flushUIBlocksWithCompletion 里面,首先会 setChildren 的 UIBlock,RCTUIManager 的 RCTSetChildren() 方法这里会根据 reactTag 通过 UIView+React 的 insertReactSubview 进行排序,之后还会设置 UIView 的相关属性,然后调用其 didUpdateReactSubviews 来 addSubView。
touch和scroll事件处理
react-native 对于 touch 和 scroll 事件是通过 RCTEventDispatcher 来处理的
- 当 native 接收到对应的事件以后,会通过 RCTEventDispatcher 的 sendEvent 来处理,这个时候会把 eventID 加入_eventQueue,_events则保存eventID对应的event
在这里如果还有未处理的事件后先 block 住,当之前的处理完之后会 dispatch 到 RCTJSThread 来处理
- 处理事件调用的是 flushEventsQueue,这里会通过 enqueueJSCall 来发送到 JS 端
说实话,跨平台开发一直是开发者热爱所在,有移动端的跨平台 react-native,有 PC 端的跨平台 electron,这些都是通过 JS 就可以实现跨平台的,当然也有其他语言也有,不过都不如 JS 应用广泛。跨平台可以减少成本,加快迭代,不用发版,再加上移动互联网的发展,JS 语言的魅力,所以说 react-native 才可以大放异彩。本文只是 react-native 的冰山一角,感兴趣的朋友可以多多了解。
本文作者coderyi