从 2016 年开始 iOS 开发到现在,几乎所有的项目里都只使用 ARC,偶尔遇到历史遗留的少数文件用到 MRC,但因为不需要修改就直接忽略,也觉得 MRC 这么古董的内存管理方式即使学会了也没有用武之地。但是最近想法有了改变,觉得使用某个框架编程,不仅需要能够熟练使用 API 来实现工程需求,而且需要了解其底层的运作机制。因为当项目做深入了需要优化的时候,就需要了解底层的原理来对项目提出解决方案。而且了解底层对某些时候的 debug 效率有很大的提升。我不想只停留在浅层次的工具使用技能,也想更多了解工具的原貌、设计与运作逻辑。
iOS 的内存管理机制
iOS 通过对象的指针引用数来管理其在内存中的持有与释放。因此每一个运行起来的应用,其内存的对象都构成了一幅对象图(object graph),开发者的一个很重要的职责就是管理好对象图,以保证应用在有限的内存分配中高效运行,避免内存泄露(memory leak)等问题的发生。
![](https://img.haomeiwen.com/i1857041/15ce4853821ad060.png)
iOS 的内存管理机制与 Android 的垃圾回收机制(garbage collection)不同。
- 垃圾回收机制:通过后台进程,从应用入口开始扫描完整的对象引用树来得知哪些对象已不被引用,从而销毁它们。
- 好处:让程序员卸下繁琐且易出错的内存管理重担,而更专注在项目的业务逻辑的开发上。
- 缺点:需要运行后台进程并消耗计算性能,某些时候对应用的性能影响较大。
- 引用数管理机制:当对象的引用数为零时就会被系统释放,否则将被保留
- 好处:机制简单,对应用性能影响较小
- 缺点:容易出错,例如内存泄露,最常见是循环引用(reference cycle),需要额外的开发者劳动成本来避免
在这种引用数管理机制下,粗略学习了 MRC、ARC 与 @autoreleasepool 的概念与常用情况,以下作对应介绍。
MRC
全称 Manual Reference Counting, 是需要开发者手动标记对象以决定其在内存中的命运的运行机制。管理通过使用 retain
, release
, 以及 autorelease
的消息发送来实现。
retain
: 持有(拥有)对象,对象引用数加 1
release
: 释放对象,对象引用数减 1
autorelease
: 通知系统,在 @autoreleasepool 代码块结束时,对对象调用 release
这里首先描述 Cocoa 框架的 4 条内存管理原则:
-
自己创建的对象,自己获得拥有权
自己创建的对象指通过alloc
,new
,copy
,mutableCopy
创建的对象,例如:
Foo *foo = [[Foo alloc] init];
// foo 对象使用中...
[foo release]; // 释放内存
- 别人创建的对象,可以通过
retain
来获得拥有权
// 例如已有 fooArray, 通过 array 方法获得其引用
NSArray *bar = [fooArray array];
// 不可以直接调用 release,因为没有拥有权
[bar release];
// 需要先 retain 来获得拥有权,然后才能释放
[bar retain];
[bar release];
-
你所拥有的对象不再需要使用时,必须将其释放
-
不能释放你不拥有的对象
见上,第 2 点例子
ARC
此处的 A 就是 automatic。其实 ARC 只是比 MRC 多了一步,就是在编译时编译器自动帮开发者添加 retain
, release
以及 autorelease
的调用,底层的内存管理机制还是和 MRC 一样。
在 ARC 模式下,我们通常在对象变量的声明里用属性标记符来指引 ARC 机制来管理我们的对象变量,它们是:strong
, retain
, weak
, copy
, assign
。默认标记是 strong
标记符的区别
-
strong: 顾名思义,就是强引用,对应 MRC 下的
retain
,即引用数加 1 -
retain: 同
strong
-
weak: 弱引用,不增加引用数,引用的对象被释放后变为
nil
-
copy: 对对象进行 copy 后再赋值,因此对象必须遵循
NSCopying
协议。如:
@property(copy) Foo *foo;
...
self.foo = bar; // 相当于 self.foo = [bar copy];
-
assign: 一般用于原始数据类型(primitive type)的赋值。可以用于对象,效果相当于
weak
,可是有一个坑是当对象被释放后,assign
属性的变量不会变成nil
,而是成为野指针(dangling pointer),因此不建议使用在对象上。
借助以上的属性标记符,我们可以在对象声明的时候集中制定它们的内存管理策略,清晰明了。
@autoreleasepool
@autoreleasepool 其实就是显式化的 ARC,让开发者能够更精细地指引系统管理对象内存。但是既然有了 ARC,我们为什么还需要烦心去精细化操作呢?Apple 给出的其中一个 使用场景 就是降低内存使用峰值(reduce peak memory footprint)。假设这么一个使用场景:
在一个循环中,需要创建临时对象,而在 iOS 内存管理机制下,临时对象在循环结束时才会被释放。因此如果循环多次运行时,临时对象的内存占用就会快速累积,造成一个内存占用峰值。在这种情况下,我们就可以使用 @autoreleasepool
来及时释放对象。如以下官方例子:
NSArray *urls = <# An array of file URLs #>;
for (NSURL *url in urls) {
@autoreleasepool {
NSError *error;
NSString *fileContents = [NSString stringWithContentsOfURL:url
encoding:NSUTF8StringEncoding error:&error];
/* Process the string, creating and autoreleasing more objects. */
}
}
如此在 autoreleasepool
的代码块中,每次用完一个 url
,临时创建的 fileContents
以及 error
(如有)就会在代码块结束时获得释放。
@autoreleasepool
怎么知道代码块中的哪些对象需要被释放?秘密就是在代码块中收到 autorelease
消息的对象(包括通过 alloc
, new
等方法创建或者显式发送 autorelease
)会在代码块结束时收到 release
消息。因此上述例子的 fileContents
在被创建时编译器隐式添加了 autorelease
,在代码块结束时被对应发送 release
。
网友评论