理解 iOS 的内存管理

引言:

ARC的出生及成长背景

苹果在 2011 年的时候,在 WWDC 大会上提出了自动的引用计数(ARC)。ARC 背后的原理是依赖编译器的静态分析能力,通过在编译时找出合理的插入引用计数管理代码,从而彻底解放程序员。

在 ARC 刚刚出来的时候,业界对此黑科技充满了怀疑和观望,加上现有的 MRC 代码要做迁移本来也需要额外的成本,所以 ARC 并没有被很快接受。直到 2013 年左右,苹果认为 ARC 技术足够成熟,直接将 macOS(当时叫 OS X)上的垃圾回收机制废弃,从而使得 ARC 迅速被接受。

2014 年的 WWDC 大会上,苹果推出了 Swift 语言,而该语言仍然使用 ARC 技术,作为其内存管理方式。

以下是引用唐巧大神的话:

为什么我要提这段历史呢?就是因为现在的 iOS 开发者实在太舒服了,大部分时候,他们根本都不用关心程序的内存管理行为。但是,虽然 ARC 帮我们解决了引用计数的大部分问题,一些年轻的 iOS 开发者仍然会做不好内存管理工作。他们甚至不能理解常见的循环引用问题,而这些问题会导致内存泄漏,最终使得应用运行缓慢或者被系统终止进程。

所以,我们每一个 iOS 开发者,需要理解引用计数这种内存管理方式,只有这样,才能处理好内存管理相关的问题。

ARC 出现之前的 MRC 时代

MRC 时期,前辈们是这样写 iOS 代码的

我们先写好一段 iOS 的代码,然后屏住呼吸,开始运行它,不出所料,它崩溃了。在 MRC 时代,即使是最牛逼的 iOS 开发者,也不能保证一次性就写出完美的内存管理代码。于是,我们开始一步一步调试,试着打印出每个怀疑对象的引用计数(Retain Count),然后,我们小心翼翼地插入合理的 retain 和 release 代码。经过一次又一次的应用崩溃和调试,终于有一次,应用能够正常运行了!于是我们长舒一口气,露出久违的微笑。

引用计数

这里面提到了引用计数,那么什么是引用计数?

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

手动管理引用计数的思考方式:

  • 自己生成的对象,自己持有
  • 非自己生成的对象,自己也能持有
  • 不再需要自己持有的对象时释放
  • 非自己持有的对象无法释放

有了这种思考方式,我们就生成了对应的 Objective-C 方法来管理引用计数。
下表是对象操作与 Objective-C 方法的对应

对象操作 Objective-C 方法 引用计数
生成并持有对象 alloc/new/copy/mutableCopy 等方法 引用计数+1
持有对象 retain 引用计数 +1
释放对象 release 引用计数 -1
废弃对象 dealloc 引用计数 -1

如图,可清晰的看到 对象操作与 Objective-C 方法的对应

对应关系

既然到了这儿,我们也能大概猜到 MRC 下程序员们是如何管理内存的了

在 MRC 模式下,所有的对象都需要手动的添加 retain、release 代码来管理内存。使用 MRC ,需要遵守谁创建,谁回收的原则。也就是谁 alloc ,谁 release ;谁 retain ,谁 release。
当引用计数为0的时候,必须回收,引用计数不为0,不能回收,如果引用计数为0,但是没有回收,会造成内存泄露。如果引用计数为0,继续释放,会造成野指针。为了避免出现野指针,我们在释放的时候,会先让指针= nil。

这块儿先不介绍这几个方法的底层实现,我们只是简单的通过一段简单的代码看看这几个方式是如何进行内存管理的。

我们首先要修改工程设置,给 main.m 加上 -fno-objc-arc 的编译参数,这个参数可以启动手动管理引用计数的模式。
然后,我们先输入如下代码,通过 Log 看到相应的引用计数的变化。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *object = [[NSObject alloc] init]; // 引用计数 +1
        NSLog(@"Reference Count = %lu", (unsigned long)[object retainCount]);
        NSObject *another = [object retain];//引用计数 +1
        NSLog(@"Reference Count = %lu", [object retainCount]);
        [another release];//引用计数 -1
        NSLog(@"Reference Count = %lu", [object retainCount]);
        [object release];// 到这儿,引用计数就为 0 了。
        
    }
    return 0;
}
// 打印的结果为:
2017-05-23 16:11:35.035467+0800 MRC[1148:75832] Reference Count = 1
2017-05-23 16:11:35.041784+0800 MRC[1148:75832] Reference Count = 2
2017-05-23 16:11:35.041806+0800 MRC[1148:75832] Reference Count = 1

为什么需要引用计数

看完上述代码,大家可能会觉得,这就是引用计数啊,这不挺简单的吗?但是,我要告诉大家的,上面那段代码只是非常简单的例子,我们还看不出来引用计数真正的用处。因为该对象的生命期只是在一个函数内,所以在真实的应用场景下,我们在函数内使用一个临时的对象,通常是不需要修改它的引用计数的,只需要在函数返回前将该对象销毁即可。

引用计数真正派上用场的场景在于面向对象的程序设计架构中,用于对象之间传递和共享数据

假如对象 A 生成了一个对象 M,需要调用对象 B 的某一个方法,将对象 M 作为参数传递过去。在没有引用计数的情况下,一般内存管理的原则是 “谁申请谁释放”,那么对象 A 就需要在对象 B 不再需要对象 M 的时候,将对象 M 销毁。但对象 B 可能只是临时用一下对象 M,也可能觉得对象 M
很重要,将它设置成自己的一个成员变量,那这种情况下,什么时候销毁对象 M 就成了一个难题。


对于这种情况,我们可以在对象 A 在调用完对象 B 后直接释放参数对象 M, B 在对参数 M 做一个 Copy ,生成另一个对象 M1,B 自己管理 M1 。


还有一种方法就是对象 A 在构造完对象 M 之后,始终不销毁对象 M,由对象 B 来完成对象 M 的销毁工作。如果对象 B 需要长时间使用对象 M,它就不销毁它,如果只是临时用一下,则可以用完后马上销毁。如果情况在复杂点,出现个对象 C,那么我们的工作是不是就更复杂了呢。


但是上述两种方法要么使得工作量大增,影响性能,要么使得对象间的耦合太过紧密,增大复杂性。

所以,这个时候,我们的引用计数就可以很好的解决这个问题。在参数 M 的传递过程中,哪些对象需要长时间使用这个对象,就把它的引用计数加 1,使用完了之后再把引用计数减 1。所有对象都遵守这个规则的话,对象的生命期管理就可以完全交给引用计数了。我们也可以很方便地享受到共享对象带来的好处。

ARC 下的内存管理

ARC 能够解决 iOS 开发中 90% 的内存管理问题,但是另外还有 10% 内存管理,是需要开发者自己处理的,这主要就是与底层 Core Foundation 对象交互的那部分,底层的 Core Foundation 对象由于不在 ARC 的管理下,所以需要自己维护这些对象的引用计数。

这里我们先抛出 ARC 不能解决的问题:

  • Block 等引发的循环引用问题
  • 底层 Core Foundation 对象需要手动管理

所有权修饰符

ARC 有效时,id 类型和对象类型同 C 语言其他类型不同,其类型上必须附加所有权修饰符。所有权修饰符一共有四种。

  • _strong 修饰符
  • _strong修饰符:id 类型和对象类型默认的所有权修饰符;它可以保证将这些修饰符的自动变量初始化为nil.
  • _strong 修饰符表示对对象的“强引用”; 附有_strong 修饰符的变量之间可以互相赋值。
  • 持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之消失
  • 通过 _strong 修饰符,不必再次键入 retain 和 release
{
    //  ARC 有效时
    id obj = [[NSObject alloc] init];//自己生成并持有对象
    //因为对象obj 强引用,自己也持有对象
}
  <!--//超出作用域,强引用失效,自动释放自己持有的对象-->
{
    //  ARC 无效时,该方法与 ARC 有效时一样
    id obj = [[NSObject alloc] init];//自己生成并持有对象
    [obj release];// 需要自己调用 release 方法来释放
}
  • _weak 修饰符
  • 弱引用,不持有所指向对象的所有权
  • 可以避免循环引用
  • 在持有某对象的弱引用时,若该对象被废弃,则此弱引用将自动失效且处于 nil 被赋值的状态。
// 避免循环引用
__weak __typeof(self) weakSelf = self;
{
    // 自己生成并持有对象
    id _strong obj0 = [NSObject alloc] init];
    // 因为 obj0 变量为强引用,所以自己持有对象
    id _weak obj1 = obj0;
    // obj1 变量持有生成对象的弱引用
}
/*
* 因为 obj0 变量超出其作用域,强引用失效
* 所以自动释放自己持有的对象
* 因为对象的所有者不存在,所以废弃该对象
 */
  • _unsafe_unretained 修饰符
  • 不安全的所有权修饰符,附有 _unsafe_unretained 修饰符的变量不属于编译器测内存管理对象
  • 为兼容iOS5以下版本的产物,可以理解成MRC下的weak
  • 在使用 _unsafe_unretained 修饰符时,赋值给附有 _strong 修饰符的变量时,要确保被赋值的对象确实存在
  • _autoreleasing 修饰符
  • 自动释放对象的引用,一般用于传递参数
  • 在 ARC 有效时,用 @autoreleasepool 块替代 NSAutoreleasePool 类,用附有 _autoreleasing 修饰符的变量替代 autorelease 方法。
  • 当没有显示指定所有权修饰符, id obj 和附有 _strong 修饰符 的obj 是完全一样的。编译器在对象变量超过作用域时,释放它并且自动将它注册到 autoreleasepool 中。
  • 使用 _weak 修饰符的变量时,要访问注册到 autoreleasepool 的对象
  • id 的指针或对象的指针在没有显示指定时会被附加上 _autoreleasing 修饰符
id _weak obj1 = obj0;
NSLog(@"class= %@",[obj1 class]);

上述代码与以下代码相同

id _weak obj1 = obj0;
id _autoreleasing tmp = obj1;
NSLog(@"class= %@",[obj1 class]);

autoreleasepool 范围以块级源代码表示,提高了程序的可读性,所以今后在ARC无效时也推荐使用 @autoreleaseepool 块。
另外,无论 ARC 是否有效,调试用的非公开函数 _objc_autoreleasePoolPrint() 都可使用。
_objc_rootRetainCount(obj)
利用这一函数可有效的帮助我们调试注册到 autoreleasepool 上的对象

ARC 的规则

  • 不能使用 retain/release/retainCount/autorelease
  • 不能使用 NSAllocateObject/NSDeallocateObject
  • 须遵循内存管理的方式命名规则
  • 不要显示调用 dealloc
  • 使用 @autorealeasepool 块代替 NSAutoreleasePool
  • 不要使用区域(NSZone)
  • 对象型变量不能作为 C 语言结构体的成员
  • 显示转换 'id' 和 'void'

循环引用问题

简单的来说循环引用就是对象 A 和对象 B,相互引用了对方作为自己的成员变量,只有当自己销毁时,才会将成员变量的引用计数减 1。因为对象 A 的销毁依赖于对象 B 销毁,而对象 B 的销毁与依赖于对象 A 的销毁,这样就造成了我们称之为循环引用(Reference Cycle)的问题,这两个对象即使在外界已经没有任何指针能够访问到它们了,它们也无法被释放。实际上,多个对象依次持有对方,形式一个环状,也可以造成循环引用问题,而且在真实编程环境中,环越大就越难被发现。

  • 解决循环引用的问题的两个方法
  1. 知道会产生循环引用,在合理的位置主动断开环中的一个引用,使得对象得以回收

主动断开循环引用这种方式常见于各种与 block 相关的代码逻辑中。
但是主动断开循环引用这种操作依赖于程序员自己手工显式地控制,相当于回到了以前 “谁申请谁释放” 的内存管理年代,它依赖于程序员自己有能力发现循环引用并且知道在什么时机断开循环引用回收内存(这通常与具体的业务逻辑相关)

  1. 常见的办法是使用弱引用 (weak reference) 的办法,弱引用虽然持有对象,但是并不增加引用计数,这样就避免了循环引用的产生。在 iOS 开发中,弱引用通常在 delegate 模式中使用。
  • 使用 Xcode 检测循环引用

Core Foundation 对象的内存管理

Core Foundation 对象主要使用在用 C语言编写的 Core Foundation 框架中,并使用引用计数的对象;在 ARC 无效时 ,Core Foundation 框架中的 retain/release 分别是 CFRetain/CFRelease;因为 Core Foundation 对象和 Objective-C 对象没有什么区别,所以在 ARC 无效时,可以使用简单的 C 语言就可以实现互换。

在 ARC 下,我们有时需要将一个 Core Foundation 对象转换成一个 Objective-C 对象,这个时候我们需要告诉编译器,转换过程中的引用计数需要做如何的调整。这就引入了 bridge 相关的关键字,以下是这些关键字的说明:

  • ==__bridge== : 只做类型转换,不修改相关对象的引用计数,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。
  • ==__bridge_retained== :类型转换后,将相关对象的引用计数加 1,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。
  • ==__bridge_transfer== :类型转换后,将该对象的引用计数交给 ARC 管理,Core Foundation 对象在不用时,不再需要调用 CFRelease 方法。

总结

这篇文章并没有涉及 MRC 以及 ARC 实现的底层,所涉及到的知识也是个人看完 高级编程第一章的知识以及 唐巧大神的文章后,自己总结的笔记。在之后的探索中,也会从底层出发来剖析内存管理的知识。

参考博客:唐巧的理解 iOS 内存管理

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

推荐阅读更多精彩内容