iOS ARC全解?

问题
简单介绍 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的东西吧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,093评论 1 32
  • 内存管理 简述OC中内存管理机制。与retain配对使用的方法是dealloc还是release,为什么?需要与a...
    丶逐渐阅读 1,958评论 1 16
  • 29.理解引用计数 Objective-C语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数...
    Code_Ninja阅读 1,477评论 1 3
  • 概述 在iOS中开发中,我们或多或少都听说过内存管理。iOS的内存管理一般指的是OC对象的内存管理,因为OC对象分...
    DamonMok阅读 3,989评论 2 20
  • 焦点网络中级八期 洛阳 杜红平 坚持分享第 160 天 昨天中午,我们学校一行九人和洛龙区的老师们一起坐大巴...
    随喜Prajana阅读 271评论 0 0