本文会通过对 NSThread
的原理进行分析,对 iOS 15 开始出现的 [_NSThreadPerformInfo dealloc]
相关崩溃进行定位,并提供相应的解决方案
一、背景
从 iOS 15.0 Beta5 开始,集成开源库 GCDAsyncSocket
的 APP 开始出现 -[_NSThreadPerformInfo dealloc]
相关的崩溃
Crash on iOS 15.0 Beta5 [GCDAsyncSocket cfstreamThread] (GCDAsyncSocket.m:7596) · Issue #775 · robbiehanson/CocoaAsyncSocket · GitHub[1]
代码语言:javascript复制Thread 32 name: GCDAsyncSocket-CFStream
Thread 32 Crashed:
0 libobjc.A.dylib 0x19b483c50 objc_release 16
1 Foundation 0x184161344 -[_NSThreadPerformInfo dealloc] 56
2 Foundation 0x1842d08dc __NSThreadPerform 160
3 CoreFoundation 0x1829b069c CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION 28
4 CoreFoundation 0x1829c12f0 __CFRunLoopDoSource0 208
5 CoreFoundation 0x1828fabf8 __CFRunLoopDoSources0 268
6 CoreFoundation 0x182900404 __CFRunLoopRun 820
7 CoreFoundation 0x182913fc8 CFRunLoopRunSpecific 600
8 Foundation 0x184134104 -[NSRunLoop 102660 (NSRunLoop) runMode:beforeDate:] 236
9 MyApp 0x104990290 0x1026b0000 36569744
10 Foundation 0x184183950 NSThread__start 764
11 libsystem_pthread.dylib 0x1f2745a60 _pthread_start 148
12 libsystem_pthread.dylib 0x1f2744f5c thread_start 8
因为该堆栈的崩溃发生在 系统库,导致很多 APP 都难以解决此类问题
二、GCDAsyncSocket
简介
GCDAsyncSocket
是一个 TCP 库。它建在 Grand Central Dispatch 之上。
通常情况下,我们可以通过下面的代码创建一个 GCDAsyncSocket
的实例
socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
并通过 connect...
方法建立一个连接
NSError *err = nil;
if (![socket connectToHost:@"deusty.com" onPort:80 error:&err]) // Asynchronous!
{
// If there was an error, it's likely something like "already connected" or "no delegate set"
NSLog(@"I goofed: %@", err);
}
GCDAsyncSocket
建立连接
GCDAsyncSocket
内部会先执行域名解析[2]
int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0);
随后,通过ip
创建 socket[3] 和配置合适的参数
int socketFD = socket(family, SOCK_STREAM, 0);
...
int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]);
...
int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]);
当 socket 创建后,会创建对应 stream
并通过 CFReadStreamSetClient
函数设置 client
最后通过 CFReadStreamScheduleWithRunLoop
注册回调任务的 runloop
当关闭连接时,我们需要通过 CFReadStreamScheduleWithRunLoop
反注册
After scheduling
stream
with a run loop, its client (set with CFReadStreamSetClient) is notified when various events happen with the stream, such as when it finishes opening, when it has bytes available, and when an error occurs. A stream can be scheduled with multiple run loops and run loop modes. Use CFReadStreamUnscheduleFromRunLoop to later removestream
from the run loop.
GCDAsyncSocket
通过 unscheduleCFStreams:
函数实现反注册
(void)unscheduleCFStreams:(GCDAsyncSocket *)asyncSocket
{
LogTrace();
NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread");
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
if (asyncSocket->readStream)
CFReadStreamUnscheduleFromRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode);
if (asyncSocket->writeStream)
CFWriteStreamUnscheduleFromRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode);
}
当GCDAsyncSocket
的实例被释放时,会通过下面的代码[4]将让 类GCDAsyncSocket
在 cfstreamThread
线程执行 (void)unscheduleCFStreams:(GCDAsyncSocket *)asyncSocket{}
方法
代码语言:javascript复制⚠️ 注意:
withObject
参数是GCDAsyncSocket
的实例waitUntilDone
参数的值是YES
,表示会阻塞当前线程
[[self class] performSelector:@selector(unscheduleCFStreams:)
onThread:cfstreamThread
withObject:self
waitUntilDone:YES];
三、 跨线程执行任务
现在,我们重点看一下系统库是如何实现 performSelector:onThread:
方法的。
通过前面的分析,我们可以注意到,系统库必须完成以下两个任务:
1、在另外的线程执行代码
2、阻塞当前线程,直到另一个线程执行完毕时恢复执行
本段内容是建立在
iOS 12.4.6 (16G183)
系统版本上面进行分析
ReleadeTrack
为了方便对 GCDAsyncSocket
的引用计数进行追踪,我创建了一个子类 ReleadeTrack
,读者可以将本文中出现 ReleadeTrack
的地方理解为 GCDAsyncSocket
@interface ReleadeTrack : GCDAsyncSocket
@end
_NSThreadPerformInfo
当 performSelector:onThread:...
方法执行时,系统库会创建一个私有类 _NSThreadPerformInfo
的实例
_NSThreadPerformInfo
的实例会持有消息相关的信息
代码语言:javascript复制⚠️ 注意:
_NSThreadPerformInfo
通过argument
持有了开发者传入的参数<ReleadeTrack: 0x10160f0a0>
通过waiter
持有了NSCondition
的实例
(lldb) ivars 0x1015b4c90
<_NSThreadPerformInfo: 0x1015b4c90>:
in _NSThreadPerformInfo:
target (id): <ReleadeTrack: 0x100cabee0>
selector (SEL): unscheduleCFStreams:
argument (id): <ReleadeTrack: 0x10160f0a0>
modes (NSMutableArray*): <__NSArrayM: 0x1015b4b40>
waiter (NSCondition*): <NSCondition: 0x1015b3c70>
signalled (char*): Value not representable, *
in NSObject:
isa (Class): _NSThreadPerformInfo (isa, 0x21a21933b3d1)
(lldb)
并通过调用 -[NSThread _nq:]
存到 NSThread
的私有属性 _private
(lldb) po $x0
<NSThread: 0x100f1f9a0>{number = 2, name = GCDAsyncSocket-CFStream}
(lldb) po $x2
<_NSThreadPerformInfo: 0x100f058e0>
(lldb) ivars 0x100f1f9a0
<NSThread: 0x100f1f9a0>:
in NSThread:
_private (id): <_NSThreadData: 0x100f318a0>
_bytes (unsigned char[44]): Value not representable, [44C]
in NSObject:
isa (Class): NSThread (isa, 0x41a21933b471)
(lldb) ivars 0x100f318a0
<_NSThreadData: 0x100f318a0>:
in _NSThreadData:
dict (id): <__NSDictionaryM: 0x100f30130>
name (id): @"GCDAsyncSocket-CFStream"
target (id): <ReleadeTrack: 0x100707f58>
selector (SEL): cfstreamThread:
argument (id): nil
seqNum (int): 2
qstate (unsigned char): Value not representable, C
qos (char): 0
cancel (unsigned char): Value not representable, C
status (unsigned char): Value not representable, C
performQ (id): nil
performD (NSMutableDictionary*): nil
attr (struct _opaque_pthread_attr_t): {
__sig (long): 1414022209
__opaque (char[56]): Value not representable, [56c]
}
tid (struct _opaque_pthread_t*): 0x100f31928 -> 0x16fa93000
pri (double): 0.5
defpri (double): 0.5
in NSObject:
isa (Class): _NSThreadData (isa, 0x1a21933b449)
(lldb)
该步执行完毕后,NSThread
的私有属性 _private
会指向 _NSThreadData
的实例,_NSThreadData
的私有属性 performQ
会保存 <_NSThreadPerformInfo: 0x100f058e0>
同时,-[NSThread _nq:]
方法会创建 CFRunloopSource
的实例并注册到 GCDAsyncSocket-CFStream
线程
NSCondition
因为 GCDAsyncSocket
销毁时,会将 waitUntilDone:YES
当做参数传入,所以,NSCondition
的实例会被创建并用于阻塞当前线程
NSCondition
的实例初始化时,会分别初始化pthread_mutex_t
和 pthread_cond_init
Foundation`-[NSCondition init]:
0x1e088accc < 0>: sub sp, sp, #0x30 ; =0x30
0x1e088acd0 < 4>: stp x20, x19, [sp, #0x10]
0x1e088acd4 < 8>: stp x29, x30, [sp, #0x20]
0x1e088acd8 < 12>: add x29, sp, #0x20 ; =0x20
0x1e088acdc < 16>: mov x19, x0
0x1e088ace0 < 20>: bl 0x1df0f6370 ; object_getIndexedIvars
0x1e088ace4 < 24>: mov x20, x0
0x1e088ace8 < 28>: adrp x1, 232117
0x1e088acec < 32>: add x1, x1, #0x868 ; =0x868
-> 0x1e088acf0 < 36>: bl 0x1dfb3d1d0 ; symbol stub for: pthread_mutex_init
0x1e088acf4 < 40>: cbz w0, 0x1e088ad1c ; < 80>
0x1e088acf8 < 44>: adrp x8, 223297
0x1e088acfc < 48>: ldr x8, [x8, #0x220]
0x1e088ad00 < 52>: stp x19, x8, [sp]
0x1e088ad04 < 56>: adrp x8, 180729
0x1e088ad08 < 60>: add x1, x8, #0x417 ; =0x417
0x1e088ad0c < 64>: mov x0, sp
0x1e088ad10 < 68>: bl 0x1df10b720 ; objc_msgSendSuper2
0x1e088ad14 < 72>: mov x19, #0x0
0x1e088ad18 < 76>: b 0x1e088ad2c ; < 96>
0x1e088ad1c < 80>: add x0, x20, #0x40 ; =0x40
0x1e088ad20 < 84>: mov x1, #0x0
0x1e088ad24 < 88>: bl 0x1dfb3d11c ; symbol stub for: pthread_cond_init
0x1e088ad28 < 92>: str xzr, [x20, #0x70]
0x1e088ad2c < 96>: mov x0, x19
0x1e088ad30 < 100>: ldp x29, x30, [sp, #0x20]
0x1e088ad34 < 104>: ldp x20, x19, [sp, #0x10]
0x1e088ad38 < 108>: add sp, sp, #0x30 ; =0x30
0x1e088ad3c < 112>: ret
随后,通过-[NSCondition lock]
方法间接调用 pthread_mutex_lock
加锁
Foundation`-[NSCondition lock]:
-> 0x1e088ae1c < 0>: stp x29, x30, [sp, #-0x10]!
0x1e088ae20 < 4>: mov x29, sp
0x1e088ae24 < 8>: bl 0x1df0f6370 ; object_getIndexedIvars
0x1e088ae28 < 12>: ldp x29, x30, [sp], #0x10
0x1e088ae2c < 16>: b 0x1e0acb720 ; symbol stub for: pthread_mutex_lock
然后,通过 -[NSCondition wait]
方法间接调用 pthread_cond_wait
函数,阻塞当前线程
-[NSCondition wait]
方法内部调用 pthread_cond_wait
函数实现阻塞
Foundation`-[NSCondition wait]:
0x1e08f249c < 0>: stp x29, x30, [sp, #-0x10]!
0x1e08f24a0 < 4>: mov x29, sp
0x1e08f24a4 < 8>: bl 0x1df0f6370 ; object_getIndexedIvars
0x1e08f24a8 < 12>: mov x1, x0
0x1e08f24ac < 16>: add x0, x0, #0x40 ; =0x40
0x1e08f24b0 < 20>: ldp x29, x30, [sp], #0x10
-> 0x1e08f24b4 < 24>: b 0x1dfb2ee7c ; pthread_cond_wait
最后,通过 svc
调用阻塞当前线程
GCDAsyncSocket-CFStream
线程
GCDAsyncSocket-CFStream
线程通过 runloop 机制和前面创建的CFRunLoopSource
回调给 __NSThreadPerformPerform
函数
随后,__NSThreadPerformPerform
函数通过performQueueDequeue
查找可以被执行的_NSThreadPerformInfo
代码语言:javascript复制
performQueueDequeue
存在的原因是部分_NSThreadPerformInfo
通过下面的方法指定了mode
- (void)performSelector:(SEL)aSelector
onThread:(NSThread *)thr
withObject:(nullable id)arg
waitUntilDone:(BOOL)wait
modes:(nullable NSArray<NSString *> *)array
API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
找到合适的任务后, __NSThreadPerformPerform
函数会通过调用 performSelector:withObject:
完成指定的任务
现在,我们通过在 [GCDAsyncSocket scheduleCFStreams:]
添加断点并打印一下堆栈
(lldb) bt
* thread #4, name = 'GCDAsyncSocket-CFStream', stop reason = breakpoint 16.1
* frame #0: 0x0000000101111828 CocoaAsyncSocket` [GCDAsyncSocket scheduleCFStreams:](self=ReleadeTrack, _cmd="scheduleCFStreams:", asyncSocket=0x0000000101b0e0b0) at GCDAsyncSocket.m:7700:2
frame #1: 0x00000001e09a2690 Foundation`__NSThreadPerformPerform 336
frame #2: 0x00000001dfeacf1c CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ 24
frame #3: 0x00000001dfeace9c CoreFoundation`__CFRunLoopDoSource0 88
frame #4: 0x00000001dfeac784 CoreFoundation`__CFRunLoopDoSources0 176
frame #5: 0x00000001dfea76c0 CoreFoundation`__CFRunLoopRun 1004
frame #6: 0x00000001dfea6fb4 CoreFoundation`CFRunLoopRunSpecific 436
frame #7: 0x00000001e087595c Foundation`-[NSRunLoop(NSRunLoop) runMode:beforeDate:] 300
frame #8: 0x0000000101111780 CocoaAsyncSocket` [GCDAsyncSocket cfstreamThread:](self=ReleadeTrack, _cmd="cfstreamThread:", unused=0x0000000000000000) at GCDAsyncSocket.m:7689:25
frame #9: 0x00000001e09a24a0 Foundation`__NSThread__start__ 984
frame #10: 0x00000001dfb392c0 libsystem_pthread.dylib`_pthread_body 128
frame #11: 0x00000001dfb39220 libsystem_pthread.dylib`_pthread_start 44
frame #12: 0x00000001dfb3ccdc libsystem_pthread.dylib`thread_start 4
(lldb)
通过堆栈,可以看到 __NSThreadPerformPerform
调用了 [GCDAsyncSocket scheduleCFStreams:]
清理 _NSThreadPerformInfo
GCDAsyncSocket-CFStream
线程执行任务结束后,会通过通过release
和 str xzr, [x25, x8]
指令销毁 _NSThreadPerformInfo
存储的各种数据
通过反汇编工具,可以反解到以下代码:
代码语言:javascript复制 r25->target = 0x0;
r25->argument = 0x0;
r25->modes = 0x0;
恢复阻塞的线程
随后,__NSThreadPerformPerform
函数会先调用 -[NSCondition lock]
加锁,并通过-[NSCondition signal]
间接调用 pthread_cond_signal
函数的方式恢复另外一个被阻塞的线程
-[NSCondition signal]
方法通过 pthread_cond_signal
函数通知
Foundation`-[NSCondition signal]:
-> 0x1e08f7dc4 < 0>: stp x29, x30, [sp, #-0x10]!
0x1e08f7dc8 < 4>: mov x29, sp
0x1e08f7dcc < 8>: bl 0x1df0f6370 ; object_getIndexedIvars
0x1e08f7dd0 < 12>: add x0, x0, #0x40 ; =0x40
0x1e08f7dd4 < 16>: ldp x29, x30, [sp], #0x10
0x1e08f7dd8 < 20>: b 0x1dfb3d128 ; symbol stub for: pthread_cond_signal
四、iOS 15.x 新版本的跨线程执行任务
从某个版本开始,苹果对 _NSThreadPerformInfo
相关的设计进行了调整。下面以 iOS 15.2 (19C57)
为例进行分析
_NSThreadPerformInfo
因为 arm64e
架构的原因,_NSThreadPerformInfo
新增了一个 _pac_signature
属性
(lldb) ivars $x0
<_NSThreadPerformInfo: 0x107824030>:
in _NSThreadPerformInfo:
_target (id): <ReleadeTrack: 0x105114018>
_selector (SEL): scheduleCFStreams:
_argument (id): <ReleadeTrack: 0x1079074d0>
_pac_signature (AQ): Value not representable, AQ
_modes (NSArray*): <__NSSingleObjectArrayI: 0x107823f10>
_waiter (NSCondition*): <NSCondition: 0x107825010>
_state (int): 2
in NSObject:
isa (Class): _NSThreadPerformInfo (isa, 0x1dae17a51)
(lldb)
同时,_NSThreadPerformInfo
现在有 3 个实例方法
22: regex = '_NSThreadPerformInfo', locations = 3, resolved = 3, hit count = 0
22.1: where = Foundation`-[_NSThreadPerformInfo dealloc], address = 0x0000000182670a1c, resolved, hit count = 0
22.2: where = Foundation`-[_NSThreadPerformInfo signal:], address = 0x00000001827dff68, resolved, hit count = 0
22.3: where = Foundation`-[_NSThreadPerformInfo wait], address = 0x00000001827dffcc, resolved, hit count = 0
-[_NSThreadPerformInfo wait]
当需要阻塞当前线程时,会通过-[_NSThreadPerformInfo wait]
间接调用 -[NSCondition lock]
和 -[NSCondition wait]
-[NSCondition unlock]
实现
-[_NSThreadPerformInfo signal:]
当需要恢复被阻塞的线程时,会通过-[_NSThreadPerformInfo signal:]
间接调用-[NSCondition lock]
和 -[NSCondition signal]
实现
-[_NSThreadPerformInfo dealloc]
另外一个重要改变是由-[_NSThreadPerformInfo dealloc]
负责释放各种属性,比如_argument
就是由下面的代码触发释放的
_NSThreadPerformInfo
生命周期分析
现在,我们先看看 _NSThreadPerformInfo
的引用计数变化情况
正常情况:
非正常情况:
我们可以注意到,系统库销毁 _NSThreadPerformInfo
的时机存在两种情况:
1、触发performSelector:onThread:
的线程销毁
2、GCDAsyncSocket-CFStream
线程销毁
对于第二种情况,我们结合两个线程的执行顺序梳理后如下:
代码语言:javascript复制A 代表触发
performSelector:onThread:
的线程 B 代表GCDAsyncSocket-CFStream
线程
经过前面的分析,我们可以发现当 A 线程 通过free
释放GCDAsyncSocket
实例的内存所有权后,
GCDAsyncSocket-CFStream
线程仍然会通过_NSThreadPerformInfo
持有悬垂指针,并通过 objc_release
减少引用计数
五、objc 内存管理机制
为了更好的理解崩溃堆栈,我们需要简单的回顾一下objc
的内存管理机制
示例代码
代码语言:javascript复制 Arc *obj = [Arc new];
在 ARC 环境下,上面的代码会变成以下的汇编代码:
代码语言:javascript复制tip: xor esi, esi 指令是通过异或操作将 esi 寄存器清零
0x100003a00 < 0>: push rbp
0x100003a01 < 1>: mov rbp, rsp
0x100003a04 < 4>: sub rsp, 0x20
0x100003a08 < 8>: mov qword ptr [rbp - 0x18], rdi
0x100003a0c < 12>: mov qword ptr [rbp - 0x10], rsi
0x100003a10 < 16>: mov rdi, qword ptr [rip 0x49d1] ; (void *)0x0000000100008408: Arc
0x100003a17 < 23>: call 0x100003d1a ; symbol stub for: objc_opt_new
0x100003a1c < 28>: mov qword ptr [rbp - 0x8], rax
# $rbp-0x8 内存位置存储 obj 的地址
0x100003a20 < 32>: lea rdi, [rbp - 0x8]
# 通过 lea 让 $rdi 寄存器 存储 $rbp-0x8
0x100003a24 < 36>: xor esi, esi
# 通过 xor 指令是通过异或操作将 esi 寄存器清零
0x100003a26 < 38>: call 0x100003d32 ; symbol stub for: objc_storeStrong
# 调用 objc_storeStrong 函数将 实例的地址 和 nil 当做参数传入
-> 0x100003a2b < 43>: add rsp, 0x20
0x100003a2f < 47>: pop rbp
0x100003a30 < 48>: ret
编译器通过添加 objc_storeStrong()
函数将对象进行销毁
objc_storeStrong
objc_storeStrong
的实现逻辑如下
void
objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
代码语言:javascript复制 rdi = 0x00007ffeefbf8868
rsi = 0x0000000000000000
因为传入的第二个参数是 nil,所以,该步操作后,obj 指向 nil,确保 obj 不会出现悬垂指针。
tip:该设计可以保证 arc 下,可以有效的减少悬垂指针问题
等价于 mrc 代码:
代码语言:javascript复制id prev = obj;
obj = nil;
objc_release(prev);
objc_release
代码语言:javascript复制__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (obj->isTaggedPointerOrNil()) return;
return obj->release();
}
objc_release
内部的逻辑是先判断被降低引用计数的对象是否属于 tagged-pointer 或者 nil,如果是可以避免后续的处理
objc_object::release()
随后,开始通过 objc_object::release()
进行再次转发
/// A pointer to an instance of a class.
typedef struct objc_object *id;
// Equivalent to calling [this release], with shortcuts if there is no override
inline void
objc_object::release()
{
ASSERT(!isTaggedPointer());
rootRelease(true, RRVariant::FastOrMsgSend);
}
objc_object::rootRelease
objc_object::rootRelease
主要进行以下任务:
1、通过 getDecodedClass
获取 class
2、通过 hasCustomRR
判断该类是否存在自定义的 release
方法,如果存在,则通过objc_msgSend
转发
3、执行引用计数减一的操作
4、随后判断是否需要执行 dealloc 的流程
5、如果需要,会通过开始执行 dealloc 方法
代码语言:javascript复制ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
if (slowpath(isTaggedPointer())) return false;
bool sideTableLocked = false;
isa_t newisa, oldisa;
oldisa = LoadExclusive(&isa.bits);
if (variant == RRVariant::FastOrMsgSend) {
// These checks are only meaningful for objc_release()
// They are here so that we avoid a re-load of the isa.
if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
ClearExclusive(&isa.bits);
if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
swiftRelease.load(memory_order_relaxed)((id)this);
return true;
}
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
return true;
}
}
if (slowpath(!oldisa.nonpointer)) {
// a Class is a Class forever, so we can perform this checkonce
// outside of the CAS loop
if (oldisa.getDecodedClass(false)->isMetaClass()) {
ClearExclusive(&isa.bits);
return false;
}
}
retry:
do {
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
return sidetable_release(sideTableLocked, performDealloc);
}
if (slowpath(newisa.isDeallocating())) {
ClearExclusive(&isa.bits);
if (sideTableLocked) {
ASSERT(variant == RRVariant::Full);
sidetable_unlock();
}
return false;
}
// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
if (slowpath(carry)) {
// don't ClearExclusive()
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
if (slowpath(newisa.isDeallocating()))
goto deallocate;
if (variant == RRVariant::Full) {
if (slowpath(sideTableLocked)) sidetable_unlock();
} else {
ASSERT(!sideTableLocked);
}
return false;
underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate
// abandon newisa to undo the decrement
newisa = oldisa;
if (slowpath(newisa.has_sidetable_rc)) {
if (variant != RRVariant::Full) {
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}
// Transfer retain count from side table to inline storage.
if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
// Need to start over to avoid a race against
// the nonpointer -> raw pointer transition.
oldisa = LoadExclusive(&isa.bits);
goto retry;
}
// Try to remove some retain counts from the side table.
auto borrow = sidetable_subExtraRC_nolock(RC_HALF);
bool emptySideTable = borrow.remaining == 0; // we'll clear the side table if no refcounts remain there
if (borrow.borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
bool didTransitionToDeallocating = false;
newisa.extra_rc = borrow.borrowed - 1; // redo the original decrement too
newisa.has_sidetable_rc = !emptySideTable;
bool stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
if (!stored && oldisa.nonpointer) {
// Inline update failed.
// Try it again right now. This prevents livelock on LL/SC
// architectures where the side table access itself may have
// dropped the reservation.
uintptr_t overflow;
newisa.bits =
addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow);
newisa.has_sidetable_rc = !emptySideTable;
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
if (stored) {
didTransitionToDeallocating = newisa.isDeallocating();
}
}
}
if (!stored) {
// Inline update failed.
// Put the retains back in the side table.
ClearExclusive(&isa.bits);
sidetable_addExtraRC_nolock(borrow.borrowed);
oldisa = LoadExclusive(&isa.bits);
goto retry;
}
// Decrement successful after borrowing from side table.
if (emptySideTable)
sidetable_clearExtraRC_nolock();
if (!didTransitionToDeallocating) {
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
}
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}
deallocate:
// Really deallocate.
ASSERT(newisa.isDeallocating());
ASSERT(isa.isDeallocating());
if (slowpath(sideTableLocked)) sidetable_unlock();
__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
关键代码整理后如下:
if (variant == RRVariant::FastOrMsgSend) {
// These checks are only meaningful for objc_release()
// They are here so that we avoid a re-load of the isa.
if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
ClearExclusive(&isa.bits);
if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
swiftRelease.load(memory_order_relaxed)((id)this);
return true;
}
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
return true;
}
}
...
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
...
if (slowpath(newisa.isDeallocating()))
goto deallocate;
...
deallocate:
// Really deallocate.
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
代码语言:javascript复制 bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}
getDecodedClass
现在,我们重点看一下第 1 步getDecodedClass
的实现代码:
inline Class
isa_t::getDecodedClass(bool authenticated) {
#if SUPPORT_INDEXED_ISA
if (nonpointer) {
return classForIndex(indexcls);
}
return (Class)cls;
#else
return getClass(authenticated);
#endif
}
inline Class
isa_t::getClass(MAYBE_UNUSED_AUTHENTICATED_PARAM bool authenticated) {
#if SUPPORT_INDEXED_ISA
return cls;
#else
uintptr_t clsbits = bits;
# if __has_feature(ptrauth_calls)
# if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
// Most callers aren't security critical, so skip the
// authentication unless they ask for it. Message sending and
// cache filling are protected by the auth code in msgSend.
if (authenticated) {
// Mask off all bits besides the class pointer and signature.
clsbits &= ISA_MASK;
if (clsbits == 0)
return Nil;
clsbits = (uintptr_t)ptrauth_auth_data((void *)clsbits, ISA_SIGNING_KEY, ptrauth_blend_discriminator(this, ISA_SIGNING_DISCRIMINATOR));
} else {
// If not authenticating, strip using the precomputed class mask.
clsbits &= objc_debug_isa_class_mask;
}
# else
// If not authenticating, strip using the precomputed class mask.
clsbits &= objc_debug_isa_class_mask;
# endif
# else
clsbits &= ISA_MASK;
# endif
return (Class)clsbits;
#endif
}
// a better definition is
// (uintptr_t)ptrauth_strip((void *)ISA_MASK, ISA_SIGNING_KEY)
// however we know that PAC uses bits outside of MACH_VM_MAX_ADDRESS
// so approximate the definition here to be constant
template <typename T>
static constexpr T coveringMask(T n) {
for (T mask = 0; mask != ~T{0}; mask = (mask << 1) | 1) {
if ((n & mask) == n) return mask;
}
return ~T{0};
}
const uintptr_t objc_debug_isa_class_mask = ISA_MASK & coveringMask(MACH_VM_MAX_ADDRESS - 1);
# define ISA_MASK 0x0000000ffffffff8ULL
虽然上面的代码看着比较复杂,但是经过编译器处理后,它就变为了以下代码:
代码语言:javascript复制0x1996c9b48 < 8>: ldr x8, [x0]
0x1996c9b4c < 12>: and x9, x8, #0xffffffff8
hasCustomRR
接下来,我们再看看第 2 步的实现代码:
代码语言:javascript复制// class or superclass has default retain/release/autorelease/retainCount/
// _tryRetain/_}isDeallocating/retainWeakReference/allowsWeakReference
#define FAST_HAS_DEFAULT_RR (1UL<<2)
struct objc_object {
private:
isa_t isa;
}
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
bool hasCustomRR() const {
return !bits.getBit(FAST_HAS_DEFAULT_RR);
}
}
struct class_data_bits_t {
friend objc_class;
// Values are the FAST_ flags above.
uintptr_t bits;
private:
bool getBit(uintptr_t bit) const
{
return bits & bit;
}
}
第 2 步的逻辑比较简单,编译处理后的汇编如下:
代码语言:javascript复制-> 0x1996c9b50 < 16>: ldr x10, [x9, #0x20]
0x1996c9b54 < 20>: tbz w10, #0x2, 0x1996c9bb4 ; < 116>
编译器通过内联优化,最后会变成本次崩溃的汇编代码:
代码语言:javascript复制libobjc.A.dylib`objc_release:
// 判断是否属于nil或者tag
0x1996c9b40 < 0>: cmp x0, #0x1 ; =0x1
0x1996c9b44 < 4>: b.lt 0x1996c9bb0 ; < 112>
// 读取 isa
0x1996c9b48 < 8>: ldr x8, [x0]
// 读取 class
0x1996c9b4c < 12>: and x9, x8, #0xffffffff8
// 读取 class 的 bits
-> 0x1996c9b50 < 16>: ldr x10, [x9, #0x20]
// 判断 class 的 bits 是否存在标志信息
0x1996c9b54 < 20>: tbz w10, #0x2, 0x1996c9bb4 ; < 116>
0x1996c9b58 < 24>: tbz w8, #0x0, 0x1996c9bd4 ; < 148>
0x1996c9b5c < 28>: mov x9, #0x100000000000000
0x1996c9b60 < 32>: lsr x10, x8, #55
0x1996c9b64 < 36>: cbz x10, 0x1996c9bb0 ; < 112>
0x1996c9b68 < 40>: subs x10, x8, x9
0x1996c9b6c < 44>: b.lo 0x1996c9b94 ; < 84>
0x1996c9b70 < 48>: mov x11, x8当
六、崩溃原理
现在,我们对前面的内容进行一下总结:
当出现悬垂指针并且悬垂指针指向的地址被其它代码重新申请后进行赋值操作,并且新值不符合 isTaggedPointer
规定,随后通过isa
--> class
-->bits
进行内存读取操作时就会触发崩溃。
小结:
经过前面的分析,我们可以得知,iOS 的新系统中存在一个 bug,该 bug 导致即使我们通过将参数waitUntilDone
设置为YES
的方式阻塞当前线程时,仍然存在触发悬垂指针的可能。
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
七、解决方案
因为崩溃的原因是调用performSelector:onThread:
时,参数会被系统私有类持有导致崩溃,所以,我们可以通过以下方案解决:
1、通过单例持有 GCDAsyncSocket
,避免调用 -[GCDAsyncSocket dealloc]
2、先主动调用-[GCDAsyncSocket disconnect]
,再释放GCDAsyncSocket
的实例
3、通过调整withObject:
的参数,避免将 GCDAsyncSocket
的实例进行传递
[[self class] performSelector:@selector(unscheduleCFWriteStreams:)
onThread:cfstreamThread
withObject:(__bridge id _Nullable)self->writeStream