序
开发过程中,总是难免写出一些bug导致崩溃,即使有些崩溃原因显而易见,我们也很难完全避免, 这时候就要通过一些技术手段来避免问题。
今天想给大家分享一下关于在dealloc中初始化一个指向自身的weak指针产生崩溃的防护方案。
在某些类的dealloc中我们会去做一些清理工作,这时候可能就会去初始化一个指向自身的weak指针,尤其是在使用懒加载的时候。
问题分析
这段代码在走到dealloc的时候是必崩的。
#import "WeakTestObj.h" @implementation WeakTestObj - (void)dealloc {
__weak typeof(self) weakSelf = self;
} @end
原因呢我们可以看一下崩溃堆栈和输出的错


很明显,当我们在dealloc初始化指向自身的weak指针时,在objc_initWeak中发生了错误,原因是因为该对象正在被释放当中。
但为何对象在释放时初始化指向自身的weak指针会崩溃呢,我们可以通过runtime源码(https://opensource.apple.com/tarballs/objc4/)进一步分析。
我们先通过Xcode看一下崩溃时的汇编代码
libobjc.A.dylib`objc_initWeak:
0x7fff2019209e <+0>: pushq %rbp
0x7fff2019209f <+1>: movq %rsp, %rbp
0x7fff201920a2 <+4>: pushq %r15
0x7fff201920a4 <+6>: pushq %r14
0x7fff201920a6 <+8>: pushq %r13
0x7fff201920a8 <+10>: pushq %r12
0x7fff201920aa <+12>: pushq %rbx
0x7fff201920ab <+13>: subq $0x28, %rsp
0x7fff201920af <+17>: testq %rsi, %rsi
0x7fff201920b2 <+20>: je 0x7fff20192227 ; <+393>
0x7fff201920b8 <+26>: movq %rsi, %r13
0x7fff201920bb <+29>: movq %rdi, -0x40(%rbp)
0x7fff201920bf <+33>: movabsq $0x7ffffffffff8, %r14 ; imm = 0x7FFFFFFFFFF8
0x7fff201920c9 <+43>: movl %r13d, %eax
0x7fff201920cc <+46>: shrl $0x9, %eax
0x7fff201920cf <+49>: movl %r13d, %r12d
0x7fff201920d2 <+52>: shrl $0x4, %r12d
0x7fff201920d6 <+56>: xorl %eax, %r12d
0x7fff201920d9 <+59>: andl $0x3f, %r12d
0x7fff201920dd <+63>: shlq $0x6, %r12
0x7fff201920e1 <+67>: leaq 0x5fea34d8(%rip), %rax ; (anonymous namespace)::SideTablesMap
0x7fff201920e8 <+74>: leaq (%rax,%r12), %r15
0x7fff201920ec <+78>: movq %r15, %rdi
0x7fff201920ef <+81>: movl $0x50000, %esi ; imm = 0x50000
0x7fff201920f4 <+86>: callq 0x7fff201952b4 ; symbol stub for: os_unfair_lock_lock_with_options
0x7fff201920f9 <+91>: movq %r13, %rax
0x7fff201920fc <+94>: shrq $0x3c, %rax
0x7fff20192100 <+98>: movq %rax, -0x38(%rbp)
0x7fff20192104 <+102>: movq %r13, %rax
0x7fff20192107 <+105>: shrq $0x31, %rax
0x7fff2019210b <+109>: andl $0x7f8, %eax ; imm = 0x7F8
0x7fff20192110 <+114>: addq 0x64853871(%rip), %rax ; (void *)0x00007fff80030870: objc_debug_taggedpointer_ext_classes
0x7fff20192117 <+121>: movq %rax, -0x30(%rbp)
0x7fff2019211b <+125>: xorl %eax, %eax
0x7fff2019211d <+127>: movq %r13, %rcx
0x7fff20192120 <+130>: testq %r13, %r13
0x7fff20192123 <+133>: js 0x7fff20192192 ; <+244>
0x7fff20192125 <+135>: movq (%rcx), %rbx
0x7fff20192128 <+138>: cmpq %rax, %rbx
0x7fff2019212b <+141>: je 0x7fff201921ba ; <+284>
0x7fff20192131 <+147>: movq (%rbx), %rax
0x7fff20192134 <+150>: leaq -0x1(%rax), %rcx
0x7fff20192138 <+154>: cmpq $0xf, %rcx
0x7fff2019213c <+158>: jb 0x7fff2019214d ; <+175>
0x7fff2019213e <+160>: movq 0x20(%rbx), %rcx
0x7fff20192142 <+164>: andq %r14, %rcx
0x7fff20192145 <+167>: testb $0x1, (%rcx)
0x7fff20192148 <+170>: je 0x7fff2019214d ; <+175>
0x7fff2019214a <+172>: movq %rbx, %rax
0x7fff2019214d <+175>: movq 0x20(%rax), %rax
0x7fff20192151 <+179>: andq %r14, %rax
0x7fff20192154 <+182>: testb $0x20, 0x3(%rax)
0x7fff20192158 <+186>: jne 0x7fff201921ba ; <+284>
0x7fff2019215a <+188>: movq %r15, %rdi
0x7fff2019215d <+191>: callq 0x7fff201952ba ; symbol stub for: os_unfair_lock_unlock
0x7fff20192162 <+196>: leaq 0x5fe9f1d3(%rip), %rdi ; runtimeLock
0x7fff20192169 <+203>: movl $0x50000, %esi ; imm = 0x50000
0x7fff2019216e <+208>: callq 0x7fff201952b4 ; symbol stub for: os_unfair_lock_lock_with_options
0x7fff20192173 <+213>: movq %rbx, %rdi
0x7fff20192176 <+216>: movq %r13, %rsi
0x7fff20192179 <+219>: xorl %edx, %edx
0x7fff2019217b <+221>: callq 0x7fff2017bcdc ; initializeAndMaybeRelock(objc_class*, objc_object*, mutex_tt<false>&, bool)
0x7fff20192180 <+226>: movq %r15, %rdi
0x7fff20192183 <+229>: movl $0x50000, %esi ; imm = 0x50000
0x7fff20192188 <+234>: callq 0x7fff201952b4 ; symbol stub for: os_unfair_lock_lock_with_options
0x7fff2019218d <+239>: movq %rbx, %rax
0x7fff20192190 <+242>: jmp 0x7fff2019211d ; <+127>
0x7fff20192192 <+244>: movq -0x38(%rbp), %rcx
0x7fff20192196 <+248>: leaq 0x5fe9e653(%rip), %rdx ; objc_debug_taggedpointer_classes
0x7fff2019219d <+255>: movq (%rdx,%rcx,8), %rbx
0x7fff201921a1 <+259>: movq -0x30(%rbp), %rcx
0x7fff201921a5 <+263>: leaq 0x5fe9e504(%rip), %rdx ; (void *)0x00007fff80030688: __NSUnrecognizedTaggedPointer
0x7fff201921ac <+270>: cmpq %rdx, %rbx
0x7fff201921af <+273>: jne 0x7fff20192128 ; <+138>
0x7fff201921b5 <+279>: jmp 0x7fff20192125 ; <+135>
0x7fff201921ba <+284>: leaq 0x5fea33ff(%rip), %rax ; (anonymous namespace)::SideTablesMap
0x7fff201921c1 <+291>: leaq 0x20(%r12,%rax), %rdi
0x7fff201921c6 <+296>: movq %rax, %r12
0x7fff201921c9 <+299>: movq %r13, %rsi
0x7fff201921cc <+302>: movq -0x40(%rbp), %r14
0x7fff201921d0 <+306>: movq %r14, %rdx
0x7fff201921d3 <+309>: movl $0x1, %ecx
0x7fff201921d8 <+314>: callq 0x7fff201905ff ; weak_register_no_lock
-> 0x7fff201921dd <+319>: movq %rax, %rbx
0x7fff201921e0 <+322>: testq %rax, %rax
崩溃发生在85行处,然后我们往上查找,可以看到是在weak_register_no_lock 这个方法内部发生了崩溃。
id weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
if (referent->isTaggedPointerOrNil()) return referent_id;
// ensure that the referenced object is viable
if (deallocatingOptions == ReturnNilIfDeallocating ||
deallocatingOptions == CrashIfDeallocating) {
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
deallocating = referent->rootIsDeallocating();
}
else {
// Use lookUpImpOrForward so we can avoid the assert in
// class_getInstanceMethod, since we intentionally make this
// callout with the lock held.
auto allowsWeakReference = (BOOL(*)(objc_object *, SEL))
lookUpImpOrForwardTryCache((id)referent, @selector(allowsWeakReference),
referent->getIsa());
if ((IMP)allowsWeakReference == _objc_msgForward) {
return nil;
}
deallocating =
! (*allowsWeakReference)(referent, @selector(allowsWeakReference));
}
if (deallocating) {
if (deallocatingOptions == CrashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of "
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
return nil;
}
}
}
// now remember it and where it is being stored
weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
append_referrer(entry, referrer);
}
else {
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}
// Do not set *referrer. objc_storeWeak() requires that the
// value not change.
return referent_id;
}
可以看一下第31行代码处,如果自身在dealloc中并且deallocatingOptions == CrashIfDeallocating,则打印错误日志并退出。
系统为什么要这么做呢? 因为在对象dealloc时,系统会去查找对象的弱引用表,并把所有指向该对象的弱引用置为nil,如果我们在对象的dealloc中去初始化一个指向该对象的弱引用指针,很明显这是会产生冲突的。
现在我们已经知道了崩溃产生的具体原因和位置,接下来就开始思考如何防护。
在37行代码处我们可以看到,如果 deallocatingOptions != CrashIfDeallocating系统直接返回了nil,此时不发生崩溃。因此我们也可以通过同样的操作来避免崩溃的出现。
在到崩溃发生前,其实经过了好几个C方法的调用,分别是 objc_initWeak,
storeWeak, weak_register_no_lock,理论上我们hook这当中任意一个方法都是可以的。hook之后判断一下该对象是否正在释放当中,如果是的话,直接返回nil。
至于如何判断是否正在释放中,我们也可以通过runtime的源码找到答案。
本人采用的是NSObjct的一个私有方法。
- (BOOL)_isDeallocating {
return _objc_rootIsDeallocating(self);
}
解决方案
接下来看一下具体的实现代码
#import "fishhook.h" //fishhook,请自行搜索
//分类暴露私有方法
@interface NSObject (runtimePrivate)
- (BOOL)_isDeallocating;
@end
static id(*sys_objc_initWeak)(id _Nullable * _Nonnull location, id _Nullable obj);
id _Nullable
aigis_objc_initWeak(id _Nullable * _Nonnull location, id _Nullable obj) {
//判断是否正在释放
if ([obj respondsToSelector:@selector(_isDeallocating)]) {
if([obj _isDeallocating]) {
return nil;
}
}
//调用系统的initWeak
return sys_objc_initWeak(location,obj);
}
//调用此方法进行hook
void start_objec_weak_defender() {
//hook objc_initWeak 方法
struct rebinding rebindObj;
rebindObj.name = "objc_initWeak";
rebindObj.replacement = aigis_objc_initWeak;
rebindObj.replaced = (void *)&sys_objc_initWeak;
struct rebinding rebindings[] = {rebindObj};
int result = rebind_symbols(rebindings, 1);
}
此外,细心的朋友们应该也注意到另一种方案,hook weak_register_no_lock之后,直接修改deallocatingOptions参数。(ps: 此方案未实现过,感兴趣的朋友们可以自行探索)
weak_register_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)