iOS开发——__ZL17_WebTryThreadLockb

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];
            }
        });
    }

唉,这段代码确实问题大啊!虽然丢人,但是一为了警醒自己,二为了给大家也都指出下边今后犯同样的问题,这里贴出来大家看一看。

这里的问题至少有两个:

  1. 子线程执行 UI 线程操作;
  2. 内存泄漏

如何解决?

问题的原因找到了,怎么办呢?线上已经发版了,而且大师兄还提醒,从上个版本就有这类 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!

你可能感兴趣的:(iOS开发——__ZL17_WebTryThreadLockb)