如何定位 CA::Render 的崩溃问题

1,416 阅读2分钟

背景

近期辅助业务方定位了一个问题,看崩溃堆栈,没有任何直接有用的信息。在网上可以看到一些相关的讨论,但是都没有最终的结论。

developer.apple.com/forums/thre…

stackoverflow.com/questions/4…

崩溃堆栈如下所示:

0	CoreGraphics	_ERROR_CGDataProvider_BufferIsNotReadable()
1	CoreGraphics	_CGDataProviderRetainBytePtr()
2	QuartzCore	CA::Render::(anonymous namespace)::create_image_from_image_data(CGImage*, CGColorSpace*, unsigned int, unsigned int, double)()
3	QuartzCore	CA::Render::create_image(CGImage*, CGColorSpace*, unsigned int, double)()
4	QuartzCore	CA::Render::copy_image(CGImage*, CGColorSpace*, unsigned int, double, double)()
5	QuartzCore	CA::Render::prepare_image(CGImage*, CGColorSpace*, unsigned int, double)()
6	QuartzCore	CA::Layer::prepare_commit(CA::Transaction*)()
7	QuartzCore	CA::Context::commit_transaction(CA::Transaction*)()
8	QuartzCore	CA::Transaction::commit()()
9	QuartzCore	CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*)()
10	CoreFoundation	___CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__()
11	CoreFoundation	___CFRunLoopDoObservers()
12	CoreFoundation	___CFRunLoopRun()
13	CoreFoundation	_CFRunLoopRunSpecific()
14	GraphicsServices	_GSEventRunModal()
15	UIKit	_UIApplicationMain()
17	libdyld.dylib	_start()

崩溃排查

思路1

崩溃发生在 CA::Render::prepare_image(CGImage*, CGColorSpace*, unsigned int, double)() 尝试将 CGImage 渲染出来,根据图片定位业务方。

step1 获取 CGImage 对象

(lldb) po $x20

<CGImage 0x1d03dbb70>

   <<CGColorSpace 0x1d00b6e60> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1)>

       width = 1056, height = 414, bpc = 8, bpp = 32, row bytes = 4224

       kCGImageAlphaPremultipliedFirst | kCGImageByteOrder32Little

       is mask? No, has mask? No, has matte? No, should interpolate? Yes

step2 将 CGImage 转换为 UIImage

+ (UIImage *)imageWithCGImage:(CGImageRef)cgImage;

step3 将 UIImage 转换为 NSData

NSData * __nullable UIImagePNGRepresentation(UIImage * __nonnull image)

lldb 抛出 error,尝试了几次都无法成功。社区有人在遇到相同问题时也曾尝试将 CGImage 渲染出来,都以失败告终,这个思路被废弃。

思路2

最近在死磕子线程更新 UI 的问题,子线程更新 UI 后在线程退出时会执行 CA::Transaction::commit,这个方法会调用CA::Layer::layer_being_drawn(CA::Transaction*, unsigned int) 来获当前正在渲染的 layer(这个描述不一定合适),layer_being_drawn 的地址可以通过断点或者 image lookup 获取。在这个崩溃栈帧的寄存器里面,可以获取到 CA::Layer 和 CA::Transaction 两个参数,在 lldb 动态调用 layer_being_drawn 尝试获取当前的 layer 对象。

CA::Layer: 0x000000014b201560
CA::Transaction: 0x000000014711f000
(lldb) po void *(*$layer_being_drawn)(void *, void *,int) = (void *(*)(void *,void *, int))0x1b56493d8
(lldb) po $layer_being_drawn((void*)0x000000014b201560, (void *)0x000000014711f000, 0)

输出结果如下,此时仍然没有获取到业务相关的信息。

<UIWindowLayer:0x1d503c020; position = CGPoint (187.5 406); bounds = CGRect (0 0; 375 812); sublayers = (<UIWindowLayer: 0x1d503c040>); allowsGroupOpacity = YES; rasterizationScale = 3>

尝试遍历 layer 的图层树

NSString* layerTreeDescription(CALayer *layer, int depth, int max_depth) {
    NSMutableString *debugDescription = [[NSMutableString alloc] initWithString:[layer debugDescription]];
    depth++;
    if (depth <= max_depth && layer.sublayers.count > 0) {
        for (CALayer *sublayer in layer.sublayers) {
            [debugDescription appendFormat:@"\n%d - %@",
             depth,
             layerTreeDescription(sublayer, depth, max_depth)];
        }
    }
    return [debugDescription copy];
}

在输出里面搜索 CGImage,发现崩溃时正在渲染的 layer 是 RLAsyncLayer 的 contents 属性值。

19 - <RLAsyncLayer:0x163e3c900; position = CGPoint (176 69); bounds = CGRect (0 0; 352 138); delegate = <RichLabel: 0x1630f21a0; frame = (0 0; 352 138); text = ''; opaque = NO; layer = <RLAsyncLayer: 0x163e3c900>>; contents = <CGImage 0x1d03dbb70>

向上查找 RLAsyncLayer 的 superLayer。

18 - <CALayer:0x1d7c21c80; position = CGPoint (176.5 69); bounds = CGRect (0 0; 353 138); delegate = <UIView: 0x164490700; frame = (0 0; 353 138); gestureRecognizers = <NSArray: 0x1689b17f0>; layer = <CALayer: 0x1d7c21c80>>; sublayers = (<CALayer: 0x1d7a3b180>, <RLAsyncLayer: 0x163e3c900>); allowsGroupOpacity = YES; backgroundColor = <CGColor 0x1d0485ff0>>

继续查找, 此时定位到了业务相关的视图 XXXCommentCell。

17 - <CALayer:0x1d7838ea0; position = CGPoint (176.5 69); bounds = CGRect (0 0; 353 138); delegate = <XXXCommentCell: 0x145ef4480; baseClass = UICollectionViewCell; frame = (0 0; 353 138); layer = <CALayer: 0x1d7838ea0>>; sublayers = (<CALayer: 0x1d7c21c80>); allowsGroupOpacity = YES; backgroundColor = <CGColor 0x1d0485ff0>>

这之后的父视图,可以获取详细的视图信息,这些信息可以精准定位到业务信息。

思路3

定位到问题图片之后,再回顾问题定位流程发现 layer_being_drawn 可以用替换成更简单的方式,直接获取最顶层 window 的 layer,然后遍历 layer 子视图, 检索 CGImage 的信息,理论上效果是一致。

结论

这个崩溃我只是辅助定位到了问题图片找到了具体的业务方,图片被下掉之后崩溃解决,崩溃的根本原因后续并没有继续跟进,这个定位方式相对简单,有些人认为是小菜一碟,然而之前跟进这个崩溃的同学在近一个月内都没有找到有效的线索(毕竟术业有专攻,这个同学是做直播相关的,希望不被他看到)。总之,这种方式还是非常有效的,希望能给遇到同样的问题同学提供一些思路(我又水了一篇,狗头保命)。