背景
在使用ReactNative v0.51版本时,发现线上有个崩溃一直没有解决,为了完成App治理crash的目标,所以花了点时间研究如何解决这个问题。
Crash介绍
可以看到如下信息
- 崩溃位置
[RCTHTTPRequestHandler sendRequest:withDelegate:]
- 崩溃线程
#27 Thread - 子线程
- 崩溃原因
NSGenericException
- 崩溃描述
Task created in a session that has been invalidated
- 崩溃分布
没有很明显的特征
直接崩溃原因
根据崩溃原因NSGenericException,可以知道这是一个OC异常。我们先尝试找到直接导致崩溃的原因是什么,再找到真正触发的条件。
根据描述”Task created in a session that has been invalidated”,大概能看出一个失效的session对象尝试去创建task导致异常,而sendRequest:withDelegate:方法中创建task的地方只有一个
1 | NSURLSession *_session; |
而session为什么会invalidate的原因也很简单,因为[NSURLSession invalidateAndCancel].
触发条件
知道直接崩溃原因,我们就要找到触发崩溃的原因。根据上面的证据,我们推测RCTHTTPRequestHandler肯定调用了invalidateAndCancel,事实也正是这样。
1 | //RCTHTTPRequestHandler.mm |
如果_session被置为nil,则不会发生问题,所以肯定是[_session invalidateAndCancel]和_session = nil执行之间被打断了,结合之前的堆栈信息,卡顿发生在子线程,基本可以肯定这是一个多线程的问题,导致[RCTHTTPRequestHandler invalidate]的方法执行没有保证原子性。
那我们就要找出sendRequest:withDelegate:和invalidate各自的调用链。
sendRequest
根据崩溃堆栈,我们可以看到sendRequest是在RCTImageLoader中发起的。
1 | _URLRequestQueue = dispatch_queue_create("com.facebook.react.ImageLoaderURLRequestQueue", DISPATCH_QUEUE_SERIAL); |
所以sendRequest是在名为”com.facebook.react.ImageLoaderURLRequestQueue”的串行队列中执行.
invalidate
通过源码,可以找到invalidate的调用路径
1 | /* |
参考RN原理, 可以知道moduleData是在RN初始化的时候注册的模块信息,RCTHTTPRequestHandler也会生成其中一个moduleData。 那我们看下moduleData.methodQueue是什么,因为这就是invalidate执行的队列。
1 | - (void)setUpMethodQueue { |
可以看到,每个module都会又一个对应的串行methodQueue,并且名称的规则是”com.facebook.react.%@Queue”, 所以RCTHTTPRequestHandler对应的队列就是”com.facebook.react.HTTPRequestHandlerQueue”
也即,invalidate是在串行队列””com.facebook.react.HTTPRequestHandlerQueue””中执行。
还不够!
就算知道了sendRequest和invalidate方法在不同队列的线程中执行,还不能百分百确定一定会发生多线程问题,除非RCTCxxBridge.invalidate中触发的moduleData实例和RCTImageLoader触发的sendRequest中RCTHTTPRequestHandler实例是同一个对象。
1 | //RCTNetworkTask中获取RCTHTTPRequestHandler的方法 |
可以看到,RCTNetworkTask执行时用到的handler,是从RCTBridge之前注册好的module中去找到符合
1 | @interface RCTHTTPRequestHandler : NSObject <RCTURLRequestHandler, RCTInvalidating> |
至此可以发现,RCTHTTPRequestHandler其实生成的实例对象,在一个RCTBridge周期内只有一个。同一个RCTHTTPRequestHandler对象的invalidate和sendRequest的执行在不同队列的不同子线程。虽然两个队列都是串行,但是两个子线程之间互相之间没有约束,一个执行时可能会被另一个打断,从而导致执行了[_session invalidateAndCancel]之后执行[_session dataTaskWithRequest:request]导致crash。
怎么复现
我复现的方式是,在RCTHTTPRequestHandler的invalidate方法中插入sleep,加大RCTHTTPRequestHandler中invalidate方法被打断的概率,同时在外面模拟RCTBridge的invalidate。
1 | //RCTHTTPRequestHandler |
这样很容易能复现这个问题。
怎么修复
既然两个线程在不同的队列执行,那最简单的修复方式就是把他们的执行放到同一个队列中去,这样两块代码再执行的时候顺序不会被中途打断。
之前我们也看到,每个moduleData都有自己的methodQueue,那比较好的方式还是在RCTHTTPRequestHandler内部用他自己的methodQueue。
1 | @synthesize methodQueue = _methodQueue; |
给ReactNative提PR
既然这里存在问题,并且改动还算合理,我就尝试把这个修改提交给ReactNative,看人家会不会采纳。最终PR还是被合并了,Bingo!
https://github.com/facebook/react-native/pull/22746
总结
通过这个crash我们可以看到,多线程的问题比较隐蔽,所以我们平时在写代码和做code review时,要特别注意线程安全,对共享变量的使用要比较小心。