iOS dealloc中初始化weak指针崩溃防护

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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)

  

以上是关于iOS dealloc中初始化weak指针崩溃防护的主要内容,如果未能解决你的问题,请参考以下文章

weak属性需要在dealloc中置nil么?

__weak存在的问题

weak引用变量是否线程安全

Dealloc weak nil

IOS 强指针(strong)和弱指针(weak)

[C++11]弱引用智能指针weak_ptr初始化和相关的操作函数