第五章 内存管理
内存管理对一门语言来说异常的重要,掌握一门语言的内存管理是很必要的。
29. 理解引用计数
OC采用引用计数进行内存管理,就是说,每个对象有一个计数器,如果想让对象不被内存管理回收,就要保证对象的引用计数大于等于1,如果引用计数变为0则表示这个对象不再被需要,其占用的内存就可以回收了。
经过不断的发展,现在OC的引用计数已经不用手动管理,而是由编译器帮助我们执行(自动引用计数ARC),不过还是要了解引用计数,在某些情况下对于写代码还是很有用的。
下面只简单介绍一下关于引用计数的一些东西,不做过深的解释,首先对象在创建出来的时候引用计数为1,OC中直接令引用计数增加用retain方法,领引用计数减少用release和autorelease,autorelease调用后不会立即将引用计数减1,而是将对象放入“自动释放池”,稍后在做处理。在非ARC下,可以通过retainCount方法查看引用计数是几,但是这种方法是不推荐的,因为这个方法返回的数字可能和实际数字不符,后面介绍为什么。前面介绍的这几个方法在ARC下使用会报错的,如果想体验一下手动管理可以通过下图步骤修改编译环境
在一个应用程序中会同时存在许多对象,这些对象之间也是可以存在持有关系的,并且可以多个对象同时持有一个对象,这就导致一个对象会因为持有者的变化而导致引用计数不断变化,当引用计数变为0,这个对象就可以摧毁了。大家可能会有一个疑问,对象之间是持有关系存在的,那么最顶端的对象是谁,在iOS中这个对象是UIApplication对象,这个对象是应用程序启动时创建的单例。另外如果不是手动将对象的内存置为空的话,引用计数变为0的对象也不一定被摧毁,只不过这个对象占用的内存被放回“可用内存池”,当这部分内存没有被覆写的时候,这个对象仍然有效,这时候可能会出现我们意想不到的bug,所以一般情况下如果确定不用一个对象的话,要对这个对象指针进行处理(将其置为空)。
下面介绍几个和内存管理息息相关的知识:
1.属性存取方法中的内存管理
一般情况下实现一个对象对一个对象的持有都是通过访问属性来实现的,这时候会用到相关属性的设置方法和获取方法,这时候属性的内存管理语义的关键字就显得格外重要,例如strong关系,就是强引用,如果一个属性被strong引用,则其设置方法可能会是下面这样:
- (void)setFoo:(id)foo{
[foo retain];
[_foo release];
_foo = foo;
}
该方法会保留新值释放旧值,当然在ARC下是不允许这样写的,我们要理解属性内存管理语义的重要性。
2.自动释放池
自动释放池的最大用处就是能延长对象生命期,尤其是在方法返回对象时候更应该用他,通过autorelease的调用,对象不会被立即释放,而是等到下次“事件循环”才执行引用计数递减工作,在这期间就会留给我们足够长的时间对对象进行相关的操作,相比较于release不会那么强硬,但是各有各的好处。另外自动释放池也会有降低内存峰值的作用,后面介绍。
3.保留环
就是循环引用,就是两个对象互相持有(也可能多个对象之间成环式的持有),这就导致所有对象的引用计数为1,最后谁也不能释放,对于不同情况下产生的保留环会有不同的处理方法,后面会介绍一种“弱引用”方法解决,也可以从外界命令环中的一个对象不再对另外一个对象进行持有。
30. 以ARC简化引用计数
ARC的出现时程序员的福音,因为再也不用为考虑引用计数发愁了,所有这些应该增加或减少引用计数的地方都由编译器的“静态分析器”帮我们解决,所以这也是为什么我们不能再ARC下调用reatin、release、autorelease、dealloc方法的原因,因为手动调用这些方法会干扰编译器的判断。另外,编译器的引用计数不是通过普通的消息派发机制,而是通过更底层的方法,这样更能提高代码的效率。我们要知道一点,ARC下还是有引用计数机制,只不过这个工作被编译器做了。
使用ARC时要遵循方法命名规则,因为编译器在分析法代码的时候会根据方法名分析代码,例如,若方法名以alloc、new、copy、mutableCopy开头,则其返回的对象归调用者所有,那么这部分代码就要负责释放方法所返回的对象。若方法名不以这四个词开头,则返回对象不归调用者所有,这种个情况下返回兑现会制自动释放,不过现在这些工作都由ARC帮我们做了。ARC还会对操作约减,将retain和release相互抵消,这样都会优化代码,节省内存。另外,ARC还有运行期组件,这些操作都会大大的优化我们的程序,具体的不过多介绍,至于这些优化的详细内部实现,只有编译器的作者知道。
ARC也会处理局部变量和实例变量的内存管理,ARC会以一种安全的方式来设置一个变量,他总是遵循先保留新值,再释放旧值,最后设置实例变量。在应用程序中,可以用下面的修饰符来改变局部变量与实例变量的语义:
__strong: 默认语义,保留此值
__unsafe_unretained: 不保留此值(这么做不安全,因为再次使用的时候对象可能已经被回收)
__weak: 不保留此值,但是变量可以安全使用,在某些情况下这个修饰符很有用
__autoreleasing: 把对象“按引用传递”给方法时,此修饰符会让此值在方法返回时自动释放
block会自动保留其捕获的全部对象,如果这些对象中有一个对象又保留了block本身,那么就会导致保留环,这时候就可以用__weak修饰局部变量来打破这种保留环,避免循环引用。
ARC可以自动的帮助我们清理实例变量,并且ARC的清理会比我们手动在dealloc方法中release更高效,ARC会用C++对象的析构函数,不过有一些非OC对象在调用的时候就要手动清理,这些框架都有对应的清理方法,例如CoreFoundation框架中的对象或是由malloc()分配在对中的内存,这时候需要我们在dealloc中调用对应的方法手动释放。如下
- (void)dealloc{
CFRelease(_coreFoundationObject);
free(_heapAllocatedMemoryBlob);
}
另外,由于ARC的自动引用计数机制,内存管理方法是不可以覆写的,我们要相信ARC会给我们更好的代码优化。
31. 在dealloc方法中只释放引用并解除监听
dealloc是一个对象生命周期中执行的最后一个方法,这个方法执行后,对象将不复存在,所以,在这个方法中我们就要考虑清除所有关于这个对象的痕迹,前面已经提到过,ARC下编辑器会自动在这个方法中添加适当的方法,解除对象的引用,对于不属于OC的对象,也应该在这个方法中释放,另外dealloc方法还有一个重要的用处就是把原来配置的观测行为都清理掉,最典型的就是移除通知的观察者:
- (void)dealloc{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
我们要谨记,不能再别的方法中调用dealloc方法,调用后后续的代码都将失效,另外我们也要遵循一个规则,就是在dealloc中不要调用其他的和释放无关的方法。
有时候,一些开销较大或系统稀缺支援的释放需要我们自行设定释放方法在合适的时候调用,例如文件描述符、套接字、大块内存等,这些东西都需要我们自行编写释放方法,至于有的时候可能在程序运行到一半的时候就退出了,这个时候还没有来的及走dealloc方法,大家可以不用关心这个问题,程序退出,程序中的对象也会销毁,另外我们可以通过UIApplicationDelegate中的applicationWillTerminate
方法,在程序退出之前做我们想要做的事情。
32. 编写“异常安全代码”时留意内存管理问题
OC和C++都支持异常,并且两门语言的异常是互通的,也就是说从一门语言里抛出的异常能用另外一门语言编辑的异常处理程序来捕获。OC中,异常的抛出应该是在极其错误的情况下,前面的错误模型已经介绍,但是有时候第三方库中也会用到异常,这时候就需要我们进行处理。
通常我们使用事务来处理一段异常,在非ARC情况下可以做如下处理:
EOCSomeClass *object;
@try {
object = [[EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
} @catch (NSException *exception) {
NSLog(@"有一个异常要处理");
} @finally {
[object release];
}
在非ARC的情况下我们可以通过@finally块,把最后的引用释放,但是在ARC下怎么处理呢?下面是ARC下相同功能的代码:
EOCSomeClass *object;
@try {
object = [[EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
} @catch (NSException *exception) {
NSLog(@"有一个异常要处理");
}
这时候不能写release方法,所以@finally块可以去掉,但是抛出异常的时候引用没有被释放啊,我们可能以为编辑器会给我们做处理,其实不是,默认情况下编辑器是不会给我们做处理的,我们需要通过-fobjc-arc-exceptions这个编译标志来开启这个功能,这时候编译器会帮我们生成安全处理异常的附加代码(附加代码的代码量还是很大的),这个功能默认不开启有两个原因,第一个就是前面说的附加码代码量很大,另外一个原因就是系统任务如果抛出异常就是很严重的错误,可以直接终止程序了,所以系统默认是不开启的。另外说一点,如果编译器发现我们编写的是Objective-C++代码,会默认开启这个状态,因为C++中会频繁使用异常。
33. 以弱引用避免保留环
保留环就是我们常说的循环引用,简单的保留环是两个对象之间互相引用,复杂的保留环可能是三个或者更多对象之间的呈链式的循环引用,前面已经介绍了,可以在声明名属性的时候使用assign、unsafe_unretained、weak三个修饰符解决保留环的问题,下面介绍一下这三个修饰符的不同点。
assign: 通常只用于整形类型(int、float、结构体等)
unsafe_unretained: 通常用于对象
weak:通常用于对象
举个例子介绍unsafe_unretained和weak区别,现系统持有两个对象,对象A和对象B,并且对象A和对象B互相持有,当系统销毁对象A之后,如果是用weak修饰,那么对象B指向对象A的引用就不存在了,这时候对象B指向nil,如果是用unsafe_unretained修饰,在系统销毁对象A之后,对象B指向对象A的引用依然存在,由此可以看出unsafe_unretained是很不安全的,所以可以发现,基本我们看不到unsafe_unretained修饰的属性,一般情况下都是使用weak修饰。
34. 以“自动释放池块”降低内存峰值
OC中的自动释放池是引用计数机制中的一项特性,前面已经介绍,自动释放池中对象的释放是等到一次循环结束,而不是像release一样马上释放,这样就留下充足的时间处理这些对象,创建自动释放池用@autoreleasepool,不过在一般情况下我们无需担心自动释放池的创建问题,因为系统会在他认为需要的地方自动创建一些自动释放池,例如主线程或者GCD机制中的线程。下面介绍几个和自动释放池有关的知识点,大家会发现,我们创建好一个工程后,main函数总是这样写的:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
其实从技术角度看,这个自动释放池可有可无,因为等到要执行自动释放操作的时候已经是程序的结尾了,那么为什么还要加呢,如果不加的话UIApplicationMain函数所自动释放的那些对象,就没有自动释放池容纳了,所以,这个释放池可以理解成最外围捕捉全部自动释放对象所用的池。
再看下面的代码:
@autoreleasepool {
// 做一些操作
@autoreleasepool {
// 做一些操作
}
}
从上面可以发现自动释放池可以嵌套使用,这样做最大的好处是可以不用等到最外面的释放池执行释放操作,内部的释放池可以先执行释放操作,自动释放池的范围的由大括号决定的,这样做可以降低内存峰值,再看下面一个例子:
for (int i = 0; i < 1000000; i++) {
@autoreleasepool {
// 执行创建对象操作
}
}
从上面可以看出这个制动释放池的作用就是不用等到循环结束再执行释放操作,如果等到循环结束的时候再执行释放操作的话,内存中创建的对象就太多了,这种情况在我们从数据库中提取数据的时候会经常发生,对于数据库中的数据我们是不知道多少的,如果我们一下查询出过多的数据,这个时候如果要把数据转换成对象就会出现上面的问题,这个时候自动释放池就派上用场了。
是否进行自动释放池优化,完全是根据应用本身决定的,在ARC出现之前,有一种老式的制动释放池写法,就是NSAutoreleasePool对象,这里不再介绍,感兴趣可以自行查阅,总的来说这个对象更加重量级,而现在的@autoreleasePool更加轻便好用。
35. 用“僵尸对象”调试内存管理问题
如果向被回收的对象发送消息有时会造成崩溃,但是这种崩溃又不是一定的,为什么会出现这种情况呢,前面已经介绍,被回收的对象占用的内存没有清空,只不过这一部分内存放入可用内存区域,如果还没有被覆写,那么对象仍然是存在的,这就是为什么有时候崩溃有时候不崩溃的原因,想要排查很困难,这时候就轮到僵尸对象上场了,当开启僵尸模式后,运行期系统不会把回收的对象放到可用内存区域,而是将回收对象变成僵尸对象,这样所有的信息都被僵尸对象接收,系统的设定是僵尸对象在收到消息后,会抛出异常,并在抛出的信息中准确的描述发送过来的信息,并且描述了是哪个对象变成了现在的僵尸对象,这就是大致的僵尸模式的工作模式,下面介绍一下如何开启僵尸模式
在僵尸模式抛出的异常信息中就有相关的对象的信息,例如有一个类EOCClass,在僵尸模式中抛出异常的时候我们会看到_NSZombie_EOCClass,从抛出的异常信息就可以追查出问题所在,有助于解决问题。
ARC模式下,由于内存不用手动管理,会很少出现僵尸对象,但容易产生上述问题的场景主要有两个:一是方法内的局部对象,在其他方法使用; 二是异步过程的回调,比如网络操作。
36. 不要使用retainCount
前面已经多次强调过这个方法的不可靠性,这里再强调一下,首先,在ARC下,只要调用这个方法编译器就会报错,在非ARC模式下,调用此方法也有很多风险,这里只说一点,retainCount返回的是某个时间点上的绝对保留计数,这一时间点无法反应生命周期全貌,并且OC中还有自动释放池机制,所以无论在哪种模式下都不应该使用这个方法。
总的来说,ARC的出现大大的简化了内存管理,但是ARC不代表不会出现内存泄漏等问题,在写代码时还是要很细心,另外书中有许多例子没有介绍,例如僵尸模式下系统是如何把一个对象变成僵尸对象的等,感兴趣的同学可以自行查阅。