iOS ARC全解?

作者: SunnyLeong | 来源:发表于2018-02-12 10:33 被阅读644次

    问题
    简单介绍 ARC 以及 ARC 实现的原理。
    考查点

    我记得在刚接触iOS的时候对这个ARC和MRC就讨论颇深,认为ARC是对程序员的一种福利,让我们节省了大量的代码,那么ARC是什么呢?

    ARC 是苹果在 WWDC 2011 提出来的技术,因此很多新入行的同学可能对此技术细节并不熟悉。但是,虽然 ARC 极大地简化了我们的内存管理工作,但是引用计数这种内存管理方案如果不被理解,那么就无法处理好那些棘手的循环引用问题。所以,这道面试题其实是考查同学对于 iOS 程序内存管理的理解深度。
    答案

    自动的引用计数(Automatic Reference Count 简称 ARC),是苹果在 WWDC 2011 年大会上提出的用于内存管理的技术。

    引用计数(Reference Count)是一个简单而有效的管理对象生命周期的方式。当我们创建一个新对象的时候,它的引用计数为 1,当有一个新的指针指向这个对象时,我们将其引用计数加 1,当某个指针不再指向这个对象是,我们将其引用计数减 1,当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。由于引用计数简单有效,除了 Objective-C 语言外,微软的 COM(Component Object Model )、C++11(C++11 提供了基于引用计数的智能指针 share_prt) 等语言也提供了基于引用计数的内存管理方式。

    引用计数这种内存管理方式虽然简单,但是手工写大量的操作引用计数的代码不但繁琐,而且容易被遗漏。于是苹果在 2011 年引入了 ARC。ARC 顾名思义,是自动帮我们填写引用计数代码的一项功能。

    ARC 的想法来源于苹果在早期设计 Xcode 的 Analyzer 的时候,发现编译器在编译时可以帮助大家发现很多内存管理中的问题。后来苹果就想,能不能干脆编译器在编译的时候,把内存管理的代码都自动补上,带着这种想法,苹果修改了一些内存管理代码的书写方式(例如引入了 @autoreleasepool 关键字)后,在 Xcode 中实现了这个想法。

    ARC 的工作原理大致是这样:当我们编译源码的时候,编译器会分析源码中每个对象的生命周期,然后基于这些对象的生命周期,来添加相应的引用计数操作代码。所以,ARC 是工作在编译期的一种技术方案,这样的好处是:

    编译之后,ARC 与非 ARC 代码是没有什么差别的,所以二者可以在源码中共存。实际上,你可以通过编译参数 -fno-objc-arc 来关闭部分源代码的 ARC 特性。

    相对于垃圾回收这类内存管理方案,ARC 不会带来运行时的额外开销,所以对于应用的运行效率不会有影响。相反,由于 ARC 能够深度分析每一个对象的生命周期,它能够做到比人工管理引用计数更加高效。例如在一个函数中,对一个对象刚开始有一个引用计数 +1 的操作,之后又紧接着有一个 -1 的操作,那么编译器就可以把这两个操作都优化掉。

    但是也有人认为,ARC 也附带有运行期的一些机制来使 ARC 能够更好的工作,他们主要是指 weak 关键字。weak 变量能够在引用计数为 0 时被自动设置成 nil,显然是有运行时逻辑在工作的。我通常并没有把这个算在 ARC 的概念当中,当然,这更多是一个概念或定义上的分歧,因为除开 weak 逻辑之外,ARC 核心的代码都是在编译期填充的。

    作者:优雅地小男子


    高级解析


    前言

    本文的ARC特指Objective C的ARC,并不会讲解其他语言。另外,本文涉及到的原理部分较多,适合有一定经验的开发者。

    什么是ARC?

    ARC的全称Auto Reference Counting. 也就是自动引用计数。那么,为什么要有ARC呢?

    我们从C语言开始。使用C语言编程的时候,如果要在堆上分配一块内存,代码如下

    `//分配内存(malloc/calloc均可)`
    
    `int * array = calloc(10, sizeof (int));`
    
    `//释放内存`
    
    `free(array);1234512345`
    
    

    C是面向过程的语言(Procedural programming),这种内存的管理方式简单直接。但是,对于面向对象编程,这种手动的分配释放毫无疑问会大大的增加代码的复杂度。

    于是,OOP的语言引入了各种各样的内存管理方法,比如Java的垃圾回收和Objective C的引用计数。关于垃圾回收和饮用计数的对比,可以参见Brad Larson的这个SO回答。

    Objective C的引用计数理解起来很容易,当一个对象被持有的时候计数加一,不再被持有的时候引用计数减一,当引用计数为零的时候,说明这个对象已经无用了,则将其释放。

    引用计数分为两种:

    • 手动引用计数(MRC)

    • 自动引用计数(ARC)

    iOS开发早期,编写代码是采用MRC的

    
    `// MRC代码`
    
    `NSObject * obj = [[NSObject alloc] init]; ``//引用计数为1`
    
    `//不需要的时候`
    
    `[obj release] ``//引用计数减1`
    
    `//持有这个对象`
    
    `[obj retain] ``//引用计数加1`
    
    `//放到AutoReleasePool`
    
    `[obj autorelease]``//在auto release pool释放的时候,引用计数减1`
    
    
    

    虽说这种方式提供了面向对象的内存管理接口,但是开发者不得不花大量的时间在内存管理上,并且容易出现内存泄漏或者release一个已被释放的对象,导致crash。

    再后来,Apple对iOS/Mac OS开发引入了ARC。使用ARC,开发者不再需要手动的retain/release/autorelease. 编译器会自动插入对应的代码,再结合Objective C的runtime,实现自动引用计数。

    比如如下ARC代码:

    
    `NSObject * obj;`
    
    `{`
    
    `obj = [[NSObject alloc] init]; ``//引用计数为1`
    
    `}`
    
    `NSLog(@``"%@"``,obj);`
    
    
    

    等同于如下MRC代码

    `NSObject * obj;`
    
    `{`
    
    `obj = [[NSObject alloc] init]; ``//引用计数为1`
    
    `[obj relrease]`
    
    `}`
    
    `NSLog(@``"%@"``,obj);`
    
    

    在Objective C中,有三种类型是ARC适用的:

    • block

    • objective 对象,id, Class, NSError*等

    • attribute((NSObject))标记的类型。

    像double *,CFStringRef等不是ARC适用的,仍然需要手动管理内存。

    Tips: 以CF开头的(Core Foundation)的对象往往需要手动管理内存。

    属性所有权

    最后,我们在看看ARC中常见的所有权关键字,

    • assign对应关键字__unsafe_unretained, 顾名思义,就是指向的对象被释放的时候,仍然指向之前的地址,容易引起野指针。

    • copy对应关键字__strong,只不过在赋值的时候,调用copy方法。

    • retain对应__strong

    • strong对应__strong

    • unsafe_unretained对应__unsafe_unretained

    • weak对应__weak。

    其中,__weak和__strong是本文要讲解的核心内容。

    ARC的内部实现

    ARC背后的引用计数主要依赖于这三个方法:

    • retain 增加引用计数

    • release 降低引用计数,引用计数为0的时候,释放对象。

    • autorelease 在当前的auto release pool结束后,降低引用计数。

    在Cocoa Touch中,NSObject协议中定义了这三个方法,由于Cocoa Touch中,绝大部分类都继承自NSObject(NSObject类本身实现了NSObject协议),所以可以“免费”获得NSObject提供的运行时和ARC管理方法,这就是为什么适用OC开发iOS的时候,你的类要继承自NSObject。

    既然ARC是引用计数,那么对应一个对象,内存中必然会有一个地方来存储这个对象的引用计数。iOS的Runtime是开源的,在这里可以下载到全部的代码,我们通过源代码一探究竟。

    我们从retain入手,

    
    `- (id)retain {`
    
    `return` `((id)self)->rootRetain();`
    
    `}`
    
    `inline id objc_object::rootRetain()`
    
    `{`
    
    `if` `(isTaggedPointer()) ``return` `(id)``this``;`
    
    `return` `sidetable_retain();`
    
    `}`
    
    
    

    所以说,本质上retain就是调用sidetable_retain,再看看sitetable_retain的实现:

    
    `id objc_object::sidetable_retain()`
    
    `{`
    
    `//获取table`
    
    `SideTable& table = SideTables()[``this``];`
    
    `//加锁`
    
    `table.lock();`
    
    `//获取引用计数`
    
    `size_t& refcntStorage = table.refcnts[``this``];`
    
    `if` `(! (refcntStorage & SIDE_TABLE_RC_PINNED)) {`
    
    `//增加引用计数`
    
    `refcntStorage += SIDE_TABLE_RC_ONE;`
    
    `}`
    
    `//解锁`
    
    `table.unlock();`
    
    `return` `(id)``this``;`
    
    `}`
    
    
    

    到这里,retain如何实现就很清楚了,通过SideTable这个数据结构来存储引用计数。我们看看这个数据结构的实现:

    QQ截图20170421165138.png

    可以看到,这个数据结构就是存储了一个自旋锁,一个引用计数map。这个引用计数的map以对象的地址作为key,引用计数作为value。到这里,引用计数的底层实现我们就很清楚了。

    存在全局的map,这个map以地址作为key,引用计数的值作为value。

    再来看看release的实现:

    `SideTable& table = SideTables()[``this``];`
    
    `bool do_dealloc = ``false``;`
    
    `table.lock();`
    
    `//找到对应地址的`
    
    `RefcountMap::iterator it = table.refcnts.find(``this``);`
    
    `if` `(it == table.refcnts.end()) { ``//找不到的话,执行dellloc`
    
    `do_dealloc = ``true``;`
    
    `table.refcnts[``this``] = SIDE_TABLE_DEALLOCATING;`
    
    `} ``else` `if` `(it->second < SIDE_TABLE_DEALLOCATING) {``//引用计数小于阈值,dealloc`
    
    `do_dealloc = ``true``;`
    
    `it->second |= SIDE_TABLE_DEALLOCATING;`
    
    `} ``else` `if` `(! (it->second & SIDE_TABLE_RC_PINNED)) {`
    
    `//引用计数减去1`
    
    `it->second -= SIDE_TABLE_RC_ONE;`
    
    `}`
    
    `table.unlock();`
    
    `if` `(do_dealloc  &&  performDealloc) {`
    
    `//执行dealloc`
    
    `((void(*)(objc_object *, SEL))objc_msgSend)(``this``, SEL_dealloc);`
    
    `}`
    
    `return` `do_dealloc;`
    
    
    

    release的到这里也比较清楚了:查找map,对引用计数减1,如果引用计数小于阈值,则调用SEL_dealloc

    Autorelease pool

    上文提到了,autorelease方法的作用是把对象放到autorelease pool中,到pool drain的时候,会释放池中的对象。举个例子

    `__weak NSObject * obj;`
    
    `NSObject * temp = [[NSObject alloc] init];`
    
    `obj = temp;`
    
    `NSLog(@``"%@"``,obj); ``//非空`
    
     |
    
    放到auto release pool中,
    
    
    `__weak NSObject * obj;`
    
    `@autoreleasepool {`
    
    `NSObject * temp = [[NSObject alloc] init];`
    
    `obj = temp;`
    
    `}`
    
    `NSLog(@``"%@"``,obj); ``//null`
    
    
    

    可以看到,放到自动释放池的对象是在超出自动释放池作用域后立即释放的。事实上在iOS 程序启动之后,主线程会启动一个Runloop,这个Runloop在每一次循环是被自动释放池包裹的,在合适的时候对池子进行清空。

    对于Cocoa框架来说,提供了两种方式来把对象显式的放入AutoReleasePool.

    • NSAutoreleasePool(只能在MRC下使用)

    • @autoreleasepool {}代码块(ARC和MRC下均可以使用)

    那么AutoRelease pool又是如何实现的呢?

    我们先从autorelease方法源码入手

    `//autorelease方法`
    
    `- (id)autorelease {`
    
    `return` `((id)self)->rootAutorelease();`
    
    `}`
    
    `//rootAutorelease 方法`
    
    `inline id objc_object::rootAutorelease()`
    
    `{`
    
    `if` `(isTaggedPointer()) ``return` `(id)``this``;`
    
    `//检查是否可以优化`
    
    `if` `(prepareOptimizedReturn(ReturnAtPlus1)) ``return` `(id)``this``;`
    
    `//放到auto release pool中。`
    
    `return` `rootAutorelease2();`
    
    `}`
    
    `// rootAutorelease2`
    
    `id objc_object::rootAutorelease2()`
    
    `{`
    
    `assert(!isTaggedPointer());`
    
    `return` `AutoreleasePoolPage::autorelease((id)``this``);`
    
    `}`
    
    
    

    可以看到,把一个对象放到auto release pool中,是调用了AutoreleasePoolPage::autorelease这个方法。

    我们继续查看对应的实现:

    `public: static inline id autorelease(id obj)`
    
    `{`
    
    `assert(obj);`
    
    `assert(!obj->isTaggedPointer());`
    
    `id *dest __unused = autoreleaseFast(obj);`
    
    `assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);`
    
    `return` `obj;`
    
    `}`
    
    `static inline id *autoreleaseFast(id obj)`
    
    `{`
    
    `AutoreleasePoolPage *page = hotPage();`
    
    `if` `(page && !page->full()) {`
    
    `return` `page->add(obj);`
    
    `} ``else` `if` `(page) {`
    
    `return` `autoreleaseFullPage(obj, page);`
    
    `} ``else` `{`
    
    `return` `autoreleaseNoPage(obj);`
    
    `}`
    
    `}`
    
    `id *add(id obj)`
    
    `{`
    
    `assert(!full());`
    
    `unprotect();`
    
    `id *ret = next;  ``// faster than `return next-1` because of aliasing`
    
    `*next++ = obj;`
    
    `protect();`
    
    `return` `ret;`
    
    `}`
    
    
    

    到这里,autorelease方法的实现就比较清楚了,

    autorelease方法会把对象存储到AutoreleasePoolPage的链表里。等到auto release pool被释放的时候,把链表内存储的对象删除。所以,AutoreleasePoolPage就是自动释放池的内部实现。

    __weak与__strong

    用过block的同学一定写过类似的代码:

    
    `__weak typeSelf(self) weakSelf = self;`
    
    `[object fetchSomeFromRemote:^{`
    
    `__strong typeSelf(weakSelf) strongSelf = weakSelf;`
    
    `//从这里开始用strongSelf`
    
    `}];`
    
    
    

    那么,为什么要这么用呢?原因是:

    block会捕获外部变量,用weakSelf保证self不会被block被捕获,防止引起循环引用或者不必要的额外生命周期。

    用strongSelf则保证在block的执行过程中,对象不会被释放掉。

    首先__strong和__weak都是关键字,是给编译器理解的。为了理解其原理,我们需要查看它们编译后的代码,使用XCode,我们可以容易的获得一个文件的汇编代码。

    比如,对于Test.m文件,当源代码如下时:

    `#import "Test.h"`
    
    `@implementation Test`
    
    `- (void)testFunction{`
    
    `{`
    
    `__strong NSObject * temp = [[NSObject alloc] init];`
    
    `}`
    
    `}`
    
    `@end`
    
    
    

    转换后的汇编代码如下:

    `Ltmp3:`
    
    `.loc    2 15 37 prologue_end    ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:37`
    
    `ldr     x9, [x9]`
    
    `ldr     x1, [x8]`
    
    `mov  x0, x9`
    
    `bl  _objc_msgSend`
    
    `adrp    x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGE`
    
    `add x8, x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGEOFF`
    
    `.loc    2 15 36 is_stmt 0       ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:36`
    
    `ldr     x1, [x8]`
    
    `.loc    2 15 36 discriminator 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:36`
    
    `bl  _objc_msgSend`
    
    `mov x8, ``#0`
    
    `add x9, sp, ``#8              ; =8`
    
    `.loc    2 15 29                 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:29`
    
    `str x0, [sp, ``#8]`
    
    `Ltmp4:`
    
    `.loc    2 16 5 is_stmt 1        ; /Users/hl/Desktop/OCTest/OCTest/Test.m:16:5`
    
    `mov  x0, x9`
    
    `mov  x1, x8`
    
    `bl  _objc_storeStrong`
    
    `.loc    2 17 1                  ; /Users/hl/Desktop/OCTest/OCTest/Test.m:17:1`
    
    `ldp x29, x30, [sp, ``#32]     ; 8-byte Folded Reload`
    
    `add sp, sp, ``#48             ; =48`
    
    `ret`
    
    `Ltmp5:`
    
    
    

    即使你不懂汇编,也能很轻易的获取到调用顺序如下

    
    `_objc_msgSend ``// alloc`
    
    `_objc_msgSend ``// init`
    
    `_objc_storeStrong ``// 强引用`
    
    
    

    在结合Runtime的源码,我们看看最关键的objc_storeStrong的实现

    `void objc_storeStrong(id *location, id obj)`
    
    `{`
    
    `id prev = *location;`
    
    `if` `(obj == prev) {`
    
    `return``;`
    
    `}`
    
    `objc_retain(obj);`
    
    `*location = obj;`
    
    `objc_release(prev);`
    
    `}`
    
    `id objc_retain(id obj) { ``return` `[obj retain]; }`
    
    `void objc_release(id obj) { [obj release]; }`
    
    
    

    我们再来看看__weak. 将Test.m修改成为如下代码,同样我们分析其汇编实现

    `.loc    2 15 35 prologue_end    ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:35`
    
    `ldr     x9, [x9]`
    
    `ldr     x1, [x8]`
    
    `mov  x0, x9`
    
    `bl  _objc_msgSend`
    
    `adrp    x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGE`
    
    `add x8, x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGEOFF`
    
    `.loc    2 15 34 is_stmt 0       ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:34`
    
    `ldr     x1, [x8]`
    
    `.loc    2 15 34 discriminator 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:34`
    
    `bl  _objc_msgSend`
    
    `add x8, sp, ``#24             ; =24`
    
    `.loc    2 15 27                 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27`
    
    `mov  x1, x0`
    
    `.loc    2 15 27 discriminator 2 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27`
    
    `str x0, [sp, ``#16]           ; 8-byte Folded Spill`
    
    `mov  x0, x8`
    
    `bl  _objc_initWeak`
    
    `.loc    2 15 27                 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27`
    
    `ldr x1, [sp, ``#16]           ; 8-byte Folded Reload`
    
    `.loc    2 15 27 discriminator 3 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27`
    
    `str x0, [sp, ``#8]            ; 8-byte Folded Spill`
    
    `mov  x0, x1`
    
    `bl  _objc_release`
    
    `add x8, sp, ``#24  `
    
    `Ltmp4:`
    
    `.loc    2 16 5 is_stmt 1        ; /Users/hl/Desktop/OCTest/OCTest/Test.m:16:5`
    
    `mov  x0, x8`
    
    `bl  _objc_destroyWeak`
    
    `.loc    2 17 1                  ; /Users/hl/Desktop/OCTest/OCTest/Test.m:17:1`
    
    `ldp x29, x30, [sp, ``#48]     ; 8-byte Folded Reload`
    
    `add sp, sp, ``#64             ; =64`
    
    `ret`
    
    
    

    可以看到,__weak本身实现的核心就是以下两个方法

    • _objc_initWeak

    • _objc_destroyWeak

    我们通过Runtime的源码分析这两个方法的实现:

    <false></false>

    
    `id objc_initWeak(id *location, id newObj)`
    
    `{`
    
    `//省略....`
    
    `return` `storeWeak        (location, (objc_object*)newObj);`
    `}`
    `void objc_destroyWeak(id *location)`
    `{`
    `(void)storeWeak        (location, nil);`
    `}`
    
    
    

    所以,本质上都是调用了storeWeak函数,这个函数内容较多,主要做了以下事情

    • 获取存储weak对象的map,这个map的key是对象的地址,value是weak引用的地址。

    • 当对象被释放的时候,根据对象的地址可以找到对应的weak引用的地址,将其置为nil即可。

    这就是在weak背后的黑魔法。

    总结

    这篇文章属于想到哪里写到哪里的类型,后边有时间了在继续总结ARC的东西吧。

    相关文章

      网友评论

        本文标题:iOS ARC全解?

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