sdk 中涉及到 UI 操作的时候,一定要注意线程问题!一定要注意线程问题!一定要注意线程问题!
从最初开始学习 iOS 的时候,我们就被告知 UI 操作一定要放在主线程进行,这是因为 UIKit 的方法不是线程安全的,保证线程安全需要极大的开销。
试想一下几种场景:
- 两个线程同时设置同一个背景图片,则很有可能因为当前图片被释放了两次而导致应用崩溃;
- 两个线程同时设置同一个 UIView 的背景色,则很有可能渲染显示的是颜色 A,而此时在 UIView 逻辑树上的背景颜色属性为 B;
- 两个线程同时操作 View 的树形结构:在线程 A 中 for 循环遍历并操作当前 View 的所有 subView,而此时线程 B 中将某个 subView 直接删除,这就导致了错乱,甚至导致应用崩溃。
出了什么问题?
最近 hybrid sdk 收尾,偶然发现线上有一类 crash 最近两个版本稳步上升,而且可以肯定的是我负责的 sdk 提供的 web 容器导致的。__ZL17_WebTryThreadLockb 是函数调用栈最后调用的 api,第一次看到的时候,咦,这是什么鬼?原谅我见的世面少,经过一顿 Stack Overflow,翻阅了好几个相关的问题,总结起来就是在子线程执行了 UI 线程的操作。
crash 线程详细如下:
Thread 17 Crashed:
0 WebCore __ZL17_WebTryThreadLockb
1 WebCore _WebThreadLock
2 UIKit -[UIWebBrowserView resignFirstResponder]
3 UIKit -[UIResponder _resignIfContainsFirstResponder]
4 UIKit -[UIView(Hierarchy) _willMoveToWindow:]
5 UIKit -[UIView(Internal) _addSubview:positioned:relativeTo:]
6 UIKit ___53-[_UINavigationParallaxTransition animateTransition:]_block_invoke_2
7 UIKit +[UIView(Animation) performWithoutAnimation:]
8 UIKit ___53-[_UINavigationParallaxTransition animateTransition:]_block_invoke
9 UIKit +[UIView(Internal) _performBlockDelayingTriggeringResponderEvents:]
10 UIKit -[_UINavigationParallaxTransition animateTransition:]
11 UIKit -[UINavigationController _startCustomTransition:]
12 UIKit -[UINavigationController _startDeferredTransitionIfNeeded:]
13 UIKit -[UINavigationController __viewWillLayoutSubviews]
14 UIKit -[UILayoutContainerView layoutSubviews]
15 UIKit -[UIView(CALayerDelegate) layoutSublayersOfLayer:]
16 QuartzCore -[CALayer layoutSublayers]
17 QuartzCore __ZN2CA5Layer16layout_if_neededEPNS_11TransactionE
18 QuartzCore __ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE
19 QuartzCore __ZN2CA7Context18commit_transactionEPNS_11TransactionE
20 QuartzCore __ZN2CA11Transaction6commitEv
21 QuartzCore __ZN2CA11Transaction14release_threadEPv
22 libsystem_pthread.dylib __pthread_tsd_cleanup
23 libsystem_pthread.dylib __pthread_exit
24 libsystem_pthread.dylib __pthread_wqthread
25 libsystem_pthread.dylib _start_wqthread
原因是什么?
Stack Overflow 上的问题和解答贴出两个大家一起参考:
app-crash-no-idea-why
ios-webtrythreadlock-crash
既然确定了是因为在子线程执行了 UI 操作导致,那么,我的 sdk 里哪里发生了这一类调用呢?先回顾了下 sdk 的所有关键模块,排除了核心部分,那些一定不会涉及到 ui 操作的模块后,就剩下 webview模块、bridge 模块和 debug 模块了。
那就继续 review 吧,一个一个来~ webview 检查了一遍都是在主线程执行的 ui 操作,应该不会出问题;debug 模块,刚开始写的时候打 log 部分确实没有考虑线程操作问题,大师兄已经帮我 review 出来并修复掉了,review 了两边也没发现有问题,应该也不会是 debug 模块导致的。那就剩 bridge 模块了~~~
艾玛,最头疼的一部分,因为 bridge 接口是提供给前端 h5 页面和 native 模块进行交互的 api,包括了公共 bridge,还包括 业务线自定义的 bridge 接口,太多了
,着实头疼~~~没办法,硬着头皮找大师兄帮忙,看看能不能缩短问题定位的周期。
大师兄毕竟大师兄!很多让我查一下分享 api、通讯录 api、照片库 api、页面
跳转 api,哎哟我去!这一下就缩小了范围!当然,其实这四类模块 bridge 接口也是一大群 api 啊!必须再进一步缩小范围。
公司的 crash 日志平台里包含一个 crash 现场的功能,以前没用过,这两天刚发现的,别说,还真好用,crash 现场记录了用户在客户端发生 crash 的前后都在做什么,比如哪个页面?比如调用了哪些接口?前后用户是怎么操作的?用户当前设备的系统信息?用户信息?等等。
很快,比对了几个同类 crash 的现场后,均发现一个现象,都是在某个业务页面中发生的,并且那个页面就是用的 sdk 提供的 web 容器!巧了!看来就是这里了!联系了下这个页面的开发同学都调用了哪些 bridge 方法?其实自己在 debug 的时候也可以通过断点拿到,但是,防止由于业务逻辑问题错误一些漏掉的,最好还是咨询下相关业务开发。两边一对比,ok,接口一致了。
很快,定位到了通讯录授权回调的部分确实有子线程执行 UI 线程操作的调用,代码如下:
ABAuthorizationStatus authStatus = ABAddressBookGetAuthorizationStatus();
if (authStatus == kABAuthorizationStatusNotDetermined)
{
//获取授权
ABAddressBookRef addressBook = ABAddressBookCreate();
ABAddressBookRequestAccessWithCompletion( addressBook, ^(bool granted, CFErrorRef error) {
if (granted)
{
[self openContact];
}
else
{
[self showAlertView];
}
});
}
唉,这段代码确实问题大啊!虽然丢人,但是一为了警醒自己,二为了给大家也都指出下边今后犯同样的问题,这里贴出来大家看一看。
这里的问题至少有两个:
- 子线程执行 UI 线程操作;
- 内存泄漏
如何解决?
问题的原因找到了,怎么办呢?线上已经发版了,而且大师兄还提醒,从上个版本就有这类 crash 啦!
额,肿么办,肿么办~~~
不能慌,以后还会经历很多类似的线上问题,不能慌!冷静冷静~对了,patch,发个 patch 修复吧!好,patch,也就能期望 patch 可以解决了。不 patch 的话,老大看到这一类蹭蹭蹭往上涨的 crash,不得开了我!不 patch 的话,就得下个版本修复了,用户体验多不好!不 patch 的话,根据墨菲定律,可能发生的事就一定会发生!这个偶现的 crash 线上一定会发生!
正确的写法:
ABAuthorizationStatus authStatus = ABAddressBookGetAuthorizationStatus();
if (authStatus == kABAuthorizationStatusNotDetermined)
{
//获取授权
ABAddressBookRef addressBook = ABAddressBookCreate();
ABAddressBookRequestAccessWithCompletion( addressBook, ^(bool granted, CFErrorRef error) {
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
if (granted)
{
[weakSelf openContact];
}
else
{
[weakSelf showAlertView];
}
});
});
}
现在,问题来了,这写法能 patch 吗?不敢说,这种多线程相关的还真不确定能 patch 搞定,没办法,咨询吧!
需要确认的部分:
1.待 patch 的方法调用了 C 函数;
2.dispatch 函数;
3. weak 指针;
4. C 函数传递 block 参数;
联系了下,得到的结论是:都支持!
欧耶!开干吧~修复代码部分、自测、准备 patch、申请 patch、自测、提交 QA 测试、灰度放量~
ok!
网友评论
weakSelf写的没有意义。。。