前言
通过这段时间的学习,对Objective-C的内存管理知识做一个总结。分享给大家,如有理解错误的地方,还望多指正。
总结从以下几个方面来说明:
- 引用计数器
- ARC(Automatic Reference Counting):自动引用计数
- 循环引用问题
- 自动释放池
autorelease pool
正文
1 引用计数器
1.理解引用计数器
说到引用计数器,有着iOS开发经验的同行一定知道,Objective-C语言内存管理的核心就是引用计数器。
简单来说,每个OC对象都有一个引用计数器,如果想使某个对象继续存在内存中,那就使其引用计数增加1,如果该对象使用结束,我们不希望它继续存在于内存中,那就使其引用计数减少1(,当该对象的引用计数等于0时候,系统收回该对象的内存。
- 引用计数器的工作原理
NSObject协议下声明了一下三个方法用于操作引用计数器:
-
retain
递增引用计数; -
release
递减引用计数; -
autorelease
待稍后清理引用释放池
时,再递减引用计数。
查看当前引用计数的方法是retainCount
,[obj retainCount]
。
对象创建出来的时候,其引用计数至少为1。若想让它继续存活,则调用retain
方法,若该对象不再被使用,则调用release
或者autorelease
方法。当引用计数为0时候,该对象占用的内存将会被回收。引用计数器的工作原理大概如此。
2 ARC(Automatic Reference Counting):自动引用计数
- 自动引用计数(ARC)的理解
在ARC出现前,开发者使用引用计数需要记住何时使用retain
、release
和autorelease
,而ARC的诞生就是为了解决这个问题。ARC省去了开发者在代码中调用retain
、release
和autorelease
精力,取代了开发者内存管理的工作。 - ARC的工作原理
Xcode的Clang编译器带有一个静态分析器
,用于检测程序中引用计数有问题的地方。例如:
if (true) {
id obj = [[SomeClass alloc] init];
[obj doSometing];
}
这段代码在MRC环境下就会出问题,因为if
条件外obj
没有被释放,此处会发生内存泄漏。静态分析器
做的就是检测这样的错误。
既然静态分析器
可以做到这些,那么可以应用这个功能,提前在程序中加入retain
、release
等操作。ARC的工作原理,就是使用了这一功能。
在ARC下,代码经过编译后,会自动为源代码添加上相对应的操作。所以在ARC下,retain
,release
,autorelease
都是不允许被使用的,否则会产生编译错误。
3.ARC的一些tips
- ARC在调用
retain
,release
,autorelease
方法的时候,并不走Objctive-C的消息派发机制,而是直接调用底层的C语言方法。这样做提升了性能,也是retain
,release
,autorelease
方法不能被重写的原因。 - OC语法有非常严格的命名规则,以下列单词开头的方法名:
new
alloc
copy
mutableCopy
。若方法返回对象,则ARC不会为返回的对象加上autorelease
,否则会在返回对象前为其加上autorelease
。 - 除了自动的调用
retain
,release
,autorelease
之外,ARC还能够把互相抵消的retain
,release
,autorelease
操作简化。若某个对象上重复多次的进行了‘retain’和‘release’操作,那么ARC有时可以成对的抵消这两个操作。 - ARC也包含运行期组件。当它检测到某方法返回对象前,为其执行了
autorelease
操作,之后该对象还要执行retain
操作,那ARC就会删除这一对操作。具体方式为,在返回对象前,不直接调用autorelease
方法,而是调用objc_autoreleaseReturnValue
函数,此函数会检测当前方法返回之后即将要执行的代码,若发现那段代码要在返回对象上执行retain
操作,则设立一个flag不再执行原有的autorelease
操作。同理,若方法返回一个自动释放的对象,而该对象需要被保留,那么不直接执行retain
,而是改为执行objc_retainAutoreleasedReturnValue
函数,此函数检测刚才设立的那个flag,若已经设置,则不执行retain
操作。
objc_autoreleaseReturnValue
和objc_retainAutoreleasedReturnValue
两个函数的实现必须通过查看机器码指令才可以判断,所以是由编译器的开发者完成的。 - 用以下修饰符修饰变量时的一些语义:
__strong
:默认语义,保留此值;
__unsafe_unretained
:不保留此值;
__weak
:不保留此值,但是变量可以安全使用,如果系统回收了该对象,那么这个变量也会被自动清空;
__autoreleasing
:在方法调用时,使用这个修饰参数值,在方法返回后,该值自动释放。 - ARC环境下,当对象被回收时,实例变量的回收问题:ARC会使用
Objctive-C++
的一项特性来清理实例变量。回收Objective-C++
对象时,待回收对象会调用所有C++对象的析构函数。编译器如果发现某个对象有C++对象,就会生成名为.cxx_destruct
的方法。ARC借助此特性,在该方法中生成清理内存所需要的代码。 -
CoreFoundation
对象需要手动管理内存,不归ARC管理,开发者必须自己调动CFRetain
/CFRelease
。
3 循环引用问题
- 循环引用的理解
如果A对象强引用了B对象,而B对象也强引用了A对象,这就是最简单的循环引用,两个对象间的互相引用。当系统要回收对象A时,由于A引用了对象B,所以对象B也需要被释放,而此时B又强引用了A,如此一来,两个对象都不能够释放,继续存活于内存中,就会出现内存泄漏。这就是循环引用的问题。 - 循环引用问题的解决
解决循环引用问题的最佳方式就是 弱引用。用unsafe_unretained
或者weak
修饰属性。在语义上unsafe_unretained
和assign
等价,区别于assign
用于修饰通用类型的属性,比如int
,float
和结构体
等,而unsafe_unretained
用于修饰对象。
例如对象A强引用了对象B,那么对象B如果弱引用了对象A,就不会出现以上的问题。当系统回收对象A时,对象B会被回收,而B对A的弱引用不会造成循环引用,所以不会出现内存泄漏的问题。
weak
等价于unsafe_unretained
,它们的不同主要表现在被修饰的属性被释放后的行为不同。当用unsafe_unretained
修饰的属性被回收后,该属性任然指向那个被回收的属性,而weak
则指向nil。使用weak
会使程序更加安全一些。 -
block
使用中的循环引用问题
这个问题在很多的技术文章中被提到过,这里也做个简单的说明。
例如:
//DemoViewController.m
@interface DemoViewController ()
@property (nonatomic, copy) void (^testBlock) (void);
@end
@implementation DemoViewController
...
- (void)viewDidLoad {
[super viewDidLoad];
[self test];
}
- (void)test {
self.testBlock = ^(){
[self doSometing];
}
}
...
@end
以上这段代码,由于testBlock
块中捕获了self
,所以testBlock
强引用了self
,而同时self
强引用着testBlock
,如此就形成了循环引用,有内存泄漏的风险。
打破这种保留的方式很简单,使用__weak
定义一个新的weakSelf
供testBlock
捕获。如下:
//DemoViewController.m
@interface DemoViewController ()
@property (nonatomic, copy) void (^testBlock) (void);
@end
@implementation DemoViewController
...
- (void)viewDidLoad {
[super viewDidLoad];
[self test];
}
- (void)test {
__weak typeof(self) weakSelf = self;
self.testBlock = ^(){
[weakSelf doSometing];
}
}
...
@end
关于block
的知识点总结,会在后面整理一份。
4 自动释放池autorelease pool
- 自动释放池的认识
在ARC中,自动释放池(autorelease pool
)是一项重要的特性。
当某个对象调用release
时,会理解递减引用计数retainCount
。如果换做调用autorelease
,则对象会被加入autorelease pool
中,当清空自动释放池autorelease pool
时,会向其中的对象发送release
消息。 - 自动释放池的使用
使用自动释放池的语法如下:
//使用语法
@autoreleasepool{
//...
}
一般情况下,系统创建的主线程或者GCD机制中的线程,都会默认创建自己的自动释放池,每次执行 事件循环 时,就会将其清空。因此,不需要自己来创建 自动释放池块。
应用程序的入口int main()
函数处,就为我们手动创建了应用程序的自动释放池。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
所以通常情况下我们无需自己创建自动释放池。当某些临时产生的对象导致应用程序的内存峰值过高时,我们可以通过创建自动释放池,来解决这个问题。
例如:
NSMutableArray *objsArray = [NSMutableArray array];
for (int i = 0; i < 10000; i ++) {
id obj = [self createSomeObjcWithi:i];
[objsArray addObject:obj];
}
以上代码就会造成程序的内存突然暴增,而等所有obj
对象都释放以后,又突然下降。
此时,增加一个自动释放池代码块即可解决这个问题:
NSMutableArray *objsArray = [NSMutableArray array];
for (int i = 0; i < 10000; i ++) {
@atuoreleasepool {
id obj = [self createSomeObjcWithi:i];
[objsArray addObject:obj];
}
}
这样一来,应用程序在执行循环时,就会有效降低内存峰值,不像原来那么高。
创建自动释放池本身也会占用一定的内存,所以是否使用自动释放池完全取决于程序本身。
关于自动释放池Draveness大神的自动释放池的前世今生 ---- 深入解析 Autoreleasepool有详细的解析。
总结
内存管理是应用程序的灵魂,虽然在ARC环境下,我们可以尽量少的投入精力在内存管理上,但是了解其中的原理和机制,会让我们在程序出问题时找到有效的解决途径,更是提高自我的一种方式。
当然,内存管理涉及到的也不止文中提到的内容,还有很多需要挖掘的地方。
文章是本人看书学习中的总结,主要用于知识巩固,顺便和大家分享交流,如果有不妥的地方,欢迎指正。