概述
-
什么是内存管理
应用程序内存管理是在程序运行时分配内存(比如创建一个对象,会增加内存占用)与清除内存(比如销毁一个对象,会减少内存占用)的过程。
-
为什么要内存管理
由于移动设备的内存极其有限,所以每个APP所占的内存也是有限制的,当APP所占用的内存较多时,系统就会发出内存警告,这时需要回收一些不需要再继续使用的内存空间,比如回收一些不再使用的对象和变量等。如果应用程序所占用的内存超过限制时,便会被系统强制关闭,所以我们需要对应用程序进行内存管理。
-
内存管理的范围
任何继承自NSObject的对象都需要管理内存,基本数据类型int、float、double、char、结构体struct以及枚举enum不需要管理内存。
因为对象和其他数据类型在系统中的存储空间不一样,其它局部变量主要存放于栈中,而对象存储于堆中,当代码块结束时这个代码块中涉及的所有局部变量会被系统回收,指向对象的指针也被系统回收,此时对象已经没有指针指向,但依然存在于内存中,造成内存泄露。
如下面一段代码:
// 在栈内存中找一块区域,命名为a,用它来存放整数2
int a = 2;
// 在栈内存中找一块区域,命名为b,用它来存放整数4
int b = 4;
// 在栈内存中找一块区域,命名为obj,用它来存放指向NSObject *的指针
// 在堆内存中找一块区域,用它来存放NSObject对象,指针变量obj指向NSObject对象的内存地址
NSObject *obj = [[NSObject alloc] init];
在内存中的表现形式如下:
堆栈.png
-
引用计数
在OC中提供了两种管理内存的方式,分别是
MRC(Manual Reference Counting)手动引用计数
和ARC(Automatic Reference Counting)自动引用计数
,在Xcode4.2以前程序员普遍使用MRC方式来管理内存,这也是内存管理的前世
;使用Xcode4.2或以上版本,编译器将自动进行内存管理,也即是ARC,这就是内存管理的今生
。下面将详细介绍MRC和ARC的内容。
MRC
MRC时代需要程序员手动管理对象的生命周期,也就是对象的引用计数由程序员来控制,什么时候retain,什么时候release,完全自己掌握。其实引用计数式内存管理我们也可以这样理解:
- 自己生成的对象,自己所持有
- 非自己生成的对象,自己也能持有
- 不再需要自己持有的对象时释放
- 非自己持有的对象无法释放
这就是引用计数式内存管理的四个法则,这四个法则同样也适用于ARC,在四法则中出现了“生成”、“持有”、“释放”三个词,而在OC内存管理中还要加上“废弃”一词,各个词表示的OC方法如下表所示:
对象操作 | Objective-C方法 | 引用计数 |
---|---|---|
生成并持有对象 | alloc/new/copy/mutableCopy等方法 | +1 |
持有对象 | retain 方法 | +1 |
释放对象 | release 方法 | -1 |
废弃对象 | dealloc方法 | 0 |
下面我们就来看一下在MRC下如何利用四法则进来内存管理。
-
自己生成的对象,自己持有
使用alloc/new/copy/mutableCopy开头的方法名意味着自己生成的对象自己持有
/*
* 自己生成并持有对象
*/
NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [NSObject new];
NSMutableString *str1 = [NSMutableString stringWithString:@"string"];
NSString *str2 = [str1 copy];
NSMutableString *str3 = [str1 mutableCopy];
-
非自己生成的对象,自己也能持有
在项目中我们可以使用类工厂的方法来取得一个对象,因为并不是使用alloc/new/copy/mutableCopy方法取得的,所以自己不是该对象的持有者,比如NSMutableArray类的array类方法,本文所说的“自己”可以理解为编程人员,“非自己”那便可以理解为系统或者编译器。
/*
* 取得非自己生成的对象,也可理解为取得系统生成的对象
*/
id obj = [NSMutableArray array];
/*
* 自己持有对象
*/
[obj retain];
-
不再需要自己持有的对象时释放
自己持有的对象,一旦不再需要,持有者有义务释放该对象,释放使用release方法。
/*
* 自己持有对象
*/
id obj = [[NSObject alloc] init];
/*
* 释放对象
*/
[obj release];
/*
* 指向对象的指针仍就被保留在obj这个变量中
* 但对象已经释放,不可访问
*/
-
无法释放非自己持有的对象
对于用alloc/new/copy/mutableCopy方法生成并持有的对象,或是用retain方法持有的对象,由于持有者是自己,所以在不需要该对象时需要将其释放,而由此以外所得到的对象绝对不能释放,倘若在应用程序中释放了非自己所持有的对象就会造成崩溃。
/*
* 取得非自己生成的对象,也可理解为取得系统生成的对象
* 但自己不持有对象
*/
id obj = [NSMutableArray array];
/*
* 释放了非自己持有的对象
* 导致程序崩溃
*/
[obj release];
/*
* 我们也可以理解为该对象由系统生成由系统去释放
* 谁创建,谁release,谁retain,谁release
*/
以上就是MRC利用四法则进行内存管理的,这里我们再来看一下autorelease在内存管理中的功能。
-
autorelease
autorelease提供了这样的功能,使对象在超出指定的生存范围时能够自动并正确地释放(调用release方法)。我们使用该方法可以使取得的对象存在,但自己不持有对象,比如NSMutableArray类的array类方法就是通过autorelease实现的。
- (id)object {
/*
* 自己生成并持有对象
*/
id obj = [[NSObject alloc] init];
/*
* 把对象注册到自动释放池,交由系统管理,自己不再持有对象
* 当自动释放池结束时该对象自动调用release
*/
[obj autorelease];
return obj;
}
我们在自己书写类工厂方法时,也应该与系统处理方式相同,快速返回一个autorelease对象的方式具体如下:
+ (instancetype)person
{
// 使用self而不是使用Person是因为这样可以在子类调用该方法时会返回子类的对象
return [[[self alloc] init] autorelease];
}
release和autorelease的区别如图所示:
release和autorelease的区别.pngautorelease的具体使用方法如下:
- 生成并持有NSAutoreleasePool对象
- 调用已分配对象的autorelease实例方法
- 废弃NSAutoreleasePool对象
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
NSAutoreleasePool对象的生存周期如图所示:
NSAutoreleasePool对象的生存周期.png
我们可以发现上面的介绍并没有考虑引用计数,只是利用四法则来进行内存管理,其实这背后的本质也就是引用计数式内存管理。我们可以看下面的例子:
/*
* 生成并持有对象,引用计数为1
*/
NSObject *obj0 = [[NSObject alloc] init];
NSLog(@"引用计数 = %lu 对象内存 = %p obj0指针变量内存 = %p", (unsigned long)[obj0 retainCount], obj0, &obj0);
/*
* retain引用计数+1
*/
NSObject *obj1 = [obj0 retain];
NSLog(@"引用计数 = %lu 对象内存 = %p obj0指针变量内存 = %p obj1指针变量内存 = %p", (unsigned long)[obj0 retainCount], obj0, &obj0,&obj1);
/*
* release引用计数-1
*/
[obj1 release];
NSLog(@"引用计数 = %lu 对象内存 = %p obj0指针变量内存 = %p obj1指针变量内存 = %p", (unsigned long)[obj0 retainCount], obj0, &obj0,&obj1);
// 打印结果
2019-08-20 17:10:48.348357+0800 MRC[12405:641180] 引用计数 = 1 对象内存 = 0x6000009646e0 obj0指针变量内存 = 0x7ffee88548b8
2019-08-20 17:10:48.348486+0800 MRC[12405:641180] 引用计数 = 2 对象内存 = 0x6000009646e0 obj0指针变量内存 = 0x7ffee88548b8 obj1指针变量内存 = 0x7ffee88548b0
2019-08-20 17:10:48.348558+0800 MRC[12405:641180] 引用计数 = 1 对象内存 = 0x6000009646e0 obj0指针变量内存 = 0x7ffee88548b8 obj1指针变量内存 = 0x7ffee88548b0
我们可以看到alloc生成对象引用计数+1为1,retain又持有了对象,引用计数再+1为2,release释放对象,引用计数-1变为1。我们能看到[obj1 release]释放后指向对象的指针仍就被保留在obj1这个变量中,只是对象的引用计数-1了而已。
ARC
ARC(Automatic Reference Counting)自动引用计数是编译器的一个特性,能够自动管理OC对象内存生命周期。在ARC中你需要专注于写你的代码,retain 、release、autorelease操作交给编译器去处理就行了,编译器在编译阶段会自动地在适当的位置插入这些代码。这在降低程序崩溃、内存泄漏等风险的同时,很大程度上减少了开发程序的工作量,如此一来,应用程序将具有可预测性,且能流畅运行,速度也将大幅提升。 MRC_ARC_示意图.png还记得上文我们提到的四个法则吗,它们在ARC有效时也是可行的,只是在源代码的书写上稍有不同,到底有什么样的变化呢?下面我们就来看一下ARC有效时编译源代码要遵守的规则。
-
ARC源代码编译规则
- 不能使用retain/release/retainCount/autorelease
- 不要显式调用dealloc,也即是可以实现dealloc方法,用于释放除了实例变量以外的其他资源,不需要调用[super dealloc],编译器会自动调用
/* ARC无效 */
- (void)dealloc {
/* 该对象用的处理 */
[super dealloc];
}
/* ARC有效 */
- (void)dealloc {
/*
* 此处运行该对象被废弃时
* 必须实现的代码
*/
}
- 使用@autoreleasepool块替代NSAutoreleasePool来创建自动释放池
- 须遵守内存管理的方法命名规则,不能声明以new开头的属性,除非为该属性定义一个新的getter名称
// 错误
@property NSString *newTitle;
// 正确
@property (getter=theNewTitle) NSString *newTitle;
我们知道在MRC中我们通过调用retain/release方法使对象的引用计数+1和-1,对象得到了很好的管理,那在ARC中是如何管理内存的呢?下面我们就介绍一下ARC中追加的所有权修饰符。
-
所有权修饰符
Objective-C编程中为了处理对象,可将变量类型定义为id类型或各种对象类型。 ARC有效时,id类型和对象类同C语言其他类型不同,其类型必须附加所有权修饰符。所有权修饰符一共4种:
- __strong修饰符
- __weak修饰符
- __unsafe_unretained修饰符
- __autoreleasing修饰符
__strong修饰符
__strong修饰符是id类型和对象类型默认的所有权修饰符。也就是说,以下源代码中id变量,实际上被附加了所有权修饰符。
id obj = [[NSObject alloc] init];
等同于以下源代码
id __strong obj = [[NSObject alloc] init];
__strong修饰符表示对对象的“强引用”,持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放。
{
/*
* 因为变量obj为强引用
* 所以自己持有对象
*/
id __strong obj = [[NSObject alloc] init];
}
/*
* 因为变量obj超出其作用域,强引用失效
* 所以自动地释放自己持有的对象
* 对象的所有者不存在,因此废弃该对象
*/
等同于ARC无效时以下代码
/* ARC无效 */
{
id obj = [[NSObject alloc] init];
[obj release];
}
在取得非自己生成自己也不持有的对象时会如何呢?
{
/*
* 取得非自己生成的对象
* 因为变量obj为强引用
* 所以自己持有对象
*/
id __strong obj = [NSMutableArray array];
}
/*
* 因为变量obj超出其作用域,强引用失效
* 所以自动地释放自己持有的对象
*/
等同于ARC无效时以下代码
/* ARC无效 */
{
id obj = [NSMutableArray array];
[obj retain];
[obj release];
}
__strong修饰符的变量不仅只在变量作用域中,在赋值上也能够正确地管理其对象的所有者,且看下面的代码
/*
* obj0持有对象A的强引用
*/
id __strong obj0 = [[NSObject alloc] init]; /* 对象A */
/*
* obj1持有对象B的强引用
*/
id __strong obj1 = [[NSObject alloc] init]; /* 对象B */
/*
* obj2不持有任何对象
*/
id __strong obj2 = nil;
/*
* obj0持有由obj1赋值的对象B的强引用
* 因为obj0被赋值,所以原先持有的对对象A的强引用失效
* 对象A的所有者不存在,因此废弃对象A
*
* 此时,持有对象B的强引用的变量为obj0和obj1
*/
obj0 = obj1;
/*
* obj2持有由obj0赋值的对象B的强引用
*
* 此时,持有对象B的强引用的变量为obj0,obj1和obj2
*/
obj2 = obj0;
/*
* 因为nil被赋予了obj1,所以对对象B的强引用失效
*
* 此时,持有对象B的强引用的变量为obj0和obj2
*/
obj1 = nil;
/*
* 因为nil被赋予了obj0,所以对对象B的强引用失效
*
* 此时,持有对象B的强引用的变量为obj2
*/
obj0 = nil;
/*
* 因为nil被赋予了obj2,所以对对象B的强引用失效
* 对象B的所有者不存在,因此废弃对象B
*/
obj2 = nil;
当然,即便是Objective-C类成员变量,也可以在方法参数上使用附有__strong修饰符的变量,且看下面代码
@interface Test : NSObject
{
id __strong obj_;
}
- (void)setObject:(id __strong)obj;
@end
@implementation Test
- (void)setObject:(id __strong)obj
{
obj_ = obj;
}
@end
// 使用Test类
{
/*
* test持有Test对象的强引用
*/
id __strong test = [[Test alloc] init];
/*
* Test对象的obj_成员变量持有NSObject对象的强引用
*/
[test setObject:[[NSObject alloc] init]];
}
/*
* 因为test变量超出其作用域,强引用失效
* 所以自动释放Test对象
* Test对象的所有者不存在,因此废弃该对象
*
* 废弃Test对象的同时
* Test对象的obj_成员变量也被废弃
* NSObject对象的强引用失效
* 自动释放NSObject对象
* NSObject对象的所有者不存在,因此废弃该对象
*/
另外,__strong修饰符同后面要介绍的__weak修饰符和__autoreleasing修饰符一起,可以保证将附有这些修饰符的局部变量初始化为nil。
id __strong obj0;
id __weak obj1;
id __autoreleasing obj2;
以下源代码与上相同
id __strong obj0 = nil;
id __weak obj1 = nil;
id __autoreleasing obj2 = nil;
__weak修饰符
貌似通过__strong修饰符编译器就能够完美地进行内存管理,但是并不能解决引用计数式内存管理中的“循环引用”的问题。下面代码就是一个循环引用。
@interface Test : NSObject
{
id __strong obj_;
}
- (void)setObject:(id __strong)obj;
@end
@implementation Test
- (void)setObject:(id __strong)obj
{
obj_ = obj;
}
@end
// 循环引用
{
/*
* test0持有Test对象A的强引用
*/
id test0 = [[Test alloc] init]; /* 对象A */
/*
* test1持有Test对象B的强引用
*/
id test1 = [[Test alloc] init]; /* 对象B */
/*
* Test对象A的obj_成员变量持有Test对象B的强引用
*
* 此时,持有Test对象B的强引用变量为Test对象A的obj_和test1
*/
[test0 setObject:test1];
/*
* Test对象B的obj_成员变量持有Test对象A的强引用
*
* 此时,持有Test对象A的强引用变量为Test对象B的obj_和test0
*/
[test1 setObject:test0];
}
/*
* 因为test0变量超出其作用域,强引用失效
* 所以自动释放Test对象A
*
* 因为test0变量超出其作用域,强引用失效
* 所以自动释放Test对象B
*
* 此时,持有Test对象A的强引用的变量为Test对象B的obj_
*
* 此时,持有Test对象B的强引用的变量为Test对象A的obj_
*
* Test对象A和Test对象B没有被废弃,发生内存泄漏
*
*/
__weak修饰符与__strong修饰符相反,提供弱引用,弱引用不能持有对象实例,使用_weak修饰符可以避免循环引用。把上面的代码成员变量obj用__weak修饰就可以了。
@interface Test : NSObject
{
id __weak obj_;
}
- (void)setObject:(id __strong)obj;
@end
__weak修饰符还有另一个优点,在持有某对象的弱引用时,若该对象被废弃,则此弱引用将自动失效且处于nil被赋值的状态(空指针),因此通过检查附有__weak修饰符的变量是否为nil可以判断被赋值的对象是否已废弃。
__unsafe_unretained修饰符
__unsafe_unretained修饰符正如其名unsafe所示,是不安全的所有权修饰符,既不持有对象的强引用也不持有弱引用,若该修饰符所修饰的变量表示的对象被废弃,该变量不会被置为nil,变成了野指针,访问野指针会崩溃。
id __unsafe_unretained obj1 = nil;
{
/*
* 因为obj0变量为强引用
* 所以自己持有对象
*/
id __strong obj0 = [[NSObject alloc] init];
/*
* 虽然obj0变量赋值给obj1
* 但是obj1变量既不持有对象的强引用也不持有弱引用
*/
obj1 = obj0;
NSLog(@"A: %@",obj1);
}
/*
* 因为obj0变量超出其作用域,强引用失效
* 所以自动释放自己持有的对象
* 因为对象无持有者,所以废弃该对象
*/
/*
* 输出obj1变量表示的对象
* obj1变量表示的对象已经被废弃
* 访问野指针,程序崩溃
*/
NSLog(@"B: %@",obj1);
我们可以看到__weak修饰符的变量,当所表示的对象被废弃时会置为nil, __unsafe_unretained修饰符的变量不会置为nil,这就是二者的区别。__weak修饰符只能用于iOS5以上版本的应用程序,所以在iOS4的应用程序中必须使用__unsafe_unretained修饰符替代__weak修饰符。
__autoreleasing修饰符
将对象赋值给附有__autoreleasing修饰符的变量等同于MRC时调用对象的autorelease方法。
// ARC有效
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
等同于下面代码
// ARC无效
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
可以理解为在ARC有效时用@autoreleasepool块替代NSAutoreleasePool类,用附有__autoreleasing修饰符的变量替代autorelease方法
@autoreleasepool和附有__autoreleasing修饰符的变量.png
我们在介绍MRC的时候讲到使用类工厂方式取得的对象,编译器会自动将对象注册到autoreleasepool中,这在ARC中也是一样的,编译器会检查方法名是否已alloc/new/copy/mutableCopy开始,如果不是则自动将返回值的对象注册到autoreleasepool。如下面代码:
@autoreleasepool {
/*
* 因为变量obj为强引用,所以自己持有对象
* 该对象由编译器判断其方法名后,自动注册到autoreleasepool
*/
id __strong obj = [NSMutableArray array];
}
/*
* 因为变量obj超出其作用域,强引用失效
* 所以自动释放自己持有的对象
*
* 同时,随着@autoreleasepool块的结束,
* 注册到autoreleasepool中的所有对象被自动释放
* 因为对象的所有者不存在,所以废弃对象
*
*/
综上,我们会发现在ARC中不再观察对象的引用计数是否为0,只用考虑该对象是否有强指针指向,只要有一个强指针指向该对象,对象的所有者就不为0,就会一直存在于内存中。当对象的所有者不存在,对象就会被废弃,内存得以回收。
所以,直接使用__weak或者__unsafe_unretained修饰变量指向一个刚创建的对象时,由于对象没有强指针指向,对象的所有者不存在,该对象会立即被释放。
// 因为没有强指针指向该对象,该对象会立即被释放
NSString * __weak string0 = [[NSString alloc] initWithFormat:@"Hello"];
// 因为没有强指针指向该对象,该对象会立即被释放
NSString * __unsafe_unretained string1 = [[NSString alloc] initWithFormat:@"Hello"];
// 编译器给出警告
warning: Assigning retained object to weak variable; object will be released after assignment
-
属性修饰符
属性中内存管理的修饰符与所有权修饰符的对应关系如下表:
属性修饰符 | 所有权修饰符 | 用法 |
---|---|---|
assgin | __unsafe_unretained修饰符 | 适用于基本数据类型 |
retain | __strong修饰符 | 适用于OC对象 |
copy | __strong修饰符(但是赋值的是被复制的对象) | 适用于NSString和block |
strong | __strong修饰符 | 适用于OC对象 |
weak | __weak修饰符 | 适用于OC对象(避免循环引用) |
其中strong
和weak
是ARC中新增的两个属性修饰符,在MRC中属性的默认修饰符是assgin
,在ARC中默认是strong
。strong用于OC对象,相当于MRC中retain,weak用于OC对象,相当于MRC中的assgin,assgin用于基本数据类型,相当于MRC中的assgin。
/*
* 下面这句对于strong的示例
* 与此同义: @property(retain) MyClass *myObject;
*/
@property(strong) MyClass *myObject;
/*
* 下面这句对于weak的示例
* 与此相似: @property(assign) MyClass *myObject;
*/
@property(weak) MyClass *myObject;
/*
* 使用assign修饰的变量所指向的对象被释放,该指针会变成野指针
* 使用weak修饰的变量所指向的对象被释放,该指针会变成空指针
*/
如果想详细了解这几个属性修饰符可以看我的另一篇文章,iOS属性的修饰符(assign、retain、copy、weak、strong)
内存管理检测
未完待续。。。
小结
本文主要是对《iOS与OS X多线程和内存管理》这本书第一章自动引用计数的总结,结合自己的理解,由于本人知识和技术水平有限,文中不乏一些错误和不规范之处,欢迎大家留言批评指正。
参考文献
《iOS与OS X多线程和内存管理》书籍
可能是史上最全面的内存管理文章
iOS里的内存管理
iOS性能调优之--内存管理
网友评论