有关 iOS 内存管理的文章相信你们都看过很多了,我本身也同样。不过网上大部分文章都没有解决对于内存管理的一些关键性的疑惑,对于初学者来讲并非很友好。本文旨在从初学者的角度出发,对 iOS 内存管理的关键部分进行介绍,但愿能对广大 iOS 学习者有所帮助。html
本文首发于我发起的 笔试面试知识整理 项目,这里是对应的 Github 仓库,由多人协做编辑而成。如发现有错误的地方,随时欢迎指正。ios
在 Objective-C 中,对象一般是使用 alloc
方法在堆上建立的。 [NSObject alloc]
方法会在对堆上分配一块内存,按照NSObject
的内部结构填充这块儿内存区域。git
一旦对象建立完成,就不可能再移动它了。由于极可能有不少指针都指向这个对象,这些指针并无被追踪。所以没有办法在移动对象的位置以后更新所有的这些指针。程序员
Objective-C中提供了两种内存管理机制:MRC(MannulReference Counting)和 ARC(Automatic Reference Counting),分别提供对内存的手动和自动管理,来知足不一样的需求。如今苹果推荐使用 ARC 来进行内存管理。github
对象操做 | OC中对应的方法 | 对应的 retainCount 变化 |
---|---|---|
生成并持有对象 | alloc/new/copy/mutableCopy等 | +1 |
持有对象 | retain | +1 |
释放对象 | release | -1 |
废弃对象 | dealloc | - |
注意:面试
这些对象操做的方法其实并不包括在OC中,而是包含在Cocoa框架下的Foundation框架中。objective-c
对象的 reatinCount
属性并无实际上的参考价值,参考苹果官方文档《Practical Memory Management》.express
本身生成的对象,本身持有。app
非本身生成的对象,本身也能持有。框架
不在须要本身持有的对象的时候,释放。
非本身持有的对象没法释放。
以下是四个黄金法则对应的代码示例:
/* * 本身生成并持有该对象 */ id obj0 = [[NSObeject alloc] init]; id obj1 = [NSObeject new];
/* * 持有非本身生成的对象 */ id obj = [NSArray array]; // 非本身生成的对象,且该对象存在,但本身不持有 [obj retain]; // 本身持有对象
/* * 不在须要本身持有的对象的时候,释放 */ id obj = [[NSObeject alloc] init]; // 此时持有对象 [obj release]; // 释放对象 /* * 指向对象的指针仍就被保留在obj这个变量中 * 但对象已经释放,不可访问 */
/* * 非本身持有的对象没法释放 */ id obj = [NSArray array]; // 非本身生成的对象,且该对象存在,但本身不持有 [obj release]; // 此时将运行时crash 或编译器报error
其中 非本身生成的对象,且该对象存在,但本身不持有
这个特性是使用autorelease
来实现的,示例代码以下:
- (id) getAObjNotRetain { id obj = [[NSObject alloc] init]; // 本身持有对象 [obj autorelease]; // 取得的对象存在,但本身不持有该对象 return obj; }
autorelease
使得对象在超出生命周期后能正确的被释放(经过调用release方法)。在调用 release
后,对象会被当即释放,而调用 autorelease
后,对象不会被当即释放,而是注册到 autoreleasepool
中,通过一段时间后 pool
结束,此时调用release方法,对象被释放。
在MRC的内存管理模式下,与对变量的管理相关的方法有:retain, release 和 autorelease。retain 和 release 方法操做的是引用记数,当引用记数为零时,便自动释放内存。而且能够用 NSAutoreleasePool 对象,对加入自动释放池(autorelease 调用)的变量进行管理,当 drain 时回收内存。
ARC 是苹果引入的一种自动内存管理机制,会根据引用计数自动监视对象的生存周期,实现方式是在编译时期自动在已有代码中插入合适的内存管理代码以及在 Runtime 作一些优化。
在ARC中与内存管理有关的变量标识符,有下面几种:
__strong
__weak
__unsafe_unretained
__autoreleasing
__strong
是默认使用的标识符。只有还有一个强指针指向某个对象,这个对象就会一直存活。
__weak
声明这个引用不会保持被引用对象的存活,若是对象没有强引用了,弱引用会被置为 nil
__unsafe_unretained
声明这个引用不会保持被引用对象的存活,若是对象没有强引用了,它不会被置为 nil。若是它引用的对象被回收掉了,该指针就变成了野指针。
__autoreleasing
用于标示使用引用传值的参数(id *),在函数返回时会被自动释放掉。
变量标识符的用法以下:
Number* __strong num = [[Number alloc] init];
注意 __strong
的位置应该放到 *
和变量名中间,放到其余的位置严格意义上说是不正确的,只不过编译器不会报错。
类中的属性也能够加上标志符:
@property (assign/retain/strong/weak/unsafe_unretained/copy) Number* num
assign
代表 setter 仅仅是一个简单的赋值操做,一般用于基本的数值类型,例如CGFloat
和NSInteger
。
strong
代表属性定义一个拥有者关系。当给属性设定一个新值的时候,首先这个值进行 retain
,旧值进行 release
,而后进行赋值操做。
weak
代表属性定义了一个非拥有者关系。当给属性设定一个新值的时候,这个值不会进行 retain
,旧值也不会进行 release
, 而是进行相似 assign
的操做。不过当属性指向的对象被销毁时,该属性会被置为nil。
unsafe_unretained
的语义和 assign
相似,不过是用于对象类型的,表示一个非拥有(unretained)的,同时也不会在对象被销毁时置为nil的(unsafe)关系。
copy
相似于 strong
,不过在赋值时进行 copy
操做而不是 retain
操做。一般在须要保留某个不可变对象(NSString最多见),而且防止它被意外改变时使用。
若是咱们给一个原始类型设置 strong\weak\copy
,编译器会直接报错:
Property with 'retain (or strong)' attribute must be of object type
设置为 unsafe_unretained
却是能够经过编译,只是用起来跟 assign
也没有什么区别。
反过来,咱们给一个 NSObject 属性设置为 assign,编译器会报警:
Assigning retained object to unsafe property; object will be released after assignment
正如警告所说的,对象在赋值以后被当即释放,对应的属性也就成了野指针,运行时跑到属性有关操做会直接崩溃掉。和设置成 unsafe_unretained
是同样的效果(设置成 weak
不会崩溃)。
unsafe_unretained
的用处unsafe_unretained
差很少是实际使用最少的一个标识符了,在使用中它的用处主要有下面几点:
兼容性考虑。iOS4 以及以前尚未引入 weak
,这种状况想表达弱引用的语义只能使用 unsafe_unretained
。这种状况如今已经不多见了。
性能考虑。使用 weak
对性能有一些影响,所以对性能要求高的地方能够考虑使用 unsafe_unretained
替换 weak
。一个例子是 YYModel 的实现,为了追求更高的性能,其中大量使用 unsafe_unretained
做为变量标识符。
当两个对象互相持有对方的强引用,而且这两个对象的引用计数都不是0的时候,便形成了引用循环。
要想破除引用循环,能够从如下几点入手:
注意变量做用域,使用 autorelease
让编译器来处理引用
使用弱引用(weak)
当实例变量完成工做后,将其置为nil
Autorelase Pool 提供了一种能够容许你向一个对象延迟发送release
消息的机制。当你想放弃一个对象的全部权,同时又不但愿这个对象当即被释放掉(例如在一个方法中返回一个对象时),Autorelease Pool 的做用就显现出来了。
所谓的延迟发送release
消息指的是,当咱们把一个对象标记为autorelease
时:
NSString* str = [[[NSString alloc] initWithString:@"hello"] autorelease];
这个对象的 retainCount 会+1,可是并不会发生 release。当这段语句所处的 autoreleasepool 进行 drain 操做时,全部标记了 autorelease
的对象的 retainCount 会被 -1。即 release
消息的发送被延迟到 pool 释放的时候了。
在 ARC 环境下,苹果引入了 @autoreleasepool
语法,再也不须要手动调用 autorelease
和 drain
等方法。
在 ARC 下,咱们并不须要手动调用 autorelease 有关的方法,甚至能够彻底不知道 autorelease 的存在,就能够正确管理好内存。由于 Cocoa Touch 的 Runloop 中,每一个 runloop circle 中系统都自动加入了 Autorelease Pool 的建立和释放。
当咱们须要建立和销毁大量的对象时,使用手动建立的 autoreleasepool 能够有效的避免内存峰值的出现。由于若是不手动建立的话,外层系统建立的 pool 会在整个 runloop circle 结束以后才进行 drain,手动建立的话,会在 block 结束以后就进行 drain 操做。详情请参考苹果官方文档。一个广泛被使用的例子以下:
for (int i = 0; i < 100000000; i++) { @autoreleasepool { NSString* string = @"ab c"; NSArray* array = [string componentsSeparatedByString:string]; } }
若是不使用 autoreleasepool ,须要在循环结束以后释放 100000000 个字符串,若是
使用的话,则会在每次循环结束的时候都进行 release 操做。
如上面所说,系统在 runloop 中建立的 autoreleaspool 会在 runloop 一个 event 结束时进行释放操做。咱们手动建立的 autoreleasepool 会在 block 执行完成以后进行 drain 操做。须要注意的是:
当 block 以异常(exception)结束时,pool 不会被 drain
Pool 的 drain 操做会把全部标记为 autorelease 的对象的引用计数减一,可是并不意味着这个对象必定会被释放掉,咱们能够在 autorelease pool 中手动 retain 对象,以延长它的生命周期(在 MRC 中)。
你们都知道在 iOS 程序的 main.m 文件中有相似这样的语句:
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
在面试中问到有关 autorelease pool 有关的知识也多半会问一下,这里的 pool 有什么做用,能不能去掉之类。在这里咱们分析一下。
根据苹果官方文档, UIApplicationMain 函数是整个 app 的入口,用来建立 application 对象(单例)和 application delegate。尽管这个函数有返回值,可是实际上却永远不会返回,当按下 Home 键时,app 只是被切换到了后台状态。
同时参考苹果关于 Lifecycle 的官方文档,UIApplication 本身会建立一个 main run loop,咱们大体能够获得下面的结论:
main.m 中的 UIApplicationMain 永远不会返回,只有在系统 kill 掉整个 app 时,系统会把应用占用的内存所有释放出来。
由于(1), UIApplicationMain 永远不会返回,这里的 autorelease pool 也就永远不会进入到释放那个阶段
在 (2) 的基础上,假设有些变量真的进入了 main.m 里面这个 pool(没有被更内层的 pool 捕获),那么这些变量实际上就是被泄露的。这个 autorelease pool 等因而把这种泄露状况给隐藏起来了。
UIApplication 本身会建立 main run loop,在 Cocoa 的 runloop 中实际上也是自动包含 autorelease pool 的,所以 main.m 当中的 pool 能够认为是没有必要的。
在基于 AppKit 框架的 Mac OS 开发中, main.m 当中就是不存在 autorelease pool 的,也进一步验证了咱们获得的结论。不过由于咱们看不到更底层的代码,加上苹果的文档中不建议修改 main.m ,因此咱们也没有理由就直接把它删掉(亲测,删掉以后不影响 App 运行,用 Instruments 也看不到泄露)。
若是一个函数的返回值是指向一个对象的指针,那么这个对象确定不能在函数返回以前进行 release,这样调用者在调用这个函数时获得的就是野指针了,在函数返回以后也不能马上就 release,由于咱们不知道调用者是否是 retain 了这个对象,若是咱们直接 release 了,可能致使后面在使用这个对象时它已经成为 nil 了。
为了解决这个纠结的问题, Objective-C 中对对象指针的返回值进行了区分,一种叫作 retained return value,另外一种叫作 unretained return value。前者表示调用者拥有这个返回值,后者表示调用者不拥有这个返回值,按照“谁拥有谁释放”的原则,对于前者调用者是要负责释放的,对于后者就不须要了。
按照苹果的命名 convention,以 alloc
, copy
, init
, mutableCopy
和 new
这些方法打头的方法,返回的都是 retained return value,例如 [[NSString alloc] initWithFormat:]
,而其余的则是 unretained return value,例如 [NSString stringWithFormat:]
。咱们在编写代码时也应该遵照这个 convention。
咱们分别在 MRC 和 ARC 状况下,分析一下两种返回值类型的区别。
在 MRC 中咱们须要关注这两种函数返回类型的区别,不然可能会致使内存泄露。
对于 retained return value,须要负责释放
假设咱们有一个 property 定义以下:
@property (nonatomic, retain) NSObject *property;
在对其赋值的时候,咱们应该使用:
self.property = [[[NSObject alloc] init] autorelease];
而后在 dealloc 方法中加入:
[_property release]; _property = nil;
这样内存的状况大致是这样的:
init 把 retain count 增长到 1
赋值给 self.property ,把 retain count 增长到 2
当 runloop circle 结束时,autorelease pool 执行 drain,把 retain count 减为 1
当整个对象执行 dealloc 时, release 把 retain count 减为 0,对象被释放
能够看到没有内存泄露发生。
若是咱们只是使用:
self.property = [[NSObject alloc] init];
这一条语句会致使 retain count 增长到 2,而咱们少执行了一次 release,就会致使 retain count 不能被减为 0 。
另外,咱们也可使用临时变量:
NSObject * a = [[NSObject alloc] init]; self.property = a; [a release];
这种状况,由于对 a 执行了一次 release,全部不会出现上面那种 retain count 不能减为 0 的状况。
注意:如今你们基本都是 ARC 写的比较多,会忽略这一点,可是根据上面的内容,咱们看到在 MRC 中直接对 self.proprety 赋值和先赋给临时变量,再赋值给 self.property,确实是有区别的!我在面试中就被问到这一点了。
咱们在编写本身的代码时,也应该遵照上面的原则,一样是使用 autorelease:
// 注意函数名的区别 + (MyCustomClass *) myCustomClass { return [[[MyCustomClass alloc] init] autorelease]; // 须要 autorelease } - (MyCustomClass *) initWithName:(NSString *) name { return [[MyCustomClass alloc] init]; // 不须要 autorelease }
对于 unretained return value,不须要负责释放
当咱们调用非 alloc,init 系的方法来初始化对象时(一般是工厂方法),咱们不须要负责变量的释放,能够当成普通的临时变量来使用:
NSString *name = [NSString stringWithFormat:@"%@ %@", firstName, lastName]; self.name = name // 不须要执行 [name release]
在 ARC 中咱们彻底不须要考虑这两种返回值类型的区别,ARC 会自动加入必要的代码,所以咱们能够放心大胆地写:
self.property = [[NSObject alloc] init]; self.name = [NSString stringWithFormat:@"%@ %@", firstName, lastName];
以及在本身写的函数中:
+ (MyCustomClass *) myCustomClass { return [[MyCustomClass alloc] init]; // 不用 autorelease }
这些写法都是 OK 的,也不会出现内存问题。
为了进一步理解 ARC 是如何作到这一点的,咱们能够参考 Clang 的文档。
对于 retained return value, Clang 是这样作的:
When returning from such a function or method, ARC retains the value at the point of evaluation of the return statement, before leaving all local scopes.
When receiving a return result from such a function or method, ARC releases the value at the end of the full-expression it is contained within, subject to the usual optimizations for local values.
能够看到基本上 ARC 就是帮咱们在代码块结束的时候进行了 release:
NSObject * a = [[NSObject alloc] init]; self.property = a; //[a release]; 咱们不须要写这一句,由于 ARC 会帮咱们把这一句加上
对于 unretained return value:
When returning from such a function or method, ARC retains the value at the point of evaluation of the return statement, then leaves all local scopes, and then balances out the retain while ensuring that the value lives across the call boundary. In the worst case, this may involve an autorelease, but callers must not assume that the value is actually in the autorelease pool.
ARC performs no extra mandatory work on the caller side, although it may elect to do something to shorten the lifetime of the returned value.
这个和咱们以前在 MRC 中作的不是彻底同样。ARC 会把对象的生命周期延长,确保调用者能拿到而且使用这个返回值,可是并不必定会使用 autorelease,文档写的是在 worst case 的状况下才可能会使用,所以调用者不能假设返回值真的就在 autorelease pool 中。从性能的角度,这种作法也是能够理解的。若是咱们可以知道一个对象的生命周期最长应该有多长,也就没有必要使用 autorelease 了,直接使用 release 就能够。若是不少对象都使用 autorelease 的话,也会致使整个 pool 在 drain 的时候性能降低。
众所周知,weak 不会持有对象,当给一个 weak 赋以一个本身生成的对象(即上面提到的 retained return value)后,对象会立马被释放。
一个很常见的 warning 就是 Assigning retained object to weak variable, object will be released after assignment.
可是咱们前面也提到了,能够持有非本身生成的对象,这经过 autorelease 实现。
那么若是一个 weak 被赋以一个非本身生成的对象(即上面提到的 unretained return value)呢?代码以下:
NSNumber __weak *number = [NSNumber numberWithInt:100]; NSLog(@"number = %@", number);
这种状况下是能够正确打印值的。
clang的文档 是这么说的:这种状况下,weak 并不会当即释放,而是会经过 objc_loadWeak
这个方法注册到 AutoreleasePool 中,以延长生命周期。
为了解决这个问题,首先让咱们理清楚属性是个什么存在。属性(property) 实际上就是一种语法糖,每一个属性背后都有实例变量(Ivar)作支持,编译器会帮咱们自动生成有关的 setter 和 getter,对于下面的 property:
@interface Counter : NSObject @property (nonatomic, retain) NSNumber *count; @end;
生成的 getter 和 setter 相似下面这样:
- (NSNumber *)count { return _count; } - (void)setCount:(NSNumber *)newCount { [newCount retain]; [_count release]; // Make the new assignment. _count = newCount; }
Property 这部分对于 MRC 和 ARC 都是适用的。
有了这部分基础,咱们再来理解一下把属性置为 nil 这个步骤。首先要明确一点,在 MRC 下,咱们并非真的把属性置为 nil,而是把 Ivar 置为 nil。
[_property release]; _property = nil;
若是用 self.property 的话还会调用 setter,里面可能存在某些不该该在 dealloc 时运行的代码。
对于 ARC 来讲,系统会自动在 dealloc 的时候把全部的 Ivar 都执行 release,所以咱们也就没有必要在 dealloc 中写有关 release 的代码了。
在上面有关 property 的内容基础上,咱们知道用:
self.property = nil
实际上就是手动执行了一次 release。而对于临时变量来讲:
NSObject *object = [[NSObject alloc] init]; object = nil;
置为 nil 这一句其实没什么用(除了让 object 在下面的代码里不能再使用以外),由于上面咱们讨论过 ,ARC 下的临时变量是受到 Autorelease Pool 的管理的,会自动释放。
由于 ARC 下咱们不能再使用 release 函数,把变量置为 nil 就成为了一种释放变量的方法。真正须要咱们把变量置为 nil 的,一般就是在使用 block 时,用于破除循环引用:
MyViewController * __block myController = [[MyViewController alloc] init…]; // ... myController.completionHandler = ^(NSInteger result) { [myController dismissViewControllerAnimated:YES completion:nil]; myController = nil; };
在 YTKNetwork 这个项目中,也能够看到相似的代码:
- (void)clearCompletionBlock { // nil out to break the retain cycle. self.successCompletionBlock = nil; self.failureCompletionBlock = nil; }
上面提到对于 unretained return value, ARC “并不必定会使用 autorelease”,下面具体解释一下。
ARC 所作的事情并不只仅局限于在编译期找到合适的位置帮你插入合适的 release
等等这样的内存管理方法,其在运行时期也作了一些优化,以下是两个优化的例子:
合并对称的引用计数操做。好比将 +1/-1/+1/-1 直接置为 0.
巧妙地跳过某些状况下 autorelease
机制的调用。
其中第二个优化,是 ARC 针对 autorelease
返回值提供的一套优化策略,大致的流程以下:
当方法所有基于 ARC 实现时,在方法 return 的时候,ARC 会调用 objc_autoreleaseReturnValue()
以替代 MRC 下的 autorelease
。在 MRC 下须要 retain 的位置,ARC 会调用 objc_retainAutoreleasedReturnValue()
。所以下面的 ARC 代码:
+ (instancetype)createSark { return [self new]; } // caller Sark *sark = [Sark createSark];
实际上会被改写成相似这样:
+ (instancetype)createSark { id tmp = [self new]; return objc_autoreleaseReturnValue(tmp); // 代替咱们调用autorelease } // caller id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替咱们调用retain Sark *sark = tmp; objc_storeStrong(&sark, nil); // 至关于代替咱们调用了release
有了这个基础,ARC 可使用一些优化技术。在调用 objc_autoreleaseReturnValue()
时,会在栈上查询 return address 以肯定 return value 是否会被直接传给 objc_retainAutoreleasedReturnValue()
。 若是没传,说明返回值不能直接从提供方发送给接收方,这时就会调用 autorelease
。反之,若是返回值能顺利的从提供方传送给接收方,那么就会直接跳过 autorelease
过程,而且修改 return address 以跳过 objc_retainAutoreleasedReturnValue()
过程,这样就跳过了整个 autorelease
和 retain
的过程。
核心思想:当返回值被返回以后,紧接着就须要被 retain 的时候,没有必要进行 autorelease + retain,直接什么都不要作就行了。
另外,当函数的调用方是非 ARC 环境时,ARC 还会进行更多的判断,在这里再也不详述,详见 《黑幕背后的 Autorelease》。
Instrument 为咱们提供了 Allocations/Leaks 这样好用的工具用来检测 memory leak 的工具。以下是内存泄露的两种类型:
Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).
Abandoned memory: Memory still referenced by your application that has no useful purpose.
其中 Leaks 工具主要用来检测 Leaked memory,在 MRC 时代 程序员会常常忘记写 release 方法致使内存泄露,在 ARC 时代这种已经不太常见。(ARC时代 主要的Leaked Memory 来自于底层 C 语言以及 一些由 C 写成的底层库,每每会由于忘记手工 free 而致使 leak )。
Allocations 工具主要用来检测 Abandoned memory. 主要思路是在一个时间切片内检测对象的声明周期以观察内存是否会无限增加。经过 hook 掉 alloc,dealloc,retain,release 等方法,来记录对象的生命周期。
https://stackoverflow.com/questions/9784762/strong-weak-retain-unsafe-unretained-assign
https://stackoverflow.com/questions/29350634/ios-autoreleasepool-in-main-and-arc-alloc-release
https://stackoverflow.com/questions/6588211/why-do-the-ios-main-m-templates-include-a-return-statement-and-an-autorelease-po
https://stackoverflow.com/questions/2702548/if-the-uiapplicationmain-never-returns-then-when-does-the-autorelease-pool-get
https://stackoverflow.com/questions/6055274/use-autorelease-when-setting-a-retain-property-using-dot-syntax
https://stackoverflow.com/questions/17601274/arc-and-autorelease
https://stackoverflow.com/questions/8292060/arc-equivalent-of-autorelease
https://stackoverflow.com/questions/7906804/do-i-set-properties-to-nil-in-dealloc-when-using-arc
http://wereadteam.github.io/2016/02/22/MLeaksFinder/?from=singlemessage&isappinstalled=0
http://clang.llvm.org/docs/AutomaticReferenceCounting.html#arc-runtime-objc-loadweak