_mainQ_beginLoadingIfApplicable crash修复的若干建议

背景

相信从iOS14.0开始,很多app就应该遇到了_mainQ_beginLoadingIfApplicable这个crash:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
thread #36, stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
* frame #0: 0x0000000111de6134 libdispatch.dylib`_dispatch_assert_queue_fail + 99
frame #1: 0x0000000111de60cc libdispatch.dylib`dispatch_assert_queue + 152
frame #2: 0x000000011cd046a5 UIKitCore`-[UIImageView _mainQ_beginLoadingIfApplicable] + 65
frame #3: 0x000000011ccfd50f UIKitCore`-[UIImageView setHidden:] + 66
frame #4: 0x000000011c11edb8 UIKitCore`-[UIButton _updateImageView] + 539
frame #5: 0x000000011c11f81d UIKitCore`-[UIButton layoutSubviews] + 379
frame #6: 0x000000011cd529ce UIKitCore`-[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 2874
frame #7: 0x000000010d8ffd87 QuartzCore`-[CALayer layoutSublayers] + 258
frame #8: 0x000000010d906239 QuartzCore`CA::Layer::layout_if_needed(CA::Transaction*) + 575
frame #9: 0x000000010d911f91 QuartzCore`CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 65
frame #10: 0x000000010d852078 QuartzCore`CA::Context::commit_transaction(CA::Transaction*, double, double*) + 496
frame #11: 0x000000010d888e13 QuartzCore`CA::Transaction::commit() + 783
frame #12: 0x000000010d889616 QuartzCore`CA::Transaction::release_thread(void*) + 210
frame #13: 0x00007fff5dcda054 libsystem_pthread.dylib`_pthread_tsd_cleanup + 551
frame #14: 0x00007fff5dcdc512 libsystem_pthread.dylib`_pthread_exit + 70
frame #15: 0x00007fff5dcd9ddd libsystem_pthread.dylib`_pthread_wqthread_exit + 77
frame #16: 0x00007fff5dcd8afc libsystem_pthread.dylib`_pthread_wqthread + 481
frame #17: 0x00007fff5dcd7b77 libsystem_pthread.dylib`start_wqthread + 15

通过这个堆栈,几乎很难断定问题出在什么地方,唯一能知道的就是子线程操作了UI。如果有上报机制,可能还能提供更多的信息:例如crash在哪些页面。

值得庆幸的是最后解决了这个问题,本文也将记录在修复过程中做的各种尝试努力,希望能给出一些建议,但是切记,这并不作为该crash的通用解决方案。

前期尝试

一开始曾经尝试过各种方法,但仍然无济于事,线上仍然在crash。
这里列一下其中的hook方式:hook UIViewlayoutSublayersOfLayer:方法(抛开是否成功的因素,该方法影响范围比较大,要小心操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@implementation UIView (CrashFix)
+ (void)load {
if (@available(iOS 14.0, *)) {
dm_swizzleSelector([self class], @selector(layoutSublayersOfLayer:), @selector(my_layoutSublayersOfLayer:));
}
}

- (void)my_layoutSublayersOfLayer:(CALayer *)layer {
if ([NSThread isMainThread]) {
[self my_layoutSublayersOfLayer:layer];
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self my_layoutSublayersOfLayer:layer];
});
}
}
@end

后期尝试

由于前期的各种方案均未成功,所以最后采取了一种群狼战术:看到可疑的地方就进行人畜无害的那种修复(切记,不是发事故报告的那种修复)。

置换layer

如果可以清楚地知道主要crash在哪些页面,那么不妨一试这个方案:为了最大程度减小全局hook带来的影响,置换crash页面的layer,重写layoutSublayers方法,在里面进行主线程保护。如果仔细观察堆栈,就会发现layoutSublayers比前文提到的layoutSublayersOfLayer:更靠近CA::Layer::layout_if_needed,保护会更彻底一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@interface CustomLayer : CALayer
@end
@implementation CustomLayer
- (void)layoutSublayers {
if ([NSThread isMainThread]) {
[super layoutSublayers];
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[super layoutSublayers];
});
}
}
@end

@interface CustomView : UIView
@end
@implementation CustomView
+ (Class)layerClass {
return [CustomLayer class];
}
@end

@implementation YourViewController
- (void)loadView {
if (@available(iOS 14.0, *)) {
self.view = [[CustomView alloc] initWithFrame:CGRectMake(0, 0, width, height)];
} else {
[super loadView];
}
}
@end

网络回调保护 & 通知保护

仔细检查crash页面的各种网络回调和通知方法,无脑添加主线程保护即可

关联逻辑保护

有的时候可能会有这样的情况,crash虽然发生在这个页面,但是crash逻辑却不一定在这个页面,如何抓取这些可以的逻辑呢?

  • 既然每次都要崩溃到_mainQ_beginLoadingIfApplicable方法里面,那么我们就hook观察一下:把调用链打印一下。同时由于堆栈中每次都是从[UIImageView setHidden:]方法中过来的,所以我们也要hook一下进行标记。

这样每次进入这些crash页面的时候,就能打印出所有可疑的对象,我们有理由相信,crash堆栈和这些对象是有一定关系的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 本地调试代码,不会上线
#ifdef DEBUG

@implementation UIImageView(Safe)

+ (void)load {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if (@available(iOS 14.0, *)) {
SEL oldSEL = @selector(_mainQ_beginLoadingIfApplicable);
SEL newSEL = @selector(my_mainQ_beginLoadingIfApplicable);
[self swizzleMethod:oldSEL withMethod:newSEL error:nil];

oldSEL = @selector(setHidden:);
newSEL = @selector(my_setHidden:);
[self swizzleMethod:oldSEL withMethod:newSEL error:nil];
}
#pragma clang diagnostic pop
}

- (void)my_setHidden:(BO)hidden {
// 使用tag进行标记
self.tag = 1;
[self my_setHidden:hidden];
self.tag = 0;
}

- (void)my_mainQ_beginLoadingIfApplicable {
// 该部分代码比较耗时,所以先注释掉,调试的时候可以打开使用
if (self.tag == 1) {
UIView *view = self.superview;
NSMutableString *str = [NSMutableString string];
while (view) {
[str appendFormat:@"->%@", NSStringFromClass([view class])];
view = view.superview;
}
NSLog(@"UIImageView的祖先:%@", str);
}
[self my_mainQ_beginLoadingIfApplicable];
}
@end

#endif

打印结果如下所示:

1
2
3
4
5
6
7
8
2021-04-14 12:44:34.303547+0800 yourappname[536:47547] UIImageView的祖先:->_UIModernBarButton->_UIButtonBarButton->_UINavigationBarContentView->UINavigationBar->UILayoutContainerView->UIView->UIViewControllerWrapperView->UINavigationTransitionView->UILayoutContainerView->UIDropShadowView->UITransitionView->YourAppWindow
2021-04-14 12:44:34.306314+0800 yourappname[536:47547] UIImageView的祖先:->_UIModernBarButton->_UIButtonBarButton->_UINavigationBarContentView->UINavigationBar->UILayoutContainerView->UIView->UIViewControllerWrapperView->UINavigationTransitionView->UILayoutContainerView->UIDropShadowView->UITransitionView->YourAppWindow
2021-04-14 12:44:34.308032+0800 yourappname[536:47547] UIImageView的祖先:->UIButton->CardListView->CustomView->_UIParallaxDimmingView->UIView->UIViewControllerWrapperView->UINavigationTransitionView->UILayoutContainerView->UIView->UIViewControllerWrapperView->UINavigationTransitionView->UILayoutContainerView->UIDropShadowView->UITransitionView->YourAppWindow
2021-04-14 12:44:34.309758+0800 yourappname[536:47547] UIImageView的祖先:->UIButton->CustomNavigationBar->CustomView->_UIParallaxDimmingView->UIView->UIViewControllerWrapperView->UINavigationTransitionView->UILayoutContainerView->UIView->UIViewControllerWrapperView->UINavigationTransitionView->UILayoutContainerView->UIDropShadowView->UITransitionView->YourAppWindow
2021-04-14 12:44:34.309874+0800 yourappname[536:47547] UIImageView的祖先:->UIButton->CustomNavigationBar->CustomView->_UIParallaxDimmingView->UIView->UIViewControllerWrapperView->UINavigationTransitionView->UILayoutContainerView->UIView->UIViewControllerWrapperView->UINavigationTransitionView->UILayoutContainerView->UIDropShadowView->UITransitionView->YourAppWindow
2021-04-14 12:44:34.309997+0800 yourappname[536:47547] UIImageView的祖先:->UIButton->CustomNavigationBar->CustomView->_UIParallaxDimmingView->UIView->UIViewControllerWrapperView->UINavigationTransitionView->UILayoutContainerView->UIView->UIViewControllerWrapperView->UINavigationTransitionView->UILayoutContainerView->UIDropShadowView->UITransitionView->YourAppWindow
2021-04-14 12:44:34.310231+0800 yourappname[536:47547] UIImageView的祖先:->UIButton->CardListView->CustomView->_UIParallaxDimmingView->UIView->UIViewControllerWrapperView->UINavigationTransitionView->UILayoutContainerView->UIView->UIViewControllerWrapperView->UINavigationTransitionView->UILayoutContainerView->UIDropShadowView->UITransitionView->YourAppWindow
......

请仔细检查这些打印列表中涉及到的自定义页面,寻找其中一些可疑的点,例如KVO、通知等,请直接无脑主线程保护,因为人头担保总有意外。

为什么只有iOS14才有这个问题?

这是因为_mainQ_beginLoadingIfApplicable是在iOS14才出来的,在iOS13上进行hook的时候会失败,原因是没有这个方法。

总结

正如前面提到的,上述方案只是一些建议,并不作为通用解决方案。如果凑巧可以解决您的问题,并且您还想进一步搞清楚最后的crash原因,那么可以在这些群狼修复逻辑里面添加一些埋点进行上报,通过上报的埋点确定最终的根本原因。

-------------本文结束 感谢您的阅读-------------

本文标题:_mainQ_beginLoadingIfApplicable crash修复的若干建议

文章作者:lingyun

发布时间:2021年05月15日 - 18:05

最后更新:2021年05月15日 - 19:05

原始链接:https://tsuijunxi.github.io/2021/05/15/mainQ-beginLoadingIfApplicable-crash修复的若干建议/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。