ARC原理探究

作者: 永远保持一颗进取心 | 来源:发表于2018-12-29 15:40 被阅读10次

    ARC原理探究

    目录:
    1. __autoreleasing 的理解
    2.autorelesepool 工作机制
    3.__weak 的实现机制
    4. 这样写有问题吗?
    5.备注

    个人知识有限,不保证准确和正确,请各位看官自行判断

    1. __autoreleasing 的理解

    首先查看官方解释

    __autoreleasing is used to denote arguments that are passed by reference (id *) and are autoreleased on return.

    大概意思是:__autoreleasing 所修饰的参数意味着通过引用形式(id *)被传递,而且会被 autoreleased(也就是执行一次[arg autorelease])

    接着看个官方说明例子

    前提:我们声明了一个方法

    - (void)performOperationWithError:(NSError * __autoreleasing *)error;
    
    NSError *err;
    BOOL ok = [myObj performOperationWithError:&err];
    if(ok) {
        //do something here
    }
    

    此处 变量err和形参error修饰词(qulifier)匹配不上;err__strong(缺省),而error__autoreleasing

    但是我们平时都是这样写,为何没问题?
    原来是 编译器 将此处的代码转换如下

    NSError *err;
    NSError * __autoreleasing temp = err;
    BOOL ok = [myObj performOperationWithError:&temp];
    err = temp;
    if(ok) {
        //do something here
    }
    

    所以,我们平时可以选择使用 __autoreleasing 修饰,这样就不用麻烦编译器了。

    2.autorelesepool 工作机制

    首先看官方文档重温下 autoreleasepool 的作用

    In a reference-counted environment (as opposed to one which uses garbage collection), an NSAutoreleasePool object contains objects that have received an autorelease message and when drained it sends a release message to each of those objects. Thus, sending autorelease instead of release to an object extends the lifetime of that object at least until the pool itself is drained (it may be longer if the object is subsequently retained). An object can be put into the same pool several times, in which case it receives a release message for each time it was put into the pool.
    In a reference counted environment, Cocoa expects there to be an autorelease pool always available. If a pool is not available, autoreleased objects do not get released and you leak memory. In this situation, your program will typically log suitable warning messages.

    大概意思是:自动释放池(autorelease)包含获取过 autorelease 消息的对象,当自动释放池被销毁(drained)时,会向这些对象发送 release 消息。对象收到 autorelease 和 release 次数相等。

    而且,我们知道 @autoreleasepool {} 是可以嵌套(nested)调用的。
    所以我们会问,@autoreleasepool {}内部是怎么实现嵌套的?

    网上有一片文章对这个问题有很好的解释,并且附带上的研究过程,我并没有信心比他写得更好,有兴趣的可以研读一下。
    此处,我把结论贴一下:

    autoreleasepool 实际上是一个栈结构,获得 autoreleased 消息的对象都会入栈
    我们使用@autoreleasepool {}的时候,编译器会在{}代码块的前后分别调用push函数和pop函数,push函数会向栈顶加一个哨兵(源码中是 nil), 用于标记一个自动释放池的开始;当执行{}里面的代码时,收到autorelease消息的对象会一一被推入栈中,当执行到pop函数时,栈内顶到哨兵对象位置之间的对象会收到release消息。

    我们用代码看下这个过程

    //在 WHObject 类 写出下面方法
    - (void)testAutoreleasingWord {
        @autoreleasepool {  
            NSObject *obj = [NSObject new];
            NSLog(@"%@", obj);
        }
    }
    

    通过[命令](#rewrite_to _cpp)转化为 C++ 代码

    //截取部分有用代码
    extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
    extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
    
    struct __AtAutoreleasePool {
      __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
      ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
      void * atautoreleasepoolobj;
    };
    
    static void _I_WHObject_testAutoreleasingWord(WHObject * self, SEL _cmd) {
    
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"));
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kp_xmzr06bs27xd6t7tpcf10_g80000gn_T_WHObject_86ce9b_mi_0, obj);
        }
    }
    

    可以看到,代码块内定义了一个结构体变量

    __AtAutoreleasePool __autoreleasepool;
    

    定义该结构体时,执行构造函数,内部执行了push

    __AtAutoreleasePool() {
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
    

    当自动释放池内的代码执行完毕,结构体变量执行析构函数,内部执行了pop

    ~__AtAutoreleasePool(){
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    

    所以我们用自己的代码简化一下这个过程如下:

    - (void)testAutoreleasingWord {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();  
        NSObject *obj = [NSObject new];
        NSLog(@"%@", obj);
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
    

    3.__weak 的实现机制

    首先我们看看官方的解释

    __weak specifies a reference that does not keep the referenced object alive. A weak reference is set to nil when there are no strong references to the object.
    大概意思是:__weak修饰的引用不会保活对象,当指向的对象没有强引用的时候,弱引用会被置空.

    温习一下 ARC 的原理

    官方文档的解释使用 强引用弱引用 的概念,有强引用时,对象不会被释放,没有强引用时对象会被释放;弱引用对此无能为力,只有读取和被置空的能力。
    而我们都知道 ARC 和 MRC 的本质都是一样的,都是使用了引用计数;ARC 是编译器帮我们自动插入了retainreleaseautorelease等代码。
    每增加一个强引用,对象的引用计数+1,反之-1

    我们知道了怎么使用(What),那么__weak的机制是什么呢(How)?

    先回答这个问题:

    有一张哈希表专门管理弱引用指针,每定义一个弱引用指针,都会把指针地址存储在哈希表中,当一个对象被释放的时候,会将相应的弱引用指针移出哈希表并置为nil。

    下面我们验证一下这个过程:
    有以下方法:

    - (void)testAutoreleasingWord {
        NSObject *obj = [NSObject new];
        NSObject * __weak weakObj = obj;
        NSLog(@"%@", weakObj);    
    }
    

    经过[命令](#rewrite_to _middle)转为中间码

    //中间码过长,截取部分
      %15 = load %1*, %1** %5, align 8
      %16 = bitcast %1** %6 to i8**
      %17 = bitcast %1* %15 to i8*
      %18 = call i8* @objc_initWeak(i8** %16, i8* %17) #3
      %19 = bitcast %1** %6 to i8**
      %20 = call i8* @objc_loadWeakRetained(i8** %19) #3
      %21 = bitcast i8* %20 to %1*
      invoke void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*), %1* %21)
              to label %22 unwind label %26
    

    在中间码中我们可以看到两个关键字眼objc_initWeakobjc_loadWeakRetained

    查看runtime源码,看看objc_loadWeakRetained做了什么

    /** 
     * This loads the object referenced by a weak pointer and returns it, after
     * retaining and autoreleasing the object to ensure that it stays alive
     * long enough for the caller to use it. This function would be used
     * anywhere a __weak variable is used in an expression.
     * 
     * @param location The weak pointer address
     * 
     * @return The object pointed to by \e location, or \c nil if \e location is \c nil.
     */
    id objc_loadWeak(id *location)
    {
        if (!*location) return nil;
        return objc_autorelease(objc_loadWeakRetained(location));
    }
    

    由注释和代码可知,这个方法是延长弱引用对象的生命周期;这个我们可以暂且不管,感兴趣的同学可以继续研究。

    接下来再看看objc_initWeak,查看 runtime 源码, 由于源码太长,这里贴一下方法的大概调用过程

    //每个方法都有要处理的逻辑,此处大大省略了其中的过程
    //因为,1.个人能力和精力暂时有限(主因)
    //     2.是代码过长
    objc_initWeak()
        storeWeak()
            weak_register_no_lock() //在哈希表中存入弱引用指针的地址
    

    由此,我们知道了弱引用形成的大概过程。哪它是怎么被置为 nil 的呢?
    我们知道,当一个对象没有了强引用时(即引用计数为0),就会被释放,相关弱引用指针就会被置为nil,而 Objective-C 对象被释放会执行 dealloc,所以我们去 runtime 源码看一看 dealloc做了哪些事情

    //dealloc 方法逐层调用,会走到 rootDealloc() 方法
    inline void
    objc_object::rootDealloc()
    {
        if (isTaggedPointer()) return;  // fixme necessary?
    
        if (fastpath(isa.nonpointer  &&  
                     !isa.weakly_referenced  &&  
                     !isa.has_assoc  &&  
                     !isa.has_cxx_dtor  &&  
                     !isa.has_sidetable_rc))
        {
            assert(!sidetable_present());
            free(this);
        } 
        else {
            object_dispose((id)this);
        }
    }
    
    

    可以看到如果对象被判断没有弱引用!isa.weakly_referenced以及满足其他条件(看官自行了解),则会执行 free 函数,释放内存。
    否则,执行 object_dispose() 函数

    id object_dispose(id obj)
    {
        if (!obj) return nil;
        objc_destructInstance(obj);    
        free(obj);
        return nil;
    }
    

    执行完 objc_destructInstance后,立刻释放对象,所以玄机就在objc_destructInstance里面

    //经过层层跳转,可以看到清理弱引用指针的方法
    //方法有点长,可以直接看其中的 for 循环 和 最后一句代码
    void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
    {
        objc_object *referent = (objc_object *)referent_id;
    
        weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
        if (entry == nil) {
            /// XXX shouldn't happen, but does with mismatched CF/objc
            //printf("XXX no entry for clear deallocating %p\n", referent);
            return;
        }
    
        // zero out references
        weak_referrer_t *referrers;
        size_t count;
        
        if (entry->out_of_line()) {
            referrers = entry->referrers;
            count = TABLE_SIZE(entry);
        } 
        else {
            referrers = entry->inline_referrers;
            count = WEAK_INLINE_COUNT;
        }
        
        //就是在这个位置,把所有的弱引用都置为nil
        for (size_t i = 0; i < count; ++i) {
            objc_object **referrer = referrers[i];
            if (referrer) {
                if (*referrer == referent) {
                    *referrer = nil;
                }
                else if (*referrer) {
                    _objc_inform("__weak variable at %p holds %p instead of %p. "
                                 "This is probably incorrect use of "
                                 "objc_storeWeak() and objc_loadWeak(). "
                                 "Break on objc_weak_error to debug.\n", 
                                 referrer, (void*)*referrer, (void*)referent);
                    objc_weak_error();
                }
            }
        }
        
        //这句代码把对象对应的弱引用入口删除
        weak_entry_remove(weak_table, entry);
    }
    

    所以,查看源码验证了回答。

    4. 这样写有问题吗?

    在公司的工程中,有看到过这种代码;按照一般的思路,return语句会在到达@autoreleasepool {}结束时直接返回。
    我们知道releaseautorelease 代码会在编译阶段插入,所以不能用一般的代码思路去理解。

    - (id)createObj{
        @autoreleasepool {
            NSObject *obj = [NSObject new];
            return obj;
        }
    }
    
    

    我们通过[命令](#rewrite_to _middle)转换成中间码

    define internal i8* @"\01-[WHObject createObj]"(%0*, i8*) #0 {
      %3 = alloca %0*, align 8
      %4 = alloca i8*, align 8
      %5 = alloca %1*, align 8
      store %0* %0, %0** %3, align 8
      store i8* %1, i8** %4, align 8
      %6 = call i8* @objc_autoreleasePoolPush() #2
      %7 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
      %8 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !8
      %9 = bitcast %struct._class_t* %7 to i8*
      %10 = call i8* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i8* (i8*, i8*)*)(i8* %9, i8* %8)
      %11 = bitcast i8* %10 to %1*
      store %1* %11, %1** %5, align 8
      %12 = load %1*, %1** %5, align 8
      %13 = bitcast %1* %12 to i8*
      %14 = call i8* @objc_retain(i8* %13) #2
      %15 = bitcast %1** %5 to i8**
      call void @objc_storeStrong(i8** %15, i8* null) #2
      call void @objc_autoreleasePoolPop(i8* %6)
      %16 = tail call i8* @objc_autoreleaseReturnValue(i8* %14) #2
      ret i8* %16
    }
    

    我们看到 objc_autoreleaseReturnValueret 语句被移动 objc_autoreleasePoolPop 后面。
    所以可以说,编译器会把 return obj; 移到 @autoreleasepool{}后面

    备注

    参考链接

    ARC原理探究:http://luoxianming.cn/2017/05/06/arc/

    官方文档(Transitioning to ARC Release Notes):
    https://developer.apple.com/library/archive/releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html

    Objective-C Autorelease Pool 的实现原理:
    http://www.cocoachina.com/ios/20150610/12093.html

    shell命令:
    <span id="rewrite_to _middle">将 Objective-C 代码转为中间码</span>

    clang -S -fobjc-arc -emit-llvm SourceCode.m -o OutputCode.txt
    

    <span id="rewrite_to _cpp">将 Objective-C 代码转为 CPP</span>

    clang -rewrite-objc -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations SourceCode.m -o OutputCode.cpp
    

    简书的页面内跳转好像无效,了解的同学劳烦告知一下

    相关文章

      网友评论

        本文标题:ARC原理探究

        本文链接:https://www.haomeiwen.com/subject/pqszixtx.html