前言
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得知,崩溃发生时,QuartzCore基地址为0x18f25e000
- 由行1得知,崩溃位于QuartzCore基地址偏移375292 byte的地方
- 由系统版本,得知QuartzCore版本为iOS13.5.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业务场景,触发断点:
至此我们已经定位到了发生问题的汇编代码,下面尝试调试复现堆栈、找到野指针对象。 我们断点的地址是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:]中:
同样,这里使用 step in 调试,遇到bl、blr 使用step over,在drawlnContext: 最后一行走到了objc_release:
step in后出现和bugly相同的堆栈:
在 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::
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...
根据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
- 第3-5行:操作栈指针
- 第6行:保存新值(x2)到x20中(GetTraitCollectionTSD 调用时,x19、x20寄存器会被保存到栈上,返回时从栈上恢复到x19、x20,所以 x20 有存储效果。Objective-C中的实例方法调用中,对象的指针 (self)通常存储在x19寄存器中)
- 第7-9行:调用 GetTraitCollectionTSD 函数,GetTraitCollectionTSD 会返回一个指向 UITraitCollection 对象的指针,将对象加载到 X0 中,将对象的指针保存到✕19 中
- 第10-11行:比较 新值 和 GetTraitCollectionTSD 返回的对象是否一样,如果一样的话,跳转第25行
- 第12行:执行 objc_release 方法,释放 GetTraitCollectionTSD 返回的对象
- 第13-17行:新值为空时跳到19行;新值不为空时调用 objc_setAssociatedObject 方法,取出新值关联对象 _UITraitCollectionlsFallbackKey
- 第18行:关联对象为空时,跳转21行
- 第19-20行:清空 GetTraitCollectionTSD 返回指针的内容,跳转到24行
- 第21-23行:retain UITraitCollection 新值,让 GetTraitCollectionTSD 返回的指针指向新值地址
- 第24行:清除 GetTraitCollectionTSD 返回指针的低位内容
- 第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-4行:使用 +[UITraitCollection _currentTraitCollectionIfExists] 获取当前的 UITraitCollection 对象 并存在 x21 寄存器中
- 第5-11行:调用 MylmageBrowserTiledView.traitCollection 方法,获得 traitCollection 对象并存在x23寄存器中
- 第12-17行:调用+[UITraitCollection _setCurrentTraitCollection]类方法,并把第二步获取到的 tiledView.traitCollection 作为参数传入
- 第18-19行:release 第二步获取到的 traitCollection
- 第20-21行:执行 UISetCurrentFallbackEnvironment 方法,参数为 MylmageBrowserTiledView
- 第22-32行:执行 MylmageBrowserTiledView 的 drawRect: 方法
- 第33-34行:执行_UIRestorePreviousFallbackEnvironment 方法,参数为MylmageBrowserTiledView
- 第35-38行:调用 +[UITraitCollection _setCurrentTraitCollection:]类方法,把第一步获取到的值赋回去
看起来前后这些铺垫都是为了执行 drawRect: 方法,所以只要我们需要 drawRect:方法,就没办法避免 _setCurrentTraitCollection 操作。 不过我们可以覆写 MylmageBrowserTiledView.traitCollection _setCurrentTraitCollection 入参为空。在 MylmageBrowserTiledView.traitCollection 方法里加入线程判断,如果非主线程,返回nil,避免子线程操作 traitCollection:
- (UITraitCollection *)traitCollection {
if (![NSThread isMainThread]) return nil;
return [super traitCollection];
}
总结&沉淀
通过上面两个案例,可以总结出一个定位野指针对象的方法:
- 找到正确版本的库,运行调试
- 根据crash堆栈中的偏移量+真机运行时对应库的基地址,计算出崩溃位置,并断点
- 尝试触发断点,打印当前命令所操作的对象,这个对象就是野指针对象
如果无法真机调试,借助 IDA/Hopper 工具,也可以看出一些端倪,不过这种方法需要阅读更多的汇编代码才能定位:
- 找到正确版本的二进制产物,使用IDA/Hopper打开
- 根据crash堆栈中的崩溃偏移量+IDA/Hopper 中库的基地址,定位到崩溃的汇编代码
- 结合汇编代码上下文,推断当前操作的对象
汇编调试不仅可以在没有源码的场景下定位问题,也可以用来给有源码的崩溃提供更细致的信息。想象这样一个场景,一个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