·您现在的位置: 江北区云翼计算机软件开发服务部 >> 文章中心 >> 网站建设 >> app软件开发 >> IOS开发 >> iOS/OSX内存管理(1):基本概念与原理
在Objective-C的内存管理中,其实就是引用计数(reference count)
的管理。内存管理就是在程序需要时程序员分配一段内存空间,而当使用完之后将它释放。如果程序员对内存资源使用不当,有时不仅会造成内存资源浪费,甚至会导致程序crach。我们将会从引用计数和内存管理规则等基本概念开始,然后讲述有哪些内存管理方法,最后注意有哪些常见内存问题。
memory management from apple document
为了解释引用计数,我们做一个类比:员工在办公室使用灯的情景。
引用PRo Multithreading and Memory Management for iOS and OS X的图
从上面员工在办公室使用灯的例子,我们对比一下灯的动作与Objective-C对象的动作有什么相似之处:
灯的动作 | Objective-C对象的动作 |
---|---|
开灯 | 创建一个对象并获取它的所有权(ownership) |
使用灯 | 获取对象的所有权 |
不使用灯 | 放弃对象的所有权 |
关灯 | 释放对象 |
因为我们是通过引用计数来管理灯,那么我们也可以通过引用计数来管理使用Objective-C对象。
引用Pro Multithreading and Memory Management for iOS and OS X的图
而Objective-C对象的动作对应有哪些方法以及这些方法对引用计数有什么影响?
Objective-C对象的动作 | Objective-C对象的方法 |
---|---|
1. 创建一个对象并获取它的所有权 | alloc/new/copy/mutableCopy (RC = 1) |
2. 获取对象的所有权 | retain (RC + 1) |
3. 放弃对象的所有权 | release (RC – 1) |
4. 释放对象 | dealloc (RC = 0 ,此时会调用该方法) |
当你alloc
一个对象objc,此时RC=1;在某个地方你又retain
这个对象objc,此时RC加1,也就是RC=2;由于调用alloc/retain
一次,对应需要调用release
一次来释放对象objc,所以你需要release
对象objc两次,此时RC=0;而当RC=0时,系统会自动调用dealloc
方法释放对象。
在开发中,我们常常都会使用到局部变量,局部变量一个特点就是当它超过作用域时,就会自动释放。而autorelease pool跟局部变量类似,当执行代码超过autorelease pool块时,所有放在autorelease pool的对象都会自动调用release
。它的工作原理如下:
NSAutoreleasePool
对象autorelease
方法NSAutoreleasePool
对象
引用Pro Multithreading and Memory Management for iOS and OS X的图
iOS 5/OS X Lion前的(等下会介绍引入ARC的写法)实例代码如下:
1 2 3 4 5 6 7 8 9 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // put object into pool id obj = [[NSObject alloc] init]; [obj autorelease]; [pool drain]; /* 超过autorelease pool作用域范围时,obj会自动调用release方法 */ |
由于放在autorelease pool的对象并不会马上释放,如果有大量图片数据放在这里的话,将会导致内存不足。
1 | for (int i = 0; i |
iOS/OS X内存管理方法有两种:手动引用计数(Manual Reference Counting)和自动引用计数(Automatic Reference Counting)。从OS X Lion和iOS 5开始,不再需要程序员手动调用retain
和release
方法来管理Objective-C对象的内存,而是引入一种新的内存管理机制Automatic Reference Counting(ARC),简单来说,它让编译器来代替程序员来自动加入retain
和release
方法来持有和放弃对象的所有权。
在ARC内存管理机制中,id
和其他对象类型变量必须是以下四个ownership qualifiers其中一个来修饰:
所以在管理Objective-C对象内存的时候,你必须选择其中一个,下面会用一些列子来逐个解释它们的含义以及如何选择它们。
如果我想创建一个字符串,使用完之后将它释放调用,使用MRC管理内存的写法应该是这样:
1 2 3 4 5 | { NSString *text = @"Hello, world"; //@"Hello, world"对象的RC=1 NSLog(@"%@", text); [text release]; //@"Hello, world"对象的RC=0 } |
而如果是使用ARC方式的话,就text
对象无需调用release
方法,而是当text
变量超过作用域时,编译器来自动加入[text release]
方法来释放内存
1 2 3 4 5 6 7 | { NSString *text = @"Hello, world"; //@"Hello, world"对象的RC=1 NSLog(@"%@", text); } /* * 当text超过作用域时,@"Hello, world"对象会自动释放,RC=0 */ |
而当你将text
赋值给其他变量anotherText
时,MRC需要retain
一下来持有所有权,当text
和anotherText
使用完之后,各个调用release
方法来释放。
1 2 3 4 5 6 7 8 9 10 11 | { NSString *text = @"Hello, world"; //@"Hello, world"对象的RC=1 NSLog(@"%@", text); NSString *anotherText = text; //@"Hello, world"对象的RC=1 [anotherText retain]; //@"Hello, world"对象的RC=2 NSLog(@"%@", anotherText); [text release]; //@"Hello, world"对象的RC=1 [anotherText release]; //@"Hello, world"对象的RC=0 } |
而使用ARC的话,并不需要调用retain
和release
方法来持有跟释放对象。
1 2 3 4 5 6 7 8 9 10 | { NSString *text = @"Hello, world"; //@"Hello, world"对象的RC=1 NSLog(@"%@", text); NSString *anotherText = text; //@"Hello, world"对象的RC=2 NSLog(@"%@", anotherText); } /* * 当text和anotherText超过作用域时,会自动调用[text release]和[anotherText release]方法, @"Hello, world"对象的RC=0 */ |
除了当__strong
变量超过作用域时,编译器会自动加入release
语句来释放内存,如果你将__strong
变量重新赋给它其他值,那么编译器也会自动加入release
语句来释放变量指向之前的对象。例如:
1 2 3 4 5 6 7 8 9 | { NSString *text = @"Hello, world"; //@"Hello, world"对象的RC=1 NSString *anotherText = text; //@"Hello, world"对象的RC=2 NSString *anotherText = @"Sam Lau"; // 由于anotherText对象引用另一个对象@"Sam Lau",那么就会自动调用[anotherText release]方法,使得@"Hello, world"对象的RC=1, @"Sam Lau"对象的RC=1 } /* * 当text和anotherText超过作用域时,会自动调用[text release]和[anotherText release]方法, * @"Hello, world"对象的RC=0和@"Sam Lau"对象的RC=0 */ |
如果变量var被
__strong
修饰,当变量var指向某个对象objc,那么变量var持有某个对象objc的所有权
前面已经提过内存管理的四条规则:
Objective-C对象的动作 | Objective-C对象的方法 |
---|---|
1. 创建一个对象并获取它的所有权 | alloc/new/copy/mutableCopy (RC = 1) |
2. 获取对象的所有权 | retain (RC + 1) |
3. 放弃对象的所有权 | release (RC – 1) |
4. 释放对象 | dealloc (RC = 0 ,此时会调用该方法) |
我们总结一下编译器是按以下方法来实现的:
__strong
变量来实现,其实编译器根据__strong
修饰符来管理对象内存。但是__strong
并不能解决引用循环(Reference Cycle)问题:对象A持有对象B,反过来,对象B持有对象A;这样会导致不能释放内存造成内存泄露问题。
引用Pro Multithreading and Memory Management for iOS and OS X的图
举一个简单的例子,有一个类Test
有个属性objc,有两个对象test1和test2的属性objc互相引用test1和test2:
1 2 3 4 5 | @interface Test : NSObject @property (strong, nonatomic) id objc; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | { Test *test1 = [Test new]; /* 对象a */ /* test1有一个强引用到对象a */ Test *test2 = [Test new]; /* 对象b */ /* test2有一个强引用到对象b */ test1.objc = test2; /* 对象a的成员变量objc有一个强引用到对象b */ test2.objc = test1; /* 对象b的成员变量objc有一个强引用到对象a */ } /* 当变量test1超过它作用域时,它指向a对象会自动release * 当变量test2超过它作用域时,它指向b对象会自动release * * 此时,b对象的objc成员变量仍持有一个强引用到对象a * 此时,a对象的objc成员变量仍持有一个强引用到对象b * 于是发生内存泄露 */ |
如何解决?于是我们引用一个__weak
ownership qualifier,被它修饰的变量都不持有对象的所有权,而且当变量指向的对象的RC为0时,变量设置为nil。例如:
1 2 | __weak NSString *text = @"Sam Lau"; NSLog(@"%@", text); |
由于text变量被__weak
修饰,text并不持有@"Sam Lau"
对象的所有权,@"Sam Lau"
对象一创建就马上被释放,并且编译器给出警告⚠️,所以打印结果为(null)
。
所以,针对刚才的引用循环问题,只需要将Test
类的属性objc设置weak修饰符,那么就能解决。
1 2 3 4 5 | @interface Test : NSObject @property (weak, nonatomic) id objc; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 | { Test *test1 = [Test new]; /* 对象a */ /* test1有一个强引用到对象a */ Test *test2 = [Test new]; /* 对象b */ /* test2有一个强引用到对象b */ test1.objc = test2; /* 对象a的成员变量objc不持有对象b */ test2.objc = test1; /* 对象b的成员变量objc不持有对象a */ } /* 当变量test1超过它作用域时,它指向a对象会自动release * 当变量test2超过它作用域时,它指向b对象会自动release */ |
__unsafe_unretained
ownership qualifier,正如名字所示,它是不安全的。它跟__weak
相似,被它修饰的变量都不持有对象的所有权,但当变量指向的对象的RC为0时,变量并不设置为nil,而是继续保存对象的地址;这样的话,对象有可能已经释放,但继续访问,就会造成非法访问(Invalid access)。例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | __unsafe_unretained id obj0 = nil; { id obj1 = [[NSObject alloc] init]; // 对象A /* 由于obj1是强引用,所以obj1持有对象A的所有权,对象A的RC=1 */ obj0 = obj1; /* 由于obj0是__unsafe_unretained,它不持有对象A的所有权,但能够引用它,对象A的RC=1 */ NSLog(@"A: %@", obj0); } /* 当obj1超过它的作用域时,它指向的对象A将会自动释放 */ NSLog(@"B: %@", obj0); /* 由于obj0是__unsafe_unretained,当它指向的对象RC=0时,它会继续保存对象的地址,所以两个地址相同 */ |
打印结果是内存地址相同:
如果将__unsafe_unretained
改为weak
的话,两个打印结果将不同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | __weak id obj0 = nil; { id obj1 = [[NSObject alloc] init]; // 对象A /* 由于obj1是强引用,所以obj1持有对象A的所有权,对象A的RC=1 */ obj0 = obj1; /* 由于obj0是__unsafe_unretained,它不持有对象A的所有权,但能够引用它,对象A的RC=1 */ NSLog(@"A: %@", obj0); } /* 当obj1超过它的作用域时,它指向的对象A将会自动释放 */ NSLog(@"B: %@", obj0); /* 由于obj0是__weak, 当它指向的对象RC=0时,它会自动设置为nil,所以两个打印结果将不同*/ |
引入ARC之后,让我们看看autorelease pool有哪些变化。没有ARC之前的写法如下:
1 2 3 4 5 6 7 8 9 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // put object into pool id obj = [[NSObject alloc] init]; [obj autorelease]; [pool drain]; /* 超过autorelease pool作用域范围时,obj会自动调用release方法 */ |
引入ARC之后,写法比之前更加简洁:
1 2 3 | @autoreleasepool { id __autoreleasing obj = [[NSObject alloc] init]; } |
相比之前的创建、使用和释放NSAutoreleasePool
对象,现在你只需要将代码放在@autoreleasepool
块即可。你也不需要调用autorelease
方法了,只需要用__autoreleasing
修饰变量即可。
引用Pro Multithreading and Memory Management for iOS and OS X的图
但是我们很少或基本上不使用autorelease pool。当我们使用XCode创建工程后,有一个app的入口文件main.m
使用了它:
1 2 3 4 5 | int main(int argc, char * argv[]) { @autoreleasepool { return UIapplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } |
有了ARC之后,新的property modifier也被引入到Objective-C类的property,例如:
1 | @property (strong, nonatomic) NSString *text; |
下面有张表来展示property modifier与ownership qualifier的对应关系
Property modifier | Ownership qualifier |
---|---|
strong | __strong |
retain | __strong |
copy | __strong |
weak | __weak |
assign | __unsafe_unretained |
unsafe_unretained | __unsafe_unretained |
要想掌握iOS/OS X的内存管理,首先要深入理解引用计数(Reference Count)这个概念以及内存管理的规则;在没引入ARC之前,我们都是通过retain
和release
方法来手动管理内存,但引入ARC之后,我们可以借助编译器来帮忙自动调用retain
和release
方法来简化内存管理和减低出错的可能性。虽然__strong
修饰符能够执行大多数内存管理,但它不能解决引用循环(Reference Cycle)问题,于是又引入另一个修饰符__weak
。被__strong
修饰的变量都持有对象的所有权,而被__weak
修饰的变量并不持有对象所有权。下篇我们介绍使用工具如何解决常见内存问题:悬挂指针和内存泄露。
全能程序员交流QQ群290551701,聚集很多互联网精英,技术总监,架构师,项目经理!开源技术研究,欢迎业内人士,大牛及新手有志于从事IT行业人员进入!