iOS疑难崩溃定位及修复实例(CATiledLayer)

146 阅读22分钟

前言

CATiledLayer是一个iOS中UI组件,具有分片异步绘制的能力,常用来实现查看大图的功能。它的分片绘制能力在查看大图的场景中非常适用,因为它只绘制屏幕中的分片,从而可以减轻GPU/CPU绘制和 内存的压力。虽然CATiledLaver的独特异步分片绘制能力带来了性能上的提升,但同时也引入了一些问题。在接下来的文章中,我们将讨论由CATiledLayer所引发的两个线上crash,并介绍用汇编调试技巧定位修复这两个crash的过程。希望这些经验能为读者带来一些启示和参考,也期待大家一起交流探讨更多疑难crash修复定位的方法经验。

案例一: tiled_layer_render

崩溃堆栈

系统版本:13.5.1(17F80)

#25 Thread
SIGSEGV
SEGV_ACCERR

0 libobjc.A.dylib	_objc_release + 16
1 QuartzCore	tiled_layer_render(_CAImageProvider*, unsigned int, unsigned int, unsigned int, unsigned int, void*) + 1464
2 QuartzCore	CAImageProviderThread(unsigned int*, bool) + 940
3 libdispatch.dylib	__dispatch_client_callout + 16
4 libdispatch.dylib	_dispatch_lane_serial_drain$VARIANT$armv81 + 564
5 libdispatch.dylib	_dispatch_lane_invoke$VARIANT$armv81 + 396
6 libdispatch.dylib	_dispatch_workloop_worker_thread + 580
7 libsystem_pthread.dylib	_pthread_wqthread + 272

定位

崩渍类型SIGSEGV,表示野指针问题,crash在_objc_release方法中,说明是在释放对象的时候,发现对象已经被释放,问题应该发生在QuartzCore库的tiled_layer_render方法中。但tiled_layer_render CAImageProviderThread方法属于系统库QuartzCore,并且不开源,如何定位野指针对象是个问题。

查看crash原始数据,获取崩溃偏移量:

#25 Thread
SIGSEGV
SEGV_ACCERR

0 libobjc.A.dylib	0x00000001885feaa0 objc_release + 16
1 QuartzCore	0x000000018f2b99fc 0x000000018f25e000 + 375292
2 QuartzCore	0x000000018f377984 0x000000018f25e000 + 1153412
3 libdispatch.dylib	0x0000000188587524 0x000000018852c000 + 374052
4 libdispatch.dylib	0x0000000188564b3c 0x000000018852c000 + 232252
5 libdispatch.dylib	0x000000018856554c 0x000000018852c000 + 234828
6 libdispatch.dylib	0×000000018856e84c 0x000000018852c000 + 272460
7 libsystem_pthread.dylib	0x00008001885d8b74 _pthread_wqthread + 272

根据原始崩溃数据可以解读出三个信息

  1. 由行1得知,崩溃发生时,QuartzCore基地址为0x18f25e000
  2. 由行1得知,崩溃位于QuartzCore基地址偏移375292 byte的地方
  3. 由系统版本,得知QuartzCore版本为iOS13.5.1中的版本

然后使用真机查看调试崩溃位置的汇编代码

  1. 使用iOS13.5.1运行app,查看QuartzCore基地址:
(lldb) image list | grep 'QuartzCore'
[  0] 762783A0-5853-3B01-9DF8-E4DD633E1C9A 0x0000000196db6000 /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.5.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/QuartzCore.framework/QuartzCore 

2. 根据bugly的偏移位 +基地址算出 QuartzCore crash 的具体位置:

0x0000000196db6000 + 0x5b9fc(375292) = Ox196e119fc

3. 对crash位置进行断点:

(lldb) breakpoint set -a 0x196E119FC
Breakpoint 3: where = QuartzCore`tiled_layer_render(_CAImageProvider*, unsigned int, unsigned int, unsigned int, unsigned int, void*) + 231, address = 0x196E119FC

4. 使用tiledlayer_render业务场景,触发断点:

image.png

至此我们已经定位到了发生问题的汇编代码,下面尝试调试复现堆栈、找到野指针对象。 我们断点的地址是0x196e119fc,实际发生crash应该是在上一行,也就是执行367行的bl objc_msgSend崩溃的(bl命令特性)。但是367行跳转到 objc_msgSend 函数,眼bugly上的crash堆 栈不符。不过 objc_msgSend 有调用上的优化,这里先断点到367行,然后 step in 进到objc_msgSend里找找其他线索:

breakpoint set -a 0x196119f8
Breakpoint 6: where = QuartzCore`tiled_layer_render(_CAImageProvider*, unsigned int, unsigned int, unsigned int, unsigned int, void*), address = ....

objc_msgSend 汇编代码如下:

libobjc.A.dylib`objc_msgSend:
->  0x1b460e5c0 <+0>:   cmp    x0, #0x0
    0x1b460e5c4 <+4>:   b.le   0x1b460e668               ; <+168>
    0x1b460e5c8 <+8>:   ldr    x13, [x0]
    0x1b460e5cc <+12>:  and    x16, x13, #0xffffffff8
    0x1b460e5d0 <+16>:  mov    x15, x16
    0x1b460e5d4 <+20>:  ldr    x11, [x16, #0x10]
    0x1b460e5d8 <+24>:  and    x10, x11, #0xfffffffffffe
    0x1b460e5dc <+28>:  tbnz   w11, #0x0, 0x1b460e630    ; <+112>
    0x1b460e5e0 <+32>:  eor    x12, x1, x1, lsr #7
    0x1b460e5e4 <+36>:  and    x12, x12, x11, lsr #48
    0x1b460e5e8 <+40>:  add    x13, x10, x12, lsl #4
    0x1b460e5ec <+44>:  ldp    x17, x9, [x13], #-0x10
    0x1b460e5f0 <+48>:  cmp    x9, x1
    0x1b460e5f4 <+52>:  b.ne   0x1b460e600               ; <+64>
    0x1b460e5f8 <+56>:  eor    x17, x17, x16
    0x1b460e5fc <+60>:  br     x17
    0x1b460e600 <+64>:  cbz    x9, 0x1b460e940           ; _objc_msgSend_uncached
    0x1b460e604 <+68>:  cmp    x13, x10
    0x1b460e608 <+72>:  b.hs   0x1b460e5ec               ; <+44>
    0x1b460e60c <+76>:  add    x13, x10, x11, lsr #44
    0x1b460e610 <+80>:  add    x12, x10, x12, lsl #4
    0x1b460e614 <+84>:  ldp    x17, x9, [x13], #-0x10
    0x1b460e618 <+88>:  cmp    x9, x1
    0x1b460e61c <+92>:  b.eq   0x1b460e5f8               ; <+56>
    0x1b460e620 <+96>:  cmp    x9, #0x0
    0x1b460e624 <+100>: ccmp   x13, x12, #0x0, ne
    0x1b460e628 <+104>: b.hi   0x1b460e614               ; <+84>
    0x1b460e62c <+108>: b      0x1b460e940               ; _objc_msgSend_uncached
    0x1b460e630 <+112>: adrp   x9, 210022
    0x1b460e634 <+116>: add    x9, x9, #0x598
    0x1b460e638 <+120>: sub    x12, x1, x9
    0x1b460e63c <+124>: lsr    x17, x11, #48
    0x1b460e640 <+128>: lsr    w9, w12, w17
    0x1b460e644 <+132>: and    x9, x9, x11, lsr #53
    0x1b460e648 <+136>: ldr    x17, [x10, x9, lsl #3]
    0x1b460e64c <+140>: cmp    x12, w17, uxtw
    0x1b460e650 <+144>: b.ne   0x1b460e65c               ; <+156>
    0x1b460e654 <+148>: sub    x17, x16, x17, lsr #32
    0x1b460e658 <+152>: br     x17
    0x1b460e65c <+156>: ldursw x9, [x10, #-0x8]
    0x1b460e660 <+160>: add    x16, x16, x9
    0x1b460e664 <+164>: b      0x1b460e5d4               ; <+20>
    0x1b460e668 <+168>: b.eq   0x1b460e68c               ; <+204>
    0x1b460e66c <+172>: and    x10, x0, #0x7
    0x1b460e670 <+176>: asr    x11, x0, #55
    0x1b460e674 <+180>: cmp    x10, #0x7
    0x1b460e678 <+184>: csel   x12, x11, x10, eq
    0x1b460e67c <+188>: adrp   x10, 267833
    0x1b460e680 <+192>: add    x10, x10, #0xb20          ; objc_debug_taggedpointer_classes
    0x1b460e684 <+196>: ldr    x16, [x10, x12, lsl #3]
    0x1b460e688 <+200>: b      0x1b460e5d0               ; <+16>
    0x1b460e68c <+204>: mov    x1, #0x0
    0x1b460e690 <+208>: movi   d0, #0000000000000000
    0x1b460e694 <+212>: movi   d1, #0000000000000000
    0x1b460e698 <+216>: movi   d2, #0000000000000000
    0x1b460e69c <+220>: movi   d3, #0000000000000000
    0x1b460e6a0 <+224>: ret    
    0x1b460e6a4 <+228>: nop    
    0x1b460e6a8 <+232>: nop    
    0x1b460e6ac <+236>: nop    
    0x1b460e6b0 <+240>: nop    
    0x1b460e6b4 <+244>: nop    
    0x1b460e6b8 <+248>: nop    
    0x1b460e6bc <+252>: nop    

这里可以重点关注跳转指令(b、br、cbz、cbnz、tbz、tbnz),因为我们crash堆栈是tiled_layer_render -> objc_release,而现在调用堆栈是 tiled_layer_render -> objc_msg, 所以问题发生前一定先进行了跳转。 ARM64中有许多跳转指令:b、br、bl、blr、br、cbz、tbz、b.le、b.ne 等,先排除bl、blr指令,因为这两个指令在跳转时会修改LR寄存器,保存执行位置和环境到栈中,从调用堆栈的角度看,就是栈顶新增了一层函数调用:

//现堆栈
objc_msgSend
tiled_layer_render (_CAImageProvider*, unsigned int, unsigned int, unsigned int, unsigned int, void*)
CAImageProviderThread (unsigned int*, bool) + 940

//bl、blr跳转后的效果
bl_target_function
objc_msgSend
tiled_layer_render (_CAImageProvider*, unsigned int, unsigned int, unsigned int, unsigned int, void*)
CAImageProviderThread (unsigned int*, bool) + 940


//b、br、cbz 等跳转后的效果
b_target_function
tiled_layer_render (_CAImageProvider*, unsigned int, unsigned int, unsigned int, unsigned int, void*)
CAImageProviderThread(unsigned int*, bool) +940

在objc_msgSend 里单步 step in 调试,遇到 bl、blr 指令step over,发现在14行的br指令跳转到-[CALayer drawlnContext:]中:

image.png

同样,这里使用 step in 调试,遇到bl、blr 使用step over,在drawlnContext: 最后一行走到了objc_release:

image.png

step in后出现和bugly相同的堆栈:

image.png

在 objc4 源码中找到 objc_release 方法实现:

__attribute__((aligned(16), flatten, noinline))
void 
objc_release(id obj)
{
    if (_objc_isTaggedPointerOrNil(obj)) return;
    return obj->release();
}

发现 objc_release 第一个参数为需要释放的对象。arm64中,x0-x7 为参数寄存器,从x0 开始依次存储函数的第x个参数。打印 X0 寄存器中的参数:

(lldb) p/x $x0
(unsigned long) $0 = 0x000000013d116850
(lldb) po 0x000000013d116850
<MyImageBrowserTiledView: 0x13d116850; frame = (0 114; 375 375); layer = <CATiledLayer: 0x....

由此可知crash时是在释放我们的分块加载图片控件 MylmageBrowserTiledView(大致实现见附录)。 从汇编代码看,drawInContext: 对 MylmageBrowserTiledView 进行 retain、release 操作是为了执行一个 objc_msgSend 方法。objc_msgSend 第一个参数是消息接受对象,第二个参数是方法名,打印x1寄存器可以看到方法名 drawLayer:inContext::

image.png

MylmageBrowserTiledView 的 layer 为 CATiledLayer 类型,看现象 CATiledLayer 会在子线程调用drawInContext: 方法,而drawlnContext:内部则调用 MylmageBrowserTiledView drawLayer:inContext:方法,调用前后需要对MylmageBrowserTiledView retain、release,和主线程对MylmageBrowserTiledView 的引用计数操作有冲突,所以产生了MylmageBrowserTiledView 的线程安全问题。 QuartzCore不开源,但GUN有高仿版:github.com/gnustep/lib… CALayer drawlnContext:方法:

- (void) drawInContext: (ContextRef)context
{
    if ([_delegate respondsToSelector: @selector (drawLayer:inContext:)]) {
        [_delegate drawLayer: self inContext: context];
    }
}

跟我们调试观察到的现象基本一致。

问题结论:CATiledLayer 绘制在子线程中进行,同时 MylmageBrowserTiledView 又会被主线程操作,多线程操作引用计数使得计数不准确,导致对象在某一场景被提前释放,其他使用场景出现野指针问题

修复

问题看起来是CATiledLayer异步渲染的特性导致的,那么苹果为什么不把QuartzCore、UIKit 设计成线程安全的?私以为这是出于性能的考虑,多线程同时访问同一个对象确实会发生问题,但如果错开时间访问就可以兼顾性能和安全。这里我的解决方案也是这个思路,即避免子线程的绘制和主线程的释放同时发生。 具体实施就是在 MylmageBrowserTiledView 释放之前,提前将MylmageBrowserTiledView从视图树中移除,从视图树中移除后就不会持续产生子线程的渲染任务,这样就不会和后续的MylmageBrowserTiledView释放产生冲突。比如页面返回的时候:

- (void)back {
    // 先从视图树中移除TiledView
    for (MyImageBrowserTiledView *view in tiledViews) {
        [view removeFromSuperview]:
    }
    //dismiss的动画后,TiledView dealloc的时候,子线程的渲染己经结束了
    [self dismissViewControllerAnimated:YES completion:nil];
}

案例二:UITraitCollection

崩溃堆栈如下:

#26 Thread
SIGSEGV
SEGV_ACCERR

0 libobjc.A.dylib           objc_release + 16
1 UIKitCore                 +[UITraitCollection _setCurrentTraitCollection:] + 44
2 UIKitCore                 -[UIView(CALayerDelegate) drawLayer:inContext:] + 632
3 QuartzCore                tiled_layer_render(_CAImageProvider*, unsigned int, unsigned int, unsigned int, unsigned int, void*) + 1456
4 QuartzCore                CAImageProviderThread(unsigned int*, bool) + 980
5 libdispatch.dylib         __dispatch_client_callout + 20
6 libdispatch.dylib         _dispatch_continuation_pop + 500
7 libdispatch.dylib         _dispatch_async_redirect_invoke + 584
8 libdispatch.dylib         _dispatch_root_queue_drain + 396
9 libdispatch.dylib         _dispatch_worker_thread2 + 164
10 libsystem_pthread.dylib  _pthread_wqthread + 228

同案例一,崩溃类型SIGSEGV,表示野指针问题,crash 在_objc_release 方法中,说明是在释放对象的时候,发现对象已经被释放。根据崩溃堆栈倒数第二行 +[UITraitCollection _setCurrentTraitCollection:] 不难猜测,可能是在set方法中释放老对象时出现的问题。bugly堆栈切到原始数据,找到set方法中的crash位置的偏移量 0x190824。获取 iOS15.5 真机运行app中的 UIKitCore 库加载地址:

(lldb) image list | grep "UIKitCore"
[ 0] 3ED35565-456D-33B-B554-6C567FA81585 0x0000000192626000 /Users/admin/Library/Developer/Xcode/iOS DeviceSupport/15.5 (xxx)/Symbols/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore

计算crash位置:

0x190824 + 0x192626000 = 0×192766824

断点:

(lldb) breakpoint set -a 1927B6824
Breakpoint 31: where = UIKitCore + [UITraitCollection _setCurrentTraitCollection...

image.png 根据crash堆栈_setCurrentTraitCollection -> objc_release 可知,当前断点位置上一行的 bl 指令会执行到 objc_release 方法。打印x0 寄存器:

(lldb) p/x $x0
(unsigned long) $49 = 0×0000000000000000

可以看到x0寄存器没有数据,也许是我们调试环境没有100%复刻线上问题环境,所以这里没有数据,不过没关系,我们可以通过阅读汇编代码来了解这里释放的是什么对象:

1 UIKitCore' + [UITraitCollection _setCurrentTraitCollection:]:
    0x1927b67f8 <+0>:   pacibsp
    0x1927b67fc <+4>:   stp     x20, x19, [sp, #-0x20]!
    0x1927b6800 <+8>:   stp     x29, x30, [sp, #0x10]
    0x1927b6804 <+12>:  add     x29, sp, #0x10
    0x1927b6808 <+16>:  mov     x20, x2
    0x1927b680c <+20>:  bl      0x1927affdc             ; GetTraitCollectionTSD
    0x1927b6810 <+24>:  mov     x19, x0
    0x1927b6814 <+28>:  ldr     x0, [x0]
    0x1927b6818 <+32>:  cmp     x0, x20
    0x1927b681c <+36>:  b.eq    0x1927b6854             ; <+92>
    0x1927b6820 <+40>:  bl      0x18fee6de8
    0x1927b6824 <+44>:  cbz     x20, 0x1927b683C         ; <+68>
    0x1927b6828 <+48>:  adrp    x1, 345421
    0x1927b682c <+52>:  add     x1, x1, #0x248           ; _UITraitCollectionlsFallbackKey
    0x1927b6830 <+56>:  mov     x0, x20
    0x1927b6834 <+60>:  bl      0x190770840             ; symbol stub for: objc_setAssociatedObject
    0x1927b6838 <+64>:  cbz     xO, 0x1927b6844         ; <+76>
    0x1927b683c <+68>:  str     xzr, [x19]
    0x1927b6840 <+72>:  b       0x1927b6850             ; <+88>
    0x1927b6844 <+76>:  mov     x0, x20
    0x1927b6848 <+80>:  bl      0x18fee6df8
    0x1927b684c <+84>:  str     x0, [x19]
    0x1927b6850 <+88>:  strb    wzr, [x19, #0x8]
    0x1927b6854 <+92>:  ldp     x29, x30, [sp, #0x10]
    0x1927b6858 <+96>:  ldp     x20, x19, [sp], #0x20
    0x1927b685c <+100>: retab
  1. 第3-5行:操作栈指针
  2. 第6行:保存新值(x2)到x20中(GetTraitCollectionTSD 调用时,x19、x20寄存器会被保存到栈上,返回时从栈上恢复到x19、x20,所以 x20 有存储效果。Objective-C中的实例方法调用中,对象的指针 (self)通常存储在x19寄存器中)
  3. 第7-9行:调用 GetTraitCollectionTSD 函数,GetTraitCollectionTSD 会返回一个指向 UITraitCollection 对象的指针,将对象加载到 X0 中,将对象的指针保存到✕19 中
  4. 第10-11行:比较 新值 和 GetTraitCollectionTSD 返回的对象是否一样,如果一样的话,跳转第25行
  5. 第12行:执行 objc_release 方法,释放 GetTraitCollectionTSD 返回的对象
  6. 第13-17行:新值为空时跳到19行;新值不为空时调用 objc_setAssociatedObject 方法,取出新值关联对象 _UITraitCollectionlsFallbackKey
  7. 第18行:关联对象为空时,跳转21行
  8. 第19-20行:清空 GetTraitCollectionTSD 返回指针的内容,跳转到24行
  9. 第21-23行:retain UITraitCollection 新值,让 GetTraitCollectionTSD 返回的指针指向新值地址
  10. 第24行:清除 GetTraitCollectionTSD 返回指针的低位内容
  11. 第25-27行:操作栈指针

crash发生在第12行,释放 GetTraitCollectionTSD 返回的对象,而第21-23行则把入参赋值给 GetTraitCollectionTSD 返回的指针,所以这是一个普通的set方法,在释放 GetTraitCollectionTSD 中取出的老 UITraitCollection 时出现了野指针问题。

问题结论:多线程操作 UITraitCollection 对象的引用计数导致引用计数不准,对象被提前释放,最终造成野指针问题。

修复

看一下 UITraitCollection 的属性:

/// 枚举,用来桥识用户界面是 Phone/Pad/TV/Carplay/Mac
@property (nonatomic, readonly) UIUserInterfaceIdiom userInterfaceIdiom; // unspecified: UIUserInterfaceIdiomUnspecified

/// 枚举,用来标识黑暗模式
@property (nonatomic, readonly) UIUserInterfaceStyle userInterfaceStyle API_AVAILABLE(tvos(10.0)) API_AVAILABLE(ios(12.0)) API_UNAVAILABLE(watchos); // unspecified: UIUserInterfaceStyleUnspecified

/// 枚举,用来标识布局方向:LTR/RTL,阿拉伯文是从左到右
@property (nonatomic, readonly) UITraitEnvironmentLayoutDirection layoutDirection API_AVAILABLE(ios(10.0)); // unspecified: UITraitEnvironmentLayoutDirectionUnspecified

/// 内部缩放比例
@property (nonatomic, readonly) CGFloat displayScale; // unspecified: 0.0

/// 枚举,屏幕尺寸等级,布局用,iPhone有紧凑和常规两种,iPad都是常规
@property (nonatomic, readonly) UIUserInterfaceSizeClass horizontalSizeClass; // unspecified: UIUserInterfaceSizeClassUnspecified
@property (nonatomic, readonly) UIUserInterfaceSizeClass verticalSizeClass; // unspecified: UIUserInterfaceSizeClassUnspecified
...

可以看出 UITraitCollection 跟名字一样,是一个大杂烩,包含了影响全局的 设备、布局、U样式 信息。不过从crash堆栈 drawLaver:inContext: ->_setCurrentTraitCollection:看,crash时在进行 MylmageBrowserTiledView 的绘制操作,绘制操作并没有用到 UITraitCollection 的这些信息。如果能避免 _setCurrentTraitCollection: 方法调用,就可以修复这个crash。 从 _setCurrentTraitCollection: 调用方drawLayer:inContext:找找线索。先算出 drawLayer:inContext: 中的crash位置:

0x186f04 + 0x192626000 = 0×1927acf04

触发断点后,截取关键汇编代码如下(此处汇编代码是使用hopper补录的,不过逻辑一致):

bl         +[UITraitCollection _currentTraitCollectionIfExists] ; +[UITraitCollection _currentTraitCollectionIfExists], Begin of try block (catch block at 0x182954f64)
mov        fp, fp
bl         0x180918950                                 ; End of try block started at 0x182954e6c, Begin of try block
mov        x21, x0
adrp       x8, #0x1ca6c7000
add        x1, x8, #0x4c0
mov        x0, x20                                     ; End of try block started at 0x182954e74, Begin of try block (catch block at 0x182954f64)
bl         0x18008edb8
mov        fp, fp
bl         0x180918950                                 ; End of try block started at 0x182954e84, Begin of try block
mov        x23, x0
ldr        x0, [x24, #0xf90]                           ; objc_cls_ref_UITraitCollection
adrp       x8, #0x1cade6000
add        x22, x8, #0xea4
mov        x1, x22                                     ; End of try block started at 0x182954e90, Begin of try block (catch block at 0x182954f64)
mov        x2, x23
bl         0x18008edb8
mov        x0, x23                                     ; End of try block started at 0x182954ea4, Begin of try block
bl         0x18008ede8
mov        x0, x20                                     ; End of try block started at 0x182954eb0, Begin of try block (catch block at 0x182954f68), argument #1 for method __UISetCurrentFallbackEnvironment
bl         __UISetCurrentFallbackEnvironment           ; __UISetCurrentFallbackEnvironment
mov        fp, fp
bl         0x180918a00                                 ; End of try block started at 0x182954eb8, Begin of try block
mov        x23, x0
adrp       x8, #0x1ca28c000
add        x1, x8, #0x340
mov        x0, x20                                     ; End of try block started at 0x182954ec4, Begin of try block (catch block at 0x182954f68)
mov        v0, v8
mov        v1, v9
mov        v2, v10
mov        v3, v11
bl         0x18008edb8
mov        x0, x23                                     ; argument #1 for method __UIRestorePreviousFallbackEnvironment
bl         __UIRestorePreviousFallbackEnvironment      ; __UIRestorePreviousFallbackEnvironment
ldr        x0, [x24, #0xf90]                           ; objc_cls_ref_UITraitCollection
mov        x1, x22
mov        x2, x21
bl         0x18008edb8
  1. 第1-4行:使用 +[UITraitCollection _currentTraitCollectionIfExists] 获取当前的 UITraitCollection 对象 并存在 x21 寄存器中
  2. 第5-11行:调用 MylmageBrowserTiledView.traitCollection 方法,获得 traitCollection 对象并存在x23寄存器中
  3. 第12-17行:调用+[UITraitCollection _setCurrentTraitCollection]类方法,并把第二步获取到的 tiledView.traitCollection 作为参数传入
  4. 第18-19行:release 第二步获取到的 traitCollection
  5. 第20-21行:执行 UISetCurrentFallbackEnvironment 方法,参数为 MylmageBrowserTiledView
  6. 第22-32行:执行 MylmageBrowserTiledView 的 drawRect: 方法
  7. 第33-34行:执行_UIRestorePreviousFallbackEnvironment 方法,参数为MylmageBrowserTiledView
  8. 第35-38行:调用 +[UITraitCollection _setCurrentTraitCollection:]类方法,把第一步获取到的值赋回去

看起来前后这些铺垫都是为了执行 drawRect: 方法,所以只要我们需要 drawRect:方法,就没办法避免 _setCurrentTraitCollection 操作。 不过我们可以覆写 MylmageBrowserTiledView.traitCollection _setCurrentTraitCollection 入参为空。在 MylmageBrowserTiledView.traitCollection 方法里加入线程判断,如果非主线程,返回nil,避免子线程操作 traitCollection:

- (UITraitCollection *)traitCollection {
    if (![NSThread isMainThread]) return nil;
    return [super traitCollection];
}

总结&沉淀

通过上面两个案例,可以总结出一个定位野指针对象的方法:

  1. 找到正确版本的库,运行调试
  2. 根据crash堆栈中的偏移量+真机运行时对应库的基地址,计算出崩溃位置,并断点
  3. 尝试触发断点,打印当前命令所操作的对象,这个对象就是野指针对象

如果无法真机调试,借助 IDA/Hopper 工具,也可以看出一些端倪,不过这种方法需要阅读更多的汇编代码才能定位:

  1. 找到正确版本的二进制产物,使用IDA/Hopper打开
  2. 根据crash堆栈中的崩溃偏移量+IDA/Hopper 中库的基地址,定位到崩溃的汇编代码
  3. 结合汇编代码上下文,推断当前操作的对象

汇编调试不仅可以在没有源码的场景下定位问题,也可以用来给有源码的崩溃提供更细致的信息。想象这样一个场景,一个controller中有两个属性blockA,blockB,其中blockA会调用blockB:

__strong typeof (self) weakSelf = self;
self.blockA = ^{
    // do some thing
    //回调
    weakSelf.blockB();
};

如果线上出现了weakSelf.blockB() 这行的野指针崩溃,如何确定野指针对象是 weakSelf 还是 blockB 呢?我们可以用反汇编工具IDA/Hopper查看app的二进制产物,然后根据偏移量定位到汇编代码,根据汇编代码上下文,可以确定这里操作的是哪个对象。

附录

MylmageBrowserTiledView 内大致逻辑如下:

@implementation MylmageBrowserTiledView

+ (Class)layerClass {
    return [CATiledLayer class];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context {
		// 自定义的绘制逻辑
}
@end