RN 与native 的交互
0>>> React 的渲染機制
1>>> react-native 渲染原理
2>>> react-native 如何与原生通信
3>>> 如何封装一个原生视图组件
4>>> react-native 的线程管理
RN的本质是利用 js 调用 native 端的组件, 从而实现相应的功能
// react
React 与 React native 的原理是相同的,都是由 javascript 实现的虚拟DOM 来驱动界面 View 的渲染,只不过 React.js 驱动 HTML DOM 的渲染, RN 是驱动 ios/android 原生组件的渲染
都知道 React native 是虚拟 DOM 机制,是存在于内存中的 JavaScript 对象对应着真实的 DOM 节点,操作DOM 很慢,但操作JavaScript 很快,由于采用diff算法只渲染相比之前改变的部分并不是全部渲染,所以更快, setState 时相当于创建了一个影子 DOM 然后比较新旧两个 DOM 的区别触发 render 函数,重新渲染需要改变部分的DOM节点
React 是 fb 推出的一个 js前端框架, 采用组件化方式简化了 web 开发
(1)DOM : 每个 HTML 页面可以看成一个 DOM 可以把 HTML标签和 js 单独封装到一个组件类中,便于复用,方便开发
(2)更高效 原生的 web 刷新 DOM, 需要刷新整个页面 react 只刷新部分页面
(3) 独创了 Virtual DOM 机制, 是一个存在于内存中的 js对象,之所以非常快是因为他从不直接操作 DOM, 它和 DOM 是一一对应的关系, 当页面发生变化时, react 会利用 DOM diff 算法, 把有变化的 DOM 进行刷新
由当前状态决定 UI 界面 状态发生变化时 由 diff算法决定刷新哪些 DOM 节点, 虚拟DOM是在DOM的基础上建立了一个抽象层,对数据和状态所做的任何改动,都会被自动且高效的同步到虚拟DOM,最后再批量同步到DOM中。
在React中,render执行的结果得到的并不是真正的DOM节点,而仅仅是JavaScript对象,称之为虚拟DOM
逐层进行节点比较 如果上面的同级节点不同,该节点及其子节点会被完全删除掉,不会进行进一步的递归比较,这样只需要对 DOM 树进行一次遍历 ,便能完成整个DOM 树的比较
DOM 结构的转换
比如这种正常的逻辑: A.parent.remove(A) D.append(A)
对于 React 只考虑同层的节点的位置变换 不同层的只有简单的创建和删除, 当发现节点中 A 不见了 ,直接销毁 A 新建一个 A 节点作为 D 的子节点
A.destroy() A = new A() A.append(new B()) A.append(new C()) D.append(A)
同级的列表节点的比较
如图: 在 JQuery 中 $(B).after(F)直接插入一个节点就好了
在 React 只能告诉新的页面是 ABFCDE组成.由 diff算法完成更新 他只能把 A 更新为 A B 更新为 B C 更新为 F 这样
如果给每个节点一个唯一标识的 key, React 就可以找到正确的位置去插入新的节点,这也是为什么项目中有些列表经常报警告缺少 key, 也是潜在的性能问题, 虽然不影响运行
对于相同类型的元素, React 会先查看两者的属性差异, 然后保留相同的底层 DOM 节点,仅仅去更新那些被修改的属性比如只修改了 width color 等 处理完 根DOM节点后, 会根据上面的判断逻辑对子节点进行递归扫描
需要注意的两点:
1> 该算法不会去尝试匹配那些不同组件类型的子元素树。 如果你看到自己在返回相似输出结果的两个组件类型之间来来回回,你可能需要把它们改为相同的类型组件。
2> key属性应该是稳定,可预测和唯一的。 不稳定的键(如使用Math.random()生的key)将导致许多组件实例和DOM节点进行不必要地重复创建,这可能导致子组件中的性能降低和state丢失。
React.render 渲染流程
1>>> React.render() 将我们的 element 根虚拟节点渲染到 container 元素中
2>>> 根据 element 的类型不同, 分别实例化 ReactDOMTextComponnet ReactDOMComponent ReactCompositeComponent类, 这些类用来管理 ReactElement ,负责将不同的 ReactElement 转化为 DOM, 并更新 DOM
3>>> ReactCompositeComponent 实例调用 mountComponent 方法后内部调用render 方法, 返回了 DOM Elements
React 的更新机制
React Element 一个描述 DOM 节点或 component 实例的字面级对象, 它包含一些信息,组件的 type 和 props, 就像一个描述 DOM 节点的元素(虚拟节点), 可以通过 React.creatElement 方法创建
reactClass 平时我们所创建的 component 组件(类或函数) ReactClass 实例化后调用 render 函数可返回 DOMElement
调用 React.render 方法, 将 element根虚拟节点渲染到 container 元素中, 根据 element 的类型不同,分别实例化 ReactDOMTextComponent ReactDOMComponent ReactCompositeComponent 类, 这些类用来管理 ReactElement 负责将不同的 ReactElement 转化为 DOM 并更新 DOM , ReactCompositeComponent 实例调用 mountComponent 方法后内部调用 render 方法, 返回 DOMElement
总结: (1)节点之间的比较 节点 node .包括两种类型 一个是 React 组件 一个是 HTML 的 DOM
节点类型不同: 直接使用新的替换旧的
类型相同: 样式不同,修改样式即可,在 React 里样式并不是纯粹的字符串也是一个对象,修改完当前节点之后,递归处理子节点
组件类型相同: 使用 React 机制处理,一般使用新的 props 替换旧的 props, 并在之后调用组件的 componentWill /DidRecieveProps 方法, 之前的组件的 render 方法会被调用\
(2)列表之间的比较
一列节点中有一个发生变化, React 并没有什么好的办法处理,循环两个列表,找出不同是唯一的处理方法
但是,降低这个算法的难度的办法是在生成一个列表时给每一个节点一个唯一的 key,就可以方便的找出哪个节点发生变化
// React component的生命周期函数
装载组件
- componentWillMount:这个方法被调用时期是组件将要被加载在视图上之前,功能比较少,即:render一个组件前最后一次修改state的机会。
- componentDidMount:即调用了render方法后,组件加载成功并被成功渲染出来以后所执行的hook函数,一般会将网络请求等加载数据的操作,放在这个函数里进行,来保证不会出现UI上的错误.
更新组件状态
存在期主要是用来处理与用户的交互,如:点击事件,都比较简单,也不是很常用,具体有以下几个过程:
- componentWillReceiveProps:指父元素对组件的props或state进行了修改。
- shouldComponentUpdate:一般用于优化,可以返回false或true来控制是否进行渲染
- componentWillUpdate:组件刷新前调用,类似componentWillMount
- componentDidUpdate:更新后的hook
卸载(删除)组件 销毁期,用于清理一些无用的内容,如:点击事件Listener 定时器的移除
- componentWillUnmount
上面函数的调用顺序是:
创建时
- getDefaultProps // 在组件内 可以 static defultProps={ name:'chris'} 组件外 MyComponent.defaultProps={name:'chris'}
- getInitialState // ES6 constructor(){this.state={}}
- componentWillMount
- render
更新时
- componentDidMount
- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
//react-native 是如何做到js与 oc 交互的 JavascriptCore
React native 会在一开始生成两个模块配置表, js按照整个表就可以找到需要调用的方法
iOS 是通过JavaScriptCore 与 oc交互
react naive 启动流程
React -native 能够在 iOS 和安卓设备上运行起来, 是因为 rn 与 native 之间有一种交互. javascriptCore 引擎, js告诉 oc需要执行什么, iOS 会自己去调用 UIKit 等框架绘制界面
1 创建 RCTRootView
2 创建 RCTBridge 桥接对象 管理 js与 oc的交互
3 创建 RCTBatchedBridge 批量桥接对象
4 执行 [RCTBatchedBridge loadSource] 加载 js 源码
5 执行 [RCTBatchedBridge initModulesWithDispatchGroup] 创建模块配置表
6 执行 [RCTJSExcutor injectJSONText] 往 js 中插入 OC 模块表
7 执行完 js代码 回调 OC 调用 OC 中的组件
8 完成渲染
RCTBridge 是负责双方通信的桥梁, 真正起作用的是RCTBatchedBridge类, 可以看一下他都做了什么?
当创建完模块的实例对象之后,会将该实例保存到一个RCTModuleData对象中,RCTModuleData里包含模块的类名,名称,方法列表,实例对象、该模块代码执行的队列以及配置信息等,js层就是根据这个对象来查询该模块的相关信息。
// react-native 加载 js源码流程
// react-native UI控件渲染流程
用户能看到的一切内容都源于 RootView, 实际上在创建 RootView 之前, RN 先创建了一个 Bridge 对象. 他是 OC与 JS 交互的桥梁, 整个 RN 的初始化过程其实也就是在创建整个桥梁对象
初始化方法的核心是 setUp 方法, setUp 方法主要任务是创建BatchedBridge, BatchedBridge的作用是批量读取 js对 OC 的方法调用,创建BatchedBridge的关键是 start 方法, 分为5个步骤:
1>> 读取 js源码
2>> 初始化模块信息
3>> 初始化 JS 代码执行器 RCTJSCExecutor 对象
4>> 生产模块列表并写入 JS 端
5>> 执行 js 源码 调用 OC
在调用 OC 之前, JS 会解析出方法的 MoudleID MethodID Argument 并放入 MessageQueue. 等待 OC 拿走,或者超时后主动发送给 OC
OC负责调用的方法是 handleBuffer, 他的参数是一个含有四个元素的数组, 每个元素也是一个数组,分别存放 moudleID methodID params , 函数内部在每一次调用方法的时候调用_ handleRequestNumber: MoudleID: MethodID: params: 方法, 通过查找模块配置表找出要调用的方法, 并通过 runtime 动态调用 [method invokeWithBridge:self moudle:moudleDate.instance arguments:params] processMethodSignature ,他会根据 js 的 callbackID 创建一个 block, 并在调用完函数之后执行这个 block
1. [RCTRootView runApplication] 通知 js 运行 APP
2. [RCTBatchedBridge _processResponse:json error:error] 处理执行完js 代码返回的响应 , 包含需要添加多少个子控件的信息
3 . [RCTBatchedBridge batchDidComplete] 批量桥接对象调用批量处理完成方法
4 . [RCTUIManager batchDidComplete] RCTUIManager 调用批量处理完成的方法,就会开始去加载 rootView 的子控件
5 . [RCTUIManager creatView: viewName :rootTags: props:] 通过 js 执行 OC 代码, 让 UIManager 创建子控件 View
6 .[RCTUIManager _layoutAndMount] 布局子组件
7 .[RCTUIManager setChildren: reactTags: ] 给 RCTRootView 对应的 RCTRootShadowView 设置子组件
8 .[RCTRootShadowView insertReactSubView:view atIndex:index++] 遍历子组件数组, 给 RCTRootShadowView 插入所有子控件
9.[RCTShadowView processUpdatedProperties:parentProperties] 处理保存在 RCTShadowView中的属性, 就会去布局 RCTShadowView 对应 UIView的所有子控件
10.[RCTView didUpdateReactSubviews] 给原生 View 添加子控件 完成 UI 渲染
render 函数的翻译
ReactElement.createElement = function (type, config, children){ ... }
UIManager 通过调用 createView 方法创建原生的 UIView
UIManager 通过 dispatchViewManagerCommand 来实现把原生UI 的方法给 JS 响应
UIManager 是一个 native moudle
- createView(int tag, String className, int rootViewTag, ReadableMap props)
- 创建View
- updateView(int tag, String className, ReadableMap props)
- 更新View
- manageChildren(int viewTag, Array moveFrom, Array moveTo, Array addChildTags, Array addAtIndices, Array removeFrom)
- 批量添加/删除/移动一个view下面的view
- measure(int reactTag, Callback callback)
- 测量View的位置、size等,结果异步回调
- measureInWindow(int reactTag, Callback callback)
- 测量View相对屏幕的位置、size等,结果异步回调
- dispatchViewManagerCommand(int reactTag, int commandId, ReadableArray commandArgs)
- 派发View命令,也就是用来调用对应View的方法
这个模块是NativeModule方式定义的,在RN的JS端启动时,端上会通过JSC把收集到的模块信息(名称)打包到JS端全局变量global.__fbBatchedBridgeConfig中,并采用延迟加载策略:设置NativeModules.[模块名]的getter,延迟通过JSC读取模块详细信息(方法、命令号等信息)。在调用的时候会放到MessageQueue的队列里,批量提交,两次批量提交限制的最小间隔为5ms。
RN和原生一样,也是先渲染内部子控件,然后再渲染外部控件。所以Component来自React的,但是UI控件是React-Native的,在render生命周期执行的时候会执行子控件的render方法,子控件会调用UIManager来把信息传递到原始的UIManagerModule,UIManagerModule根据传过来的Tag找到对应的UIManager,最后生成一个Operation添加到UI处理队列中,当mDispatchUIRunnables执行runable的时候调用Operation.execute抽象方法,其实就是调用UIManager.createViewInstance来真正生成View,然后调用viewManager.updateProperties 设置View的属性。这样一个控件就创建出来了
// react- native 的事件处理流程
-
1.在创建RCTRootContentView的时候,内部会创建RCTTouchHandler
RCTTouchHandler:继承UIGestureRecognizer,也就是它就是一个手势
它会作为RCTRootContentView的手势,这样点击RCTRootContentView,就会触发RCTTouchHandler
RCTTouchHandler:内部实现了touchBegin等触摸方法,用来处理触摸事件
-
2.在创建RCTTouchHandler的时候,内部会创建RCTEventDispatcher
RCTEventDispatcher:用来把事件处理传递给JS的方法处理,也就是当UI界面产生事件,就会执行JS的代码处理。
-
3.通过RCTRootContentView截获点击事件
产生事件就会去触犯RCTRootContentView中的RCTTouchHandler对象。
-
4.当产生事件的时候,会执行[RCTTouchHandler touchBegin]
-
5.RCTTouchHandler的touch方法,会执行[RCTTouchHandler _updateAndDispatchTouches:eventName:]
内部会创建RCTTouchEvent事件对象
-
6.[RCTEventDispatcher sendEvent:event] -> 让事件分发对象调用发送事件对象
内部会把事件保存到_eventQueue(事件队列中)
-
7.[RCTEventDispatcher flushEventsQueue] -> 让事件分发对象冲刷事件队列,就是获取事件队列中所有事件执行
-
8.[RCTEventDispatcher dispatchEvent:event] -> 遍历事件队列,一个一个分发事件
分发事件的本质:就是去执行JS的代码,相应事件。
-
9.[RCTBatchedBridge enqueueJSCall:[[event class] moduleDotMethod] args:[event arguments]]; -> 让桥架对象调用JS处理事件
本质:就是产生事件调用JS代码
-
10.这样就能完成把UI事件交给JS代码响应
RN与原生(iOS)之间的通信
OC端与 JS 端分别有一个Bridge, 两个 Bridge 都保存了同样一份模块配置表, OC 要告诉 JS 自己有什么模块 , 模块里有什么方法, JS 知道这些方法才可以调用, JS 调用 OC 模块方法的时候, 通过 Bridge 里的配置表把模块ID 方法 ID 和参数传给 OC, OC 通过配置表找到对应的方法执行, 如图
详细流程:
1.JS端调用某个OC模块暴露出来的方法。
2.把上一步的调用分解为ModuleName,MethodName,arguments,再扔给MessageQueue处理。
在初始化时模块配置表上的每一个模块都生成了对应的remoteModule对象,对象里也生成了跟模块配置表里一一对应的方法,这些方法里可以拿到自身的模块名,方法名,并对callback进行一些处理,再移交给MessageQueue。具体实现在BatchedBridgeFactory.js的_createBridgedModule里,整个实现区区24行代码,感受下JS的魔力吧。
3.在这一步把JS的callback函数缓存在MessageQueue的一个成员变量里,用CallbackID代表callback。在通过保存在MessageQueue的模块配置表把上一步传进来的ModuleName和MethodName转为ModuleID和MethodID。
4.把上述步骤得到的ModuleID,MethodId,CallbackID和其他参数argus传给OC。至于具体是怎么传的,后面再说。
5.OC接收到消息,通过模块配置表拿到对应的模块和方法。
实际上模块配置表已经经过处理了,跟JS一样,在初始化时OC也对模块配置表上的每一个模块生成了对应的实例并缓存起来,模块上的每一个方法也都生成了对应的RCTModuleMethod对象,这里通过ModuleID和MethodID取到对应的Module实例和RCTModuleMethod实例进行调用。具体实现在_handleRequestNumber:moduleID:methodID:params:。
6.RCTModuleMethod对JS传过来的每一个参数进行处理。
RCTModuleMethod可以拿到OC要调用的目标方法的每个参数类型,处理JS类型到目标类型的转换,所有JS传过来的数字都是NSNumber,这里会转成对应的int/long/double等类型,更重要的是会为block类型参数的生成一个block。
例如-(void)select:(int)index response:(RCTResponseSenderBlock)callback 这个方法,拿到两个参数的类型为int,block,JS传过来的两个参数类型是NSNumber,NSString(CallbackID),这时会把NSNumber转为int,NSString(CallbackID)转为一个block,block的内容是把回调的值和CallbackID传回给JS。
这些参数组装完毕后,通过NSInvocation动态调用相应的OC模块方法。
7.OC模块方法调用完,执行block回调。
8.调用到第6步说明的RCTModuleMethod生成的block。
9.block里带着CallbackID和block传过来的参数去调JS里MessageQueue的方法invokeCallbackAndReturnFlushedQueue。
10.MessageQueue通过CallbackID找到相应的JS callback方法。
11.调用callback方法,并把OC带过来的参数一起传过去,完成回调。
整个流程就是这样,简单概括下,差不多就是:JS函数调用转ModuleID/MethodID -> callback转CallbackID -> OC根据ID拿到方法 -> 处理参数 -> 调用OC方法 -> 回调CallbackID -> JS通过CallbackID拿到callback执行
说完原理,现在就是具体js怎么调用 OC , OC 怎么调用 JS
一 .JS调用 OC
1> 新建两个OC 文件遵守 RCTBridgeMoudle协议,这里以集成 native 的微博分享给 RN 调用为例
2> 通过 RCT_EXPORT_MOUDLE()暴露当前模块 RCT_EXPORT_METHOD()暴露 native 方法
3> 在 JS端通过 NativeModules获取当前 native 模块,调用模块的方法,传递参数,同时支持回调,在native方法里添加 block 回调或者 promise 回调,即可支持OC 回调传递结果给 JS
4> 导出常量原生模块可以导出一些常量,在JS 端可以随时访问,用这种方法来传递一些静态数据,可以避免通过 Bridge 进行一次来回交互
- (NSDictionary *)constantsToExport
{
return @{ @"firstDayOfTheWeek": @"Monday" };
}
Js端访问 console.log(CalendarManager.firstDayOfTheWeek);
RCT_REMAP_METHOD(testPromiseEvent,
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSArray *events =@[@"Promise ",@"test ",@" array"];
if (events) {
resolve(events);
} else {
NSError *error=[NSError errorWithDomain:@"我是Promise回调错误信息..." code:101 userInfo:nil];
reject(@"no_events", @"There were no events", error);
}
}
// 当然也可以用这种 promise 的回调 在 js里可以使用 . then() 或者 async await 来获取 promise 结果
//tips: 关于 RCT_EXPORT_METHOD() 可以暴露方法给 JS 调用, 内部实现就是利用 runtime 遍历该实例的方法, 过滤含有__ rct _export__的然后保存到模块配置表中
二. OC 调用 JS(给 JS 发送事件)
即使没有被js调用 本地模块也可以主动给 JS 发送事件通知, 最直接的方式是使用 eventDispatcher
#import "RCTBridge.h"
#import "RCTEventDispatcher.h"
@implementation CalendarManager
@synthesize bridge = _bridge;
// 进行设置发送事件通知给JavaScript端
- (void)calendarEventReminderReceived:(NSNotification *)notification
{
NSString *name = [notification userInfo][@"name"];
[self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder"
body:@{@"name": name}];
}
@end
在 js中可以这样订阅事件import { NativeAppEventEmitter } from 'react-native';
var subscription = NativeAppEventEmitter.addListener(
'EventReminder',
(reminder) => console.log(reminder.name)
);
...
// 千万不要忘记忘记取消订阅, 通常在componentWillUnmount函数中实现。
subscription.remove();
三. RN 使用原生 UI 组件
1> 像原生自定义 UI 组件一样, 新建 class 自定义视图 , 这里以一个可以支持手势缩放的相册浏览视图为例
2> 新建 xxxViewManager 继承自 RCTViewManager
3> RCT_EXPORT_MODULE(RCTPhotoView)导出模块 RCT_EXPORT_VIEW_PROPERTY(imgURL, NSString); RCT_EXPORT_VIEW_PROPERTY(onSingleTap, RCTBubblingEventBlock);导出属性和方法供 js 调用
4> 重写 -(UIView *)View{ }的方法 返回自定义的UI 组件
5> 新建一个 JS 类 利用var RCTPhotoView = requireNativeComponent('RCTPhotoView', ImageBrowserView)
导出该自定义组件
6> 在需要的地方使用该组件(使用姿势与其他组件一样)
// ViewManager 的定义 .h
// 新建 JS 类导出该原生 UI 组件 使用的时候直接导入即可
// 使用 赋值 url属性 实现原生组件的点击方法的回调
// 最后说下 RN 的线程管理
RN 最主要的有兩個線程, UI MainThread 和 JSThread, UIThread 创建一个事件循环之后, 就一直有个 runloop 维持, 不断接收处理 App 事件, JSThread更像一个底层数据采集器, 不断上传数据和事件, ios 通过 JavascriptCore 提供的 js Bridge, UIThread将这些事件和数据转化为 UI 改变, UI main thread 跟 JS thread更像是CS 模型,JS thread更像服务端, UI main thread是客户端, UI main thread 不断询问JS thread并且请求数据,如果数据有变,则更新UI界面。
//RCTJavaScriptContext javascriptCore 引擎初始化
- (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 本身是单线程语言, native 是多线程机制, 那么 js如何来使用 native 的多线程, 或者说如何让js在其他线程执行
在 iOS 开发中,一谈到线程管理,肯定离不开 GCD(Grand Central Dispatch)与 NSOperation/NSOperationQueue 技术选型上的争论。关于这两者普遍的观点为:GCD 较轻量,使用起来较灵活,但在任务依赖、并发数控制、任务状态控制(线程取消/挂起/恢复/监控)等方面存在先天不足;NSOperation/NSOperationQueue 基于 GCD 做的封装,使用较重,在某些情景下性能不如 GCD,但在并发环境下复杂任务处理能很好地满足一些特性,业务扩展性较好。
global.nativeFlushQueueImmediate 是Native提供的接口,__nativeCall把需要调用的module,method,params都塞到队列里,然后传递到Native
__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;
}
Native模块查询接口:global.nativeRequireModuleConfig和调用接口global.nativeFlushQueueImmediate,他们是在JS引擎(JSContext)初始化时,定义到全局变量的。
先从 JS 端看起,JS 调用 Native 的逻辑在 MessageQueue.js 的 _nativeCall 方法中。在最小调用间隔(MIN_TIME_BETWEEN_FLUSHES_MS=5ms)内,JS 端会将调用信息存储在 _queue 数组中,通过 global. nativeFlushQueueImmediate 方法来调用 Native 端的功能。global.nativeFlushQueueImmediate 方法在 iOS 端映射的是一个全局的 Block,如图
nativeFlushQueueImmediate 在这里只是做了一个中转,功能的实现是通过调用 RCTBatchedBridge.m 中的 handleBuffer 方法。在 handleBuffer 中针对每个组件使用一个 queue 来处理对应任务。其中,这个 queue 是组件数据 RCTModuleData 中的属性 methodQueue,后文会详细介绍。
虽然 JS 只具备单线程操作的能力,但通过利用 Native 端多线程处理能力,仍可以很好地处理 RN 中的任务。回到刚开始抛出的问题,RN 在这里用 GCD 而非 NSOperationQueue 来处理线程,主要原因有:
-
- GCD 更加轻量,更方便与 Block 结合起来进行线程操作,性能上优于 NSOperationQueue 的执行;
- 虽然 GCD 在控制线程数上有缺陷,不如 NSOperationQueue 有直接的 API 可以控制最大并发数,但由于 JS 是单线程发起任务,在 5ms 内会积累的任务数创造的并发不高,不用考虑最大并发数带来的 CPU 性能问题。
- 关于线程依赖的处理,由于 JS 端是在同一个线程顺序执行任务的,而在 Native 端对这些任务进行了分类,针对同类别任务在同一个 FIFO 队列中执行。这样的应用场景及 Native 端对任务的分类处理,规避了线程依赖的复杂处理。
// 自定义UI 组件的线程管理
Native(iOS)端处理并发任务的线程是 RCTModuleData 中的属性 methodQueue。RCTModuleData 是对组件对象的实例(instance)、方法(methods)、所属线程(methodQueue)等方面的描述。每一个 module 都有个独立的线程来管理,具体线程的初始化在 RCTModuleData 的 setUpMethodQueue 中进行设置
这个方法开放了给组件自定义线程的接口。如果组件实现了 methodQueue 方法,则获取此方法中设置的 queue;否则默认创建一个子线程
-
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
} - (dispatch_queue_t)methodQueue
{
return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
}
- (dispatch_queue_t)methodQueue
- 如果不通过 methodQueue 方法设定具体的执行队列(dispatch_queue_t),则系统会自动创建一个默认线程,线程名称为 ModuleNameQueue;
- 对同类别组件进行划分,采用相同的执行队列(比如系统 UI 组件都是在 RCTUIManagerQueue 中执行)。这样有两点好处,一是为了控制组件执行队列的无序生长,二也可以控制特殊情况下的线程并发数。
是不是发现除了默认的 React-native 线程和主线程还有一个 RCTJSThread ,这是个什么东西, 看图:
说的很清楚了吧, 他不是一个线程,而是一个队列, 能够使 method 强制回到 js线程执行,
- javaScriptThread 是一个 NSThread 对象,看到这里才知道真正具备执行任务的是这里的 JavaScriptThread,而不是前面的 RCTJSThread。在 handBuffer 方法中之所以用 RCTJSThread,而不用 nil 替代,我的看法是为了可读性和扩展性。可读性是指如果在各个组件中将当前线程对象设置为 nil,使用者会比较迷惑;扩展性是指如果后面业务有扩展,发现根据 nil 比较不能满足需求,只需修改 RCTJSThread 初始化的地方,业务调用的地方完全没有感知。所以你完全可以
- (dispatch_queue_t)methodQueue
{
return RCTJSThread;
}这样写 回到 js线程
// RCTUIManagerQueue
RN 中的 UI 操作都是在 RCTUIManagerQueue 中进行的, 他是一个并发队列,但是优先级是最高的, 由于苹果在 iOS 8.0 之后引入了 NSQualityOfService,淡化了原有的线程优先级概念,所以 RN 在这里优先使用了 8.0 的新功能,而对 8.0 以下的沿用原有的方式。但不论用哪种方式,都保证 RCTUIManagerQueue 在并发队列中优先级是最高的。到这里或许有疑问了,UI 操作不是要在主线程里操作吗,这里为什么是在一个子线程中操作?其实在此执行的是 UI 的准备工作,当真正需要把 UI 元素加入主界面,开始图形绘制时,才需要在主线程里操作
在這裡回到主線程操作
End