您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
从 iOS 视角解密 React Native 中的线程
 
 来源:geek.csdn.net 发布于:2017-1-19
  2266  次浏览      16
 

【导语】React Native(后文简称 RN)自推出至今,已在国内不少公司得到了推广应用,前景颇为看好。而当前市面上对 RN 源代码级别的研究文章较少,对理解以及应用 RN 上带来诸多不便。线程管理是 RN 的一个基础内容,理清它对了解 RN 中的组件设计、事件交互、复杂任务处理有很大的帮助。由此,本文将基于 iOS 端的源代码介绍 RN 中线程管理的相关内容。

在 iOS 开发中,一谈到线程管理,肯定离不开 GCD(Grand Central Dispatch)与 NSOperation/NSOperationQueue 技术选型上的争论。关于这两者普遍的观点为:GCD 较轻量,使用起来较灵活,但在任务依赖、并发数控制、任务状态控制(线程取消/挂起/恢复/监控)等方面存在先天不足;NSOperation/NSOperationQueue 基于 GCD 做的封装,使用较重,在某些情景下性能不如 GCD,但在并发环境下复杂任务处理能很好地满足一些特性,业务扩展性较好。

但是 RN 在线程管理是如何选用 GCD 和 NSOperation 的?带着此问题,一起从组件中的线程、JSBundle 加载中的线程以及图片组件中的线程三个方面,逐步看看其中的线程管理细节。

组件中的线程

组件中的线程交互

RN 的本质是利用 JS 来调用 Native 端的组件,从而实现相应的功能。由于 RN 的 JS 端只具备单线程操作的能力,而 Native 端支持多线程操作,所以如何将 JS 的单线程与 Native 端的多线程结合起来,是 JS 与 Native 端交互的一个重要问题。图 1,直观展示了 RN 是如何处理的。

图 1 JS 调用 Native 端

先从 JS 端看起,如图 1 所示,JS 调用 Native 的逻辑在 MessageQueue.js 的 _nativeCall 方法中。在最小调用间隔(MIN_TIME_BETWEEN_FLUSHES_MS=5ms)内,JS 端会将调用信息存储在 _queue 数组中,通过 global. nativeFlushQueueImmediate 方法来调用 Native 端的功能。global.nativeFlushQueueImmediate 方法在 iOS 端映射的是一个全局的 Block,如图 2 所示。

图 2 global.nativeFlushQueueImmediate 方法映射

nativeFlushQueueImmediate 在这里只是做了一个中转,功能的实现是通过调用 RCTBatchedBridge.m 中的 handleBuffer 方法,具体代码如图 3 所示。在 handleBuffer 中针对每个组件使用一个 queue 来处理对应任务。其中,这个 queue 是组件数据 RCTModuleData 中的属性 methodQueue,后文会详细介绍。

图 3 handleBuffer 方法调用

从上面的代码追踪可以看出,虽然 JS 只具备单线程操作的能力,但通过利用 Native 端多线程处理能力,仍可以很好地处理 RN 中的任务。回到刚开始抛出的议题,RN 在这里用 GCD 而非 NSOperationQueue 来处理线程,笔者认为主要原因有:

GCD 更加轻量,更方便与 Block 结合起来进行线程操作,性能上优于 NSOperationQueue 的执行;

虽然 GCD 在控制线程数上有缺陷,不如 NSOperationQueue 有直接的 API 可以控制最大并发数,但由于 JS 是单线程发起任务,在 5ms 内会积累的任务数创造的并发不高,不用考虑最大并发数带来的 CPU 性能问题。

关于线程依赖的处理,由于 JS 端是在同一个线程顺序执行任务的,而在 Native 端对这些任务进行了分类(后文会有叙述),针对同类别任务在同一个 FIFO 队列中执行。这样的应用场景及 Native 端对任务的分类处理,规避了线程依赖的复杂处理。

组件中线程自定义

前文提到了 Native(iOS)端处理并发任务的线程是 RCTModuleData 中的属性 methodQueue。RCTModuleData 是对组件对象的实例(instance)、方法(methods)、所属线程(methodQueue)等方面的描述。每一个 module 都有个独立的线程来管理,具体线程的初始化在 RCTModuleData 的 setUpMethodQueue 中进行设置,详细代码可见图 4。

图 4 线程自定义

图 4 中的 174 行至 177 行是开放给组件自定义线程的接口。如果组件实现了 methodQueue 方法,则获取此方法中设置的 queue;否则默认创建一个子线程。问题来了,既然可以自定义线程,那 RN 中内置组件是如何定义的,对开发过程中的自定义组件在设置线程的时候需要注意什么?

图 5 是本地项目中实现 methodQueue 的组件,除去以 RCTWB 开头的自定义组件,其它都是系统自带的。通过查看每一个组件 methodQueue 方法的实现,发现有的是在主线程执行,有的是在 RCTJSThread 中执行,表 1 所示的是其中主要系统组件的具体情况。

表 1 RCTJSThread 中的主要系统组件一览果

图 5 本地项目中实现 methodQueue

RCTJSThread

RCTJSThread 是在 RCTBridge 中定义的一个私有属性,如图 6 所示。

图 6 RCTJSThread 定义

RCTJSThread 的类型是 dispatch_queue_t,它是 GCD 中管理任务的队列,与 block 联合起来使用。一个 block 封装一个特定的任务,dispatch_queue_t 一次执行一个 block,相互独立的 dispatch_queue_t 可以并发执行 block。

RCTJSThread 的初始化比较有意思,并没有采用 dispatch_queue_create 来创建一个 queue 实例,而是指向 KCFNull。我在整个源代码里全局搜了一下,没有其他的地方对 RCTJSThread 进行初始化。事实上,RCTJSThread 在设计上不是用来执行任务的,而是用来进行比较的,看图 7 中的代码。

图 7 RCTJSThread 设计

RCTBatchedBridge.m 中的 handleBuffer 是处理 JS 向 Native 端的事件请求的。在第 928 行,如果一个组件中定义的 queue 是 RCTJSThread,则在 JSExecutor 中执行 executeBlockOnJavaScriptQueue: 方法,具体执行代码如图 8 所示。

图 8 执行 executeBlockOnJavaScriptQueue: 方法

_javaScriptThread 是一个 NSThread 对象,看到这里才知道真正具备执行任务的是这里的 JavaScriptThread,而不是前面的 RCTJSThread。在 handBuffer 方法中之所以用 RCTJSThread,而不用 nil 替代,我的看法是为了可读性和扩展性。可读性是指如果在各个组件中将当前线程对象设置为 nil,使用者会比较迷惑;扩展性是指如果后面业务有扩展,发现根据 nil 比较不能满足需求,只需修改 RCTJSThread 初始化的地方,业务调用的地方完全没有感知。

RCTUIManagerQueue

RN 的 UI 组件调用都是在 RCTUIManagerQueue 完成的,关于它的创建如图 9 所示。

图 9 创建 RCTUIManagerQueue 代码

由于苹果在 iOS 8.0 之后引入了 NSQualityOfService,淡化了原有的线程优先级概念,所以 RN 在这里优先使用了 8.0 的新功能,而对 8.0 以下的沿用原有的方式。但不论用哪种方式,都保证 RCTUIManagerQueue 在并发队列中优先级是最高的。到这里或许有疑问了,UI 操作不是要在主线程里操作吗,这里为什么是在一个子线程中操作?其实在此执行的是 UI 的准备工作,当真正需要把 UI 元素加入主界面,开始图形绘制时,才需要在主线程里操作,具体代码见图 10。

>

图 10 UI 操作代码

这里 RCTUIManagerQueue 是一个先进先出的顺序队列,保证了 UI 的顺序执行不出错,但这里是把 UI 的一些需要准备的工作(比如计算 frame)放在一个子线程里面操作完成后,再统一提交给主线程进行操作的。这个过程是阻塞的,针对一些低端机型渲染复杂界面,会出现打开 RN 页面的一段空白页面的情况,这是 RN 需要优化的一个地方。

前面介绍了组件中线程的相关情况,针对平常开发中的自定义组件,有以下两点需要关注:

如果不通过 methodQueue 方法设定具体的执行队列(dispatch_queue_t),则系统会自动创建一个默认线程,线程名称为 ModuleNameQueue;

对同类别组件进行划分,采用相同的执行队列(比如系统 UI 组件都是在 RCTUIManagerQueue 中执行)。这样有两点好处,一是为了控制组件执行队列的无序生长,二也可以控制特殊情况下的线程并发数。

JSBundle 加载中的线程操作

前面叙述的组件相关的线程情况,从业务场景方面来看,略显简单,下面将介绍一下场景复杂点的线程操作。

React Native 中加载过程业务逻辑比较多,需要先将 JSBundle 资源文件加载进内存,同时解析 Native 端组件,将组件相关配置信息加载进内存,然后再执行 JS 代码。图 11 所示的 Native 端加载过程代码,在 RCTBatchedBridge.m 的 start 方法中。其中片段 1 是将 JSBundle 文件加载进内存,片段 2 是初始化 RN 在 Native 端组件,片段 3 是设置 JS 执行环境以及初始化组件的配置,片段 4 是执行 JS 代码。这 4 个代码片段对应 4 个任务,其中任务 4 依赖任务 1/2/3,需要在它们全部执行完毕后才能执行。任务 1/3 可以并行,没有依赖关系。任务 3 依赖任务 2,需要任务 2 执行完毕后才能开始执行。

图 11 Native 端加载过程代码

为控制任务 4 和任务 1/2/3 之间的依赖关系,定义了 dispatch_group_t initModulesAndLoadSource 管理依赖;而任务 3 依赖任务 2 是采取阻塞的方式。下面分别看各个任务中的处理情况。

先看片段 1 的代码,如图 12 所示。

图 12 片段 1 代码

dispatch_group_enter(group);

dispatch_async(queue, ^{

  dispatch_group_leave(group);

});

但这里并没有使用 dispatch_async,而是采用默认的同步方式。具体原因在于 loadSource 中有一部分属性是下一个队列需要使用到的,这部分属性的初始化需要在这个队列中进行阻塞的同步执行。LoadSource 方法中有一部分逻辑是异步的,这部分数据可以在 initModulesAndLoadSource 的 group 合并的时候处理。 片段 2 的处理比较简单,跳过直接看片段 3 的代码,如图 13 所示。

图 13 片段 3 代码详情

片段 3 中的任务是又一个复合任务,由一个新的 group(setupJSExecutorAndModuleConfig)来管理依赖。有两个并发任务,初始化 JS Executor(119-124 行)和获取 module 配置(127-133 行)。这两个并发任务都放在并发队列 bridgeQueue 中执行,完成后进行合并处理(135-150 行)。需要注意的是片段 3 中采用 dispatch_group_async(group, queue, ^{ });来执行队列中的任务,其效果与前文叙述的 dispatch_group_enter/dispatch_group_leave 相同。

从上面的分析可以看出,GCD 利用 dispatch_group_t 可以很好地处理线程间的依赖关系。里面的线程操作虽不能像前文中组件的线程对开发有直接帮助,但是一个很好的利用 GCD 解决复杂任务的实例。

图片中的线程

看过 SDWebImage 的源码的同学知道,SDWebImage 采用的是 NSOperationQueue 来管理线程。但是 RN 在 image 组件中并没有采用 NSOperationQueue,还是一如继往地使用 GCD,有图 14 为证。眼尖的同学会发现图中明明有一个 NSOperationQueue 变量 _imageDecodeQueue,这是干什么用的?有兴趣可以在工程中搜索一下这个变量,除了在这里定义了一下,没有在其他任何地方使用。

图 14 NSOperationQueue or GCD?

我猜当时作者是不是也在纠结要不要使用 NSOperationQueue,而决定用 GCD 之后忘了删掉这个变量。

既然决定了使用 GCD,就需要解决两个棘手的问题,控制线程的并发数以及取消线程的执行。这两个问题也是 GCD 与 NSOperationQueue 进行比较时谈论最多的问题,且普遍认为当有此类问题时,需要弃 GCD 而选 NSOperationQueue。下面就来叙述一下 RN 中是如何来解决这两个问题的。

最大并发数的控制

首先是控制线程的并发数。在 RCTImageLoader 中有一个属性 maxConcurrentLoadingTasks,如图 15 所示。除此之外,还有一个控制图片解码任务的并发数 maxConcurrentDecodingTasks。加载图片和解码图片是一项非常耗内存/CPU 的操作,所以需要根据业务需求的具体情况来灵活设定。

图 15 maxConcurrentLoadingTasks 属性

控制线程的最大并发数的逻辑在 RCTImageLoader 的 dequeueTasks 方法中,如图 16 所示。并发任务存储在数组 _pendingTasks 中,当前进行中的任务数存储在 _activeTasks 中。由于不能像 NSOperationQueue 中的任务一样,执行完毕后就被自动清除,在这里需要手动清除已经执行完毕的任务,将任务从 _pendingTasks 中移除,并改变并发任务数,具体在代码的 240 行至 251 行。

图 16 dequeueTasks 方法定义

接下来看控制任务的执行代码,在 267 行至 276 行。遍历需要执行的任务数组,如果规定的条件超过了最大并发任务数,中断操作;否则直接执行任务,同时将计数器加 1。由于所有任务都是在 _URLCacheQueue 这个顺序队列中执行的,且一次只能执行一个任务,所以并发的实现是在 RCTNetworkTask 中进行的,有兴趣的同学可以深入看看。

进展到这里,最大并发数的控制还有一个关键任务没有完成,就是如何保证加入队列中的任务能全部完成。具体操作分为三个方面:首先是在加载图片的入口方法中有一次调用,如图 17 所示。

图 17 图片加载入口方法中的调用

其次,需要处理等待任务。比如当前队列中已经有了最大并发数个任务了,下一个任务过来的时候只能暂时加入队列等待了。如果后续没有事件来调用 dequeueTasks 方法,超过最大并发数之外的任务将会得不到执行。一个通用的做法是用一个定时器来维持,定时扫描任务队列来执行任务。但是 RN 里面借助了图片渲染的逻辑巧妙地避开了这个,即在解码完成时调用一次 dequeueTasks 方法,这时候能保证等待任务能全部执行完毕,具体如图 18 所示代码。

图 18 解码完成,调用 dequeueTasks 方法

最后,还有一种情况,即在线程取消的时候也需要调用一次 dequeueTasks 方法,来保证线程取消的情况下任务也能继续完成。这样综合上述三种情况的调用,加入队列中的任务都能全部执行完毕了。

线程的取消

如果说上面的最大并发数的控制还可以有方法自定义实现,但是线程的取消一直是 GCD 中无法做到的,只能通过 NSOperation 的接口来实现。上文提到了加载图片并发操作是在 RCTNetworkTask 中实现的,而 RCTNetworkTask 调用的是 RN 中 RCTNetwork 中的代码,先来简单介绍一下 RCTNetwork 的实现。

图 19 所示的是与图片访问相关的实现类 RCTDataRequestHandler。可以看出,数据访问的任务是用 NSOperationQueue 来管理的,线程的取消是调用 NSOperation 的 cancel 方法来执行的。后面介绍到的图片下载任务的取消即基于此。

图 19 RCTDataRequestHandler 实现

回到图片组件中的线程取消上来。在 RCTImageLoader 的加载/解码图片的方法中返回参数为任务取消 block:RCTImageLoaderCancellationBlock。Block 的具体实现在每一个方法中,以 loadImageWithURLRequest 为例,如图 20 所示。[task cancel]调用的是上述的 NSOperation 的 cancel 方法。

图 20 取消加载

至此,RN 在优先使用 GCD 的情况下,完成了图片组件中的线程相关逻辑。还是回到最开始讨论的话题,GCD 在控制任务状态,比如取消、悬挂、等待、监控线程等,目前采取自定义方法没有很好的方式实现,还得借助于 NSOperation。而在控制最大并发数方面,RN 提供了一个很好的自定义实现的例子,值得学习。

结语

本文从组件、JSBundle 加载、图片中的线程三个方面,对 RN 的源代码实现,以具体的实例,叙述了 RN 中线程管理的详细情况。这三个例子,从技术实现上,复杂度逐步增加,覆盖了线程中任务依赖、最大并发数的控制、线程取消等经典讨论点。特别是在任务依赖、最大并发数的控制上,给我们呈现了用 GCD 来解决的一个很好的实例。

   
2266 次浏览       16
 
相关文章

手机软件测试用例设计实践
手机客户端UI测试分析
iPhone消息推送机制实现与探讨
Android手机开发(一)
 
相关文档

Android_UI官方设计教程
手机开发平台介绍
android拍照及上传功能
Android讲义智能手机开发
相关课程

Android高级移动应用程序
Android系统开发
Android应用开发
手机软件测试
最新课程计划
信息架构建模(基于UML+EA)3-21[北京]
软件架构设计师 3-21[北京]
图数据库与知识图谱 3-25[北京]
业务架构设计 4-11[北京]
SysML和EA系统设计与建模 4-22[北京]
DoDAF规范、模型与实例 5-23[北京]

android人机界面指南
Android手机开发(一)
Android手机开发(二)
Android手机开发(三)
Android手机开发(四)
iPhone消息推送机制实现探讨
手机软件测试用例设计实践
手机客户端UI测试分析
手机软件自动化测试研究报告
更多...   


Android高级移动应用程序
Android应用开发
Android系统开发
手机软件测试
嵌入式软件测试
Android软、硬、云整合


领先IT公司 android开发平台最佳实践
北京 Android开发技术进阶
某新能源领域企业 Android开发技术
某航天公司 Android、IOS应用软件开发
阿尔卡特 Linux内核驱动
艾默生 嵌入式软件架构设计
西门子 嵌入式架构设计
更多...