ReactNative iOS 交互

本文深入解析了React Native中JS与Native的交互流程。包括将OC类或实例函数导出给JS的步骤,动态导出Native API、生成配置表并注入JS引擎等过程,还介绍了回调函数的处理、线程问题及React的消息循环,利用懒加载机制缓解内存占用问题。

有前面的讲解,现在看这个应该不态费劲了,可以看到AppRegistry模块工厂函数中,执行了 BatchedBridge.registerCallableModule(‘AppRegistry’,AppRegistry);,把自己注册到BatchedBridge的CallableModule中,所以在上一节中,__callFunction才能在_callableModules找到AppRegistry实例,才能调用其runApplication函数。自己写的模块代码可以用React Native这种方式暴露给Natvie调用,和直接暴露的区别是,符合React Natvie的模块化原则,另外一个直观的好处是你的模块可以是懒加载的,并且不会污染全局空间。

目前终于把从N-JS的整个路径跑通了,我们梳理下整个流程看看。

[RCTBatchedBridge enqueueJSCall:@“AppRegistry.runApplication” args:[“MGReactNative”,{“rootTag”:1,“initialProps”:{}}]];

RCTJavaScriptContext callFunctionOnModule:@“AppRegistr”

                  method:@"runApplication"
               arguments:["MGReactNative",{"rootTag":1,"initialProps":{}}]
                callback:(RCTJavaScriptCallback)onComplete

//main.js

__fbBatchedBridge.callFunctionReturnFlushedQueue(“AppRegistr”,“runApplication”,[“MGReactNative”,{“rootTag”:1,“initialProps”:{}}])

//main.js

BatchedBridge.__callFunction(“AppRegistr”,“runApplication”,[“MGReactNative”,{“rootTag”:1,“initialProps”:{}}])

//main.js

var moduleMethods = BatchedBridge._callableModules[module];

if (!moduleMethods) {
  moduleMethods = require(module);
}
moduleMethods[method].apply(moduleMethods, args);

JS调用Native (JS->Native)


接下来我们看看从JS如何调用Native,换句话说Native如何开放API给JS

我们以弹Alert框的接口为例,这是Native的OC代码,导出RCTAlertManager类的alertWithArgs:(NSDictionary *)args

callback:(RCTResponseSenderBlock)callback)方法

@interface RCTAlertManager() : NSObject <RCTBridgeModule, RCTInvalidating>

@end

@implementation RCTAlertManager

RCT_EXPORT_MODULE()

RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args

callback:(RCTResponseSenderBlock)callback)

{

#end

要把OC类或实例的函数导出给JS用,需实现以下三个步骤
  • OC类实现RCTBridgeModule协议

  • 在.m的类实现中加入RCT_EXPORT_MODULE(),帮助你实现RCTBridgeModule协议

  • 要导出的函数用RCT_EXPORT_METHOD()宏括起来,不用这个宏,不会导出任何函数

现在从JS里可以这样调用这个方法:

var RCTAlertManager=require(‘react-native’).NativeModules.AlertManager; RCTAlertManager.alertWithArgs({message:‘JS->Native Call’,buttons:[{k1:‘button1’},{k2:‘button1’}]},function(id,v) {console.log(‘RCTAlertManager.alertWithArgs() id:’ + id +’ v:’ + v)});

执行之后的效果,弹出一个Alert

alert.png

对于详细的如何导出函数推荐阅读Native Modules

我们今天的目的不是和女神喝茶聊天,是深入女神内心,是内心咳咳。来看看今天的重点

动态导出Native API,延迟加载Native 模块

在JS中可以直接使用RCTAlertManager.alertWithArgs来调用,说明JS中已经定义了和OC对象相对应的JS对象,我们从导出一个Native类开始,完整跟踪下这个过程。

生成Native API 配置表

RCTAlertManager类实现了RCTBridgeModule协议,并且在类的实现里包含了RCT_EXPORT_MODULE() 宏

@protocol RCTBridgeModule

#define RCT_EXPORT_MODULE(js_name) \

RCT_EXTERN void RCTRegisterModule(Class); \

  • (NSString *)moduleName { return @#js_name; } \

  • (void)load { RCTRegisterModule(self); }

// Implemented by RCT_EXPORT_MODULE

  • (NSString *)moduleName;

@optional

在OC里,一个类所在文件被引用时,系统会调用其+(void)load函数,当RCTAlertManager所在文件被引用时,系统调用load 函数,函数里简单的调用RCTRegisterModule(self) 把自己注册到一个全局数组RCTModuleClasses,这样系统中导出的类都会自动注册到这个全局变量数组里(so easy)。

在JS中有一个BatchedBridge用来和Native通讯,在Natvie中也有一个RCTBatchedBridge类,它封装了JSContext即JS引擎

在RCTBatchedBridge start 函数中,做了5件事

  1. jsbundle文件的下载或本地读取(异步)
  1. 初始化导出给JS用的Native模块
  1. 初始化JS引擎
  1. 生成配置表,并注入到JS引擎中,
  1. 执行jsbundle文件。

//伪代码

  • (void)start

{

//1 jsbundle文件的下载或本地读取(异步)

NSData *sourceCode;

[self loadSource:^(NSError *error, NSData *source) {sourceCode = source}];

//2 初始化导出给JS用的Native模块

[self initModules];

//3 初始化JS引擎

[self setUpExecutor];

//4 生成Native模块配置表 把配置表注入到JS引擎中

NSSting* config = [self moduleConfig];

[self injectJSONConfiguration:config onComplete:^(NSError *error) {});

//5 最后执行jsbundle

[self executeSourceCode:sourceCode];

}

现在我们最关心第二步初始化Native模块 initModules 和moduleConfig 到底是什么

//伪代码

  • (void)initModules

{

//遍历上节讲到的RCTGetModuleClasses全局数组,用导出模块的类或者实例创建RCTModuleData

for (Class moduleClass in RCTGetModuleClasses())

{

NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);

//这里一个很有意思的地方,如果导出的类或其任何父类重写了init方法,或者类中有setBridge方法

//则React Native假设开发者期望这个导出模块在Bridge第一次初始化时实例化,否则怎么样,大家想想

if ([moduleClass instanceMethodForSelector:@selector(init)] != objectInitMethod ||

[moduleClass instancesRespondToSelector:setBridgeSelector]) {

module = [moduleClass new];

}

// 创建RCTModuleData

RCTModuleData *moduleData;

if (module) {

moduleData = [[RCTModuleData alloc] initWithModuleInstance:module];

} else {

moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass bridge:self];

}

//保存到数组中,数组index就是这个模块的索引

[_moduleDataByID addObject:moduleData];

}

}

initModules里根据是否重写init或添加了setBridge来决定是不是要马上实例化RCTGetModuleClasses里的导出类,然后用实例或类创建RCTModuleData,缓存到本地,以便JS调用时查询。

再来看第四步导出的 NSSting* config = [self moduleConfig] 是什么内容

{“remoteModuleConfig”:

[[“RCTStatusBarManager”],

[“RCTSourceCode”],

[“RCTAlertManager”],

[“RCTExceptionsManager”],

[“RCTDevMenu”],

[“RCTKeyboardObserver”],

[“RCTAsyncLocalStorage”],

.

.

.

]}

它仅仅是一个类名数组。

注入配置表到JS引擎,并创建对应的JS对象

生产配置表后,通过下面的方法把这个类名数组注入到JSContext,赋值给JS全局变量__fbBatchedBridgeConfig

[_javaScriptExecutor injectJSONText:configJSON

asGlobalObjectNamed:@“__fbBatchedBridgeConfig”

callback:onComplete];

在JS端,当有人引用了BatchedBridge var BatchedBridge=require('BatchedBridge');,其工厂函数会通过 __fbBatchedBridgeConfig配置表创建MessageQueue的实例BatchedBridge

var MessageQueue=require(‘MessageQueue’);

var BatchedBridge=new MessageQueue(

__fbBatchedBridgeConfig.remoteModuleConfig,

__fbBatchedBridgeConfig.localModulesConfig);

我们看看MessageQueue的构造函数,构造函数里为每个导出类创建了一个对应的module对象,因为此时config里只有一个导出类的名字,所以这里只为这个对象增加了一个成员变量 module.moduleID,并把module保存到this.RemoteModules数组里

_genModule(config, moduleID) {

let module = {};

if (!constants && !methods && !asyncMethods) {

module.moduleID = moduleID;

}

this.RemoteModules[moduleName] = module;

}

接着我们顺藤摸瓜看看那里使用的BatchedBridge.RemoteModules

NativeModules模块

NativeModules在初始化时,用BatchedBridge.RemoteModules保存的类名列表,为每个JS对象增加了函数等属性

__d(‘NativeModules’,function(global, require, module, exports) { ‘use strict’; var RemoteModules=require(‘BatchedBridge’).RemoteModules; var NativeModules={}; //遍历NativeModules中导出类名 Object.keys(RemoteModules).forEach(function(moduleName){ //把类名定义为NativeModules的一个属性,比如AlertManager类,定义只有就可以用NativeModules.AlertManager 访问 Object.defineProperty(NativeModules,moduleName,{ //这个属性(AlertManager)是可以遍历的,当然属性也是个对象里面有属性和函数 enumerable:true, //属性都有get和set函数,当调用访问这个属性时,会调用get函数 NativeModules.AlertManager get:function(){ var module=RemoteModules[moduleName]; if(module&&typeof module.moduleID===‘number’&&global.nativeRequireModuleConfig){ //调用Native提供的全局函数nativeRequireModuleConfig查询AlertManager 导出的常量和函数 var json=global.nativeRequireModuleConfig(moduleName); module=config&&BatchedBridge.processModuleConfig(JSON.parse(json),module.moduleID); RemoteModules[moduleName]=module; } return module; } }); }); module.exports=NativeModules; });

React Native 把所有的Native导出类定义在一个NativeModules模块里,所以使用Natvie接口时也可以直接这样拿到对应的JS对象

var RCTAlertManager=require('NativeModules').AlertManager;

代码里我加了注释

思考一个问题,为什么React Natvie搞的那么麻烦,为什么不在上一个步骤里(MessageQueue的构造函数)里就创建出完整的JS对象。

没错,就是模块的懒加载,虽然Native导出了Alert接口,在JS引擎初始化后,JS里只存在一个名字为AlertManager的空对象

906C7A90-0A85-4FD6-B433-39CE041D4445.png

当调用了RCTAlertManager.alertWithArgs({message:‘JS->Native Call’,buttons:[{k1:‘button1’}时,才会调用AlertManager 的get函数到Native里查询导出的常量和函数,并定义到AlertManager中。

7RGT1@Z}N19_9{KQ~P_SDFE.jpg

Native模块对应的JS对象中函数是如何调用到Native

RCTAlertManager.alertWithArgs 这个函数是如何调用到Native里的呢,在BatchedBridge.processModuleConfig函数中,用_genMethod创建了一个闭包fn为每个函数赋值,这个函数转调self.__nativeCall(module, method, args, onFail, onSucc); 我们调用RCTAlertManager.alertWithArgs函数,其实都是调用的这个fn闭包。

_genMethod(module, method, type) {

fn = function(…args) {

return self.__nativeCall(module, method, args, onFail, onSucc);

};

return fn;

}

__nativeCall,好熟悉的名字,

__nativeCall(module, method, params, onFail, onSucc) {

this._queue[MODULE_IDS].push(module);

this._queue[METHOD_IDS].push(method);

this._queue[PARAMS].push(params);

global.nativeFlushQueueImmediate(this._queue);

this._queue = [[],[],[]];

this._lastFlush = now;

}

global.nativeFlushQueueImmediate 是Native提供的接口,__nativeCall把需要调用的module,method,params都塞到队列里,然后传递到Native,

我们在回到Native 找到上文提到的两个关键接口,Native模块查询接口:global.nativeRequireModuleConfig和调用接口global.nativeFlushQueueImmediate,他们是在JS引擎(JSContext)初始化时,定义到全局变量的。

//RCTContextExecutor setUP

//简化过的代码

  • (void)setUp

{

self->_context.context[@“nativeRequireModuleConfig”] = ^NSString *(NSString *moduleName) {

NSArray *config = [weakBridge configForModuleName:moduleName];

return RCTJSONStringify(config, NULL);

};

self->_context.context[@“nativeFlushQueueImmediate”] = ^(NSArray<NSArray *> *calls){

[weakBridge handleBuffer:calls batchEnded:NO];

};

}

[weakBridge handleBuffer:calls batchEnded:NO]; 经过一系列传递,调用到_handleRequestNumber 中,用moduleID找到RCTModuleData,再用methodID 找到id<RCTBridgeMethod> method 然后在moduleData.instance实例中执行

  • (BOOL)_handleRequestNumber:(NSUInteger)i

moduleID:(NSUInteger)moduleID

methodID:(NSUInteger)methodID

params:(NSArray *)params

{

RCTModuleData *moduleData = _moduleDataByID[moduleID];

id method = moduleData.methods[methodID];

[method invokeWithBridge:self module:moduleData.instance arguments:params];

}

这里有必要再强调一次moduleData.instance 这个地方。

  • (id)instance

{

if (!_instance) {

_instance = [_moduleClass new];

}

return _instance;

}

还记的前面BatchedBridge 初始化时的initModules吗

//这里一个很有意思的地方,如果导出的类或其任何父类重写了init方法,或者类中有setBridge方法

//则React Native假设开发者期望这个导出模块在Bridge第一次初始化时实例化,否则怎么样,大家想想

if ([moduleClass instanceMethodForSelector:@selector(init)] != objectInitMethod ||

[moduleClass instancesRespondToSelector:setBridgeSelector]) {

module = [moduleClass new];

}

否则就是在用户真正调用时,在moduleData.instance里实例化,React Native已经懒到骨髓了。

RCTModuleData中每个函数的封装 RCTModuleMethod里还有一个优化点,JS传递到Native的参数需要进行响应的转换,RCTModuleMethod在调用函数只前,先预解析一下,创建每个参数转换的block,缓存起来,这样调用时,就直接使用对应函数指针进行参数转换了,大要详细了解可以看 - (void)processMethodSignature函数。

回调函数

前面我们为了直观,忽略了回调函数,alertWithArgs的第二个参数是一个JS回调函数,用来指示用户点击了哪个button,并打印出一行日志。

RCTAlertManager.alertWithArgs({message:‘JS->Native Call’,buttons:[{k1:‘button1’},{k2:‘button1’}]},function(id,v)

{console.log(‘RCTAlertManager.alertWithArgs() id:’ + id +’ v:’ + v)});

回调函数的调用和直接从Native调用JS是差不多的,再回头看看__nativeCall 函数我们忽略的部分

__nativeCall(module, method, params, onFail, onSucc) {

//Native接口最多支持两个回调

if (onFail || onSucc) {

onFail && params.push(this._callbackID);

this._callbacks[this._callbackID++] = onFail;

onSucc && params.push(this._callbackID);

this._callbacks[this._callbackID++] = onSucc;

}

this._queue[MODULE_IDS].push(module);

this._queue[METHOD_IDS].push(method);

this._queue[PARAMS].push(params);

global.nativeFlushQueueImmediate(this._queue);

this._queue = [[],[],[]];

this._lastFlush = now;

}

可以看到把onFail,onSucc两个函数类型转化为两个数字ID插入到参数列表后面,并把函数函数缓存起来。

从Native调用过来也比较简单了,传递过callbackID到JS,就可以执行到回调函数。

JS传递的参数仅仅是个整形ID,Native如何知道这个ID就是个回调函数呢?

答案是和其他参数一样通过Native的函数签名,如果发现对应的参数是个block,则从JS传递过来的ID就是对应的回调ID,把其转化为RCTResponseSenderBlock的闭包。

RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args callback:(RCTResponseSenderBlock)callback)

到此为止,我们已经把整个JS->Natvie的流程都走通了,

梳理一下整个流程。

调用图.png

总结一下

  1. Native初始化时, Native生成要导出模块的名字列表(注意注意注意),仅仅是模块(类)名字列表, ModuleConfig
  1. 在React Native 的JS引擎初始化完成后,向JSContext注入ModuleConfig,赋值到JS全局变量 __fbBatchedBridgeConfig
  1. 还记得那个N->JS大使—JS对象BatchedBridge吗,BatchedBridge创建的时候会用__fbBatchedBridgeConfig变量里Native模块名字列表定义一个同名的JS对象,但是是一个没有任何方法的空对象,只增加了一个获取方法数组的get函数。此时初始化的操作已完成。
  1. 很久很久之后,有人用RCTAlertManager.alertWithArgs 调用了Native的代码,咳咳,这人是我,此时JS去获取RCTAlertManager方法列表时,发现是空的,就调用Native提供的查询函数nativeRequireModuleConfig 获取RCTAlertManager对象的详细的导出信息(方法列表),并定义成同名的JS函数,此函数转调到OC的实现
  1. 此时RCTAlertManager对应的JS对象才定义完整,JS找到了alertWithArgs函数,每个对应的JS函数都是一个封装了调用__nativeCall的闭包,JS通过此函数转发到Native

可以看出,Native导出的配置表并不是在一开始就完整的注入JS并定义对应的JS对象,而是仅仅注入了一个模块名字,当运行期间有人调用的时候,才再从Native查询调用模块的详细配置表,这种懒加载机制缓解了一个大型的app导出的Api很多,全部导入JS导致初始化时内存占用过大的问题。

消息循环


线程问题

React Native为JS引擎创建了一个独立的线程

//RCTJavaScriptContext

  • (instancetype)init

{

NSThread *javaScriptThread = [[NSThread alloc] initWithTarget:[self class]

selector:@selector(runRunLoopThread)

object:nil];

javaScriptThread.name = @“com.facebook.React.JavaScript”;

[javaScriptThread start];

return [self initWithJavaScriptThread:javaScriptThread context:nil];

}

所有的JS代码都运行在"com.facebook.React.JavaScript"后台线程中,所有的操作都是异步,不会卡死主线程UI。并且JS调用到Native中的接口中有强制的线程检查,如果不是在React线程中则抛出异常。

这样有一个问题,从JS调用Native中的代码是执行在这个后台线程中,我们上文的RCTAlertManager.alertWithArgs明显是个操作UI的接口,执行在后台线程会crash,在导出RCTAlertManager时,通过实现方法- (dispatch_queue_t)methodQueue,原生模块可以指定自己想在哪个队列中被执行

  • (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); }

类似的,如果一个操作需要花费很长时间,原生模块不应该阻塞住,而是应当声明一个用于执行操作的独立队列。举个例子,RCTAsyncLocalStorage模块创建了自己的一个queue,这样它在做一些较慢的磁盘操作的时候就不会阻塞住React本身的消息队列:

  • (dispatch_queue_t)methodQueue { return dispatch_queue_create(“com.facebook.React.AsyncLocalStorageQueue”, DISPATCH_QUEUE_SERIAL); }

React的消息循环

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

后记


总结一下这三次面试下来我的经验是:

  1. 一定不要死记硬背,要理解原理,否则面试官一深入就会露馅!

  2. 代码能力一定要注重,尤其是很多原理性的代码(之前两次让我写过Node中间件,Promise.all,双向绑定原理,被虐的怀疑人生)!

  3. 尽量从面试官的问题中表现自己知识的深度与广度,让面试官发现你的闪光点!

  4. 多刷面经!

我把所有遇到的面试题都做了一个整理,并且阅读了很多大牛的博客之后写了解析,免费分享给大家,算是一个感恩回馈吧,有需要的朋友【点击我】获取。祝大家早日拿到自己心怡的工作!

篇幅有限,仅展示部分内容



发知识点,真正体系化!**

[外链图片转存中…(img-yMm3D8SZ-1712951602272)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

后记


总结一下这三次面试下来我的经验是:

  1. 一定不要死记硬背,要理解原理,否则面试官一深入就会露馅!

  2. 代码能力一定要注重,尤其是很多原理性的代码(之前两次让我写过Node中间件,Promise.all,双向绑定原理,被虐的怀疑人生)!

  3. 尽量从面试官的问题中表现自己知识的深度与广度,让面试官发现你的闪光点!

  4. 多刷面经!

我把所有遇到的面试题都做了一个整理,并且阅读了很多大牛的博客之后写了解析,免费分享给大家,算是一个感恩回馈吧,有需要的朋友【点击我】获取。祝大家早日拿到自己心怡的工作!

篇幅有限,仅展示部分内容



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值