iOS dealloc中初始化weak指针崩溃防护
序
开发过程中,总是难免写出一些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)