问题
简单介绍 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这个数据结构来存储引用计数。我们看看这个数据结构的实现:
可以看到,这个数据结构就是存储了一个自旋锁,一个引用计数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的东西吧。