可能大家都知道RN的实现机制主要是两个常驻线程,一个JS线程,一个UI线程,相互通信,js处理计算,给主线程发消息渲染。基于这一点的基础上探索RN的实现细节。
首先因为不清楚RN工程中如何将JS标签转变成原生的RCTView,所以想到断点调试数据,看调用顺序是怎样的。因为涉及线程频繁的切换,断点调试的探索过程还是花费了较久的时间。
由于断点了RCTView 的init 方法发现,所有的视图创建都是有RCTUIManager调用createView方法,传代一个tag标志,一个props字典创建的view。现在问题是谁调用了RCTUIManager呢,又是如何调用的呢,看程序的调用栈发现,是由RCTBatchBridge 解析找到RCTUIManager ,并调用对应的createView方法的,打印了RTCBatchBridge的结构和内容发现:
RCT中记录了几个主要的数组,一组数组名为 _moduleDataByID,装了原生最主要的一些处理Module,其中包括了RCTUIManager 的module,长度为80,此处RCTUIManager在数组中的下标为70 ,下文中会用到这个序号。应该可以猜想到,这个数组的长度肯定可以变大。
从 RCTBatchBridge 的类结构中可以看到,记录Module的数组中装的并不是RCTUIManager这些类的实例,而是RCTModuleData的实例。
作者用 RCTModuleData 对象类来描述了一个 例如RCTUIManager Module的实例,记录了每个Module的class,暴露那些方法(NSArray),
这样RCTBatchBridge就记录了原生主要的功能Module的表了。
JS要想访问原生的Module类,一定得利用JSContext,暴露原生方法,所以在RN的工程代码中搜索关健词,找到一下一段代码:
于是在此处断点,并重新进入RN页面,发现此处穿戴的参数call数字里记录的都是数字,例如下图:
此处图上对于参数的解析是猜想,这就是最初要特意提到RCTUIManager在 RCTBatchBridge 的module数组中的下标为70的原因。
在RCTBatchBridge调用Module的实现工程中可以看到,bridge用JS传代的序号,在自己的module数组中,使用序号作为下标,取出对应的Module,并调用Module中对应方法列表的序号的function。
那么又有一个问题,原生代码,为什么可以直接使用js传代过来的序号呢,或者说,js怎么知道原生module列表中的序号对应的Module是什么呢?这个可能就需要在js代码中去找答案了。
首先原生要声明那些nativeModule要向js暴露,暴露哪些方法。我们可以先看看RCTUIManager的部分声明。
宏声明导出module
RCT_EXPORT_MODULE()
#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }
及在+load方法中注册。
暴露方法
在看原生RCTUIManager的部分export声明
前面宏生成的方法名前缀都为rct_export,原生使用动态运行时,找到改Module export的所有方法列表
以上是原生注册moduleclass和function的注册部分,那么js如何知道原生到底export了那些module呢,包括顺序如何获取呢?
我在RCTBatchBridge的start方法中看到调用了一个叫injectJSONConfiguration的方法,方法的是现实
原生调用了js的 global 对象下注册了 __fbBatchedBridgeConfig 的对象,对象打印如下:
{"remoteModuleConfig":
[["ExceptionsManager"],
["JSCExecutor"],
["ViewManager"],
["ARTNodeManager"],
["ARTGroupManager"],
["ARTRenderableManager"],
["ARTShapeManager"],
["ARTSurfaceViewManager"],
["ARTTextManager"],
["AccessibilityManager"],
["ActionSheetManager"],
["ActivityIndicatorViewManager"],
["AdSupport"],
["AlertManager"],
["AppState"],
["AssetsLibraryRequestHandler"],
["AsyncLocalStorage"],
["CameraRollManager"],
["Clipboard"],
["DataRequestHandler"],
["DatePickerManager"],
["DeviceInfo"],
["DevLoadingView"],
["DevMenu"],
["DevSettings"],
["EventDispatcher"],
["FileRequestHandler"],
["GIFImageDecoder"],
["HTTPRequestHandler"],
["I18nManager"],
["ImageEditingManager"],
["ImageLoader"],
["ImagePickerIOS"],
["ImageStoreManager"],
["ImageViewManager"],
["JSCSamplingProfiler"],
["KeyboardObserver"],
["LinkingManager"],
["LocalAssetImageLoader"],
["LocationObserver"],
["ModalHostViewManager"],
["NativeAnimatedModule"],
["NavigatorManager"],
["NavItemManager"],
["NetInfo"],
["Networking"],
["PerfMonitor"],
["PhotoLibraryImageLoader"],
["PickerManager"],
["PlatformConstants"],
["ProgressViewManager"],
["PushNotificationManager"],
["RawTextManager"],
["RedBox"],
["RefreshControlManager"],
["ScrollContentViewManager"],
["ScrollViewManager"],
["SegmentedControlManager"],
["SettingsManager"],
["SliderManager"],
["SourceCode"],
["StatusBarManager"],
["SwitchManager"],
["TabBarItemManager"],
["TabBarManager"],
["TextFieldManager"],
["TextManager"],
["TextViewManager"],
["Timing"],
["TVNavigationEventEmitter"],
["UIManager"],
["Vibration"],
["WebSocketExecutor"],
["WebSocketModule"],
["WebViewManager"],
["DevModule"],
["ImagePickerManager"],
["UiModule"],
["NetModule"],
["MapModule"]]}
也就是说,原生Module在 +load方法中将自己的class向原生RCTModuleClasses静态数组中注册,bridge创建启动时会读取RCTModuleClasses数组的值,构建原生希望暴露给JS的所有对象的Module对象数组,start方法中调用injectJSONConfiguration方法,将数据写到global对象中,供js调用。
下面是NativeModule.js中在global.__fbBatchedBridgeConfig对象中读取原生module的信息,if分支是安卓的分支,else是iOS的逻辑分支。
这样便是由原生向js传带了NativeModule的顺序。
并且在NativeModule.js中找到了应证:
在上面的js注视信息中说到
// Initially this config will only contain the module name when running in JSC. The actual
// configuration of the module will be lazily loaded.
module的具体信息是懒加载的,用的时候才会去构建,也就是,此时js只拿到了module的classname,如何拿到module的function信息呢?
我在NativeModule.js中找到loadModule的function
function loadModule(name: string, moduleID: number): ?Object {
invariant(global.nativeRequireModuleConfig,
'Can\'t lazily create module without nativeRequireModuleConfig');
const config = global.nativeRequireModuleConfig(name);
const info = genModule(config, moduleID);
return info && info.module;
}
js调用了glaobal下的nativeRequireModuleConfig的方法,传入了ModuleName,那方法实现是什么样的呢?
context[@"nativeRequireModuleConfig"] = ^NSArray *(NSString *moduleName) {
RCTJSCExecutor *strongSelf = weakSelf;
if (!strongSelf.valid) {
return nil;
}
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"nativeRequireModuleConfig", @{ @"moduleName": moduleName });
NSArray *result = [strongSelf->_bridge configForModuleName:moduleName];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"js_call,config");
return RCTNullIfNil(result);
};
从上面一段代码中可以看到,是由原生向js暴露了查询Module详细方法列表的方法,返回方法的列表。
所以js调用原生Module的方法时传带的信息是一串id的数组了。
通过以上的一些尝试算是明白了RN的大致调用的实现方式,此处只是浅显的描述了view的构建过程。其他Module的调用原理也是类似的。