现在iOS开发已经是ARC甚至是swift的时代,但是内存管理仍是一个重点关注的问题,如果只知盲目开发而不知个中原理,踩坑就跳不出来了,理解好内存管理,能让我们写出更有质量的代码。
内存管理是程序设计中很重要的一部分,程序在运行的过程中消耗内存,运行结束后释放占用的内存。如果程序运行时一直分配内存而不及时释放无用的内存,会造成这样的后果:程序占用的内存越来越大,直至内存消耗殚尽,程序因无内存可用导致崩溃,这样的情况我们称之为内存泄漏
。
ObjC的内存管理比较简洁,然而要深刻理解也不是一件易事,本文将介绍如何使用ObjC进行内存管理。
1. 引用计数
在ObjC中,对象什么时候会释放(或者对象占用的内存什么时候会被回收利用)?
答案:当对象没有被任何变量引用(也可以说是没有指针指向该对象)的时候,就会被释放。
那怎么知道对象已经没有被引用了呢?
ObjC采用引用计数(reference counting)的技术进行管理:
- 每个对象都有一个关联的整数,称为引用计数器
- 当代码需要使用该对象时,则将对象的引用技术加1
- 当代码结束使用该对象时,则将对象的引用技术减1
- 当引用计数的值变为0时,表示对象没有被任何代码使用,此时对象将被释放
与之对应的消息发送方法如下:
- 当对象被创建(通过
alloc
、new
或copy
等方法)时,其引用计数初始值为1
- 给对象发送
retain
消息,其引用计数加1
- 当对象引用计数归0时,ObjC给对象发送
dealloc
消息销毁对象
下面通过一个简单的例子来说明:
场景:有一个宠物中心(内存
):可以派出小动物(对象
)陪小朋友们玩耍(对象引用者
),现在xiaoming想和小狗一起玩耍。
新建Dog类,重写其创建和销毁的方法:
// // Dog.m // TextARC // // Created by taobaichi on 2017/3/27. // Copyright © 2017年 MaChao. All rights reserved. // #import "Dog.h" @implementation Dog -(instancetype)init{ if (self = [super init]) { NSLog(@"小狗被派出去啦"); } return self; } -(void)dealloc{ NSLog(@"小狗回到宠物中心"); [super dealloc]; } @end
在main方法中创建dog对象,给dog发送消息
// // main.m // TextARC // // Created by taobaichi on 2017/3/27. // Copyright © 2017年 MaChao. All rights reserved. // #import <Foundation/Foundation.h> #import "Dog.h" int main(int argc, const char * argv[]) { @autoreleasepool { Dog * dog = [[Dog alloc]init]; //模拟:xiaoming需要和小狗玩耍,需要将其引用计数加1 [dog retain]; NSLog(@"小狗的引用计数为 %ld",dog.retainCount); //模拟:xiaoming不和小狗玩耍了,需要将其引用计数减1 [dog release]; NSLog(@"小狗的引用计数为 %ld",dog.retainCount); //没人需要和小狗玩耍了,将其引用计数减1 [dog release]; //将其指针置为nil,否则变成野指针 dog = nil; } return 0; }
输出结果为
2017-03-27 11:10:21.017977 TextARC[2645:84069] 小狗被派出去啦! 初始引用计数为 1 2017-03-27 11:10:21.018125 TextARC[2645:84069] 小狗的引用计数为 2 2017-03-27 11:10:21.018145 TextARC[2645:84069] 小狗的引用计数为 1 2017-03-27 11:10:21.018159 TextARC[2645:84069] 小狗回到宠物中心
可以看到,引用技术帮助宠物中心很好的标记了小狗的使用状态,在完成任务的时候及时收回到宠物中心。
思考几个问题:
- NSString 引用技术问题
如果我们尝试查看一个string的引用技术
NSString * str = @"hello guys"; NSLog(@"%ld",str.retainCount);
打印结果:
2017-03-27 11:14:33.191171 TextARC[2715:86088] str的引用计数: -1
str的引用计数为-1,这可以理解为NSString
实际上是一个字符串常量,是没有引用计数的(或者它的引用计数是一个很大的值(使用%lu可以打印查看),对它做引用计数操作没实质上的影响)。
- 赋值不会拥有某个对象
NSString * name = dog.name;
这里仅仅是指针赋值操作,并不会增加name的引用计数,需要持有对象必须要发送retain消息
- dealloc
由于释放对象是会调用dealloc
方法,因此重写dealloc
方法来查看对象释放的情况,如果没有调用则会造成内存泄露。在上面的例子中我们通过重写dealloc
让小狗被释放的时候打印日志来告诉我们已经完成释放。
- 在上面的例子中,如果我们增加这样一个操作
//没人需要和小狗玩耍了,将其引用计数减1 [dog release]; NSLog(@"小狗的引用计数为·:%ld",dog.retainCount);
会发现获取到的引用计数为1,为什么不是0呢?
这是因为对引用计数为1的对象release
时,系统知道该对象将被回收,就不会再对该对象的引用计数进行减1操作,这样可以增加对象回收的效率。
另外,对已释放的对象发送消息是不可取的,因为对象的内存已被回收,如果发送消息时,该内存已经被其他对象使用了,得到的结果是无法确定的,甚至会造成崩溃。
2. 自动释放池
现在已经明确了,当不再使用一个对象时应该将其释放,但是在某些情况下,我们很难理清一个对象什么时候不再使用(比如xiaoming和小狗玩耍结束的时间不确定),这可怎么办
ObjC提供autorelease
方法来解决这个问题,当给一个autorelease
消息时,方法会在未来某个时间给这个对象发送release
消息将其释放,在这个时间段内,对象还是可以使用的。
那autorelease
的原理是什么呢
原理就是对象接收到autorelease
消息时,它会被添加到了当前的自动释放池中,当自动释放池被销毁时,会给池里所有对象发送release
消息。
这里就引出了自动释放池
这个概念,什么是自动释放池呢?顾名思义,就是一个池,这个池可以容纳对象,而且可以自动释放,这就大大增加了我们处理对象的灵活性。
自动释放池怎么创建?
ObjC提供两种方法创建自动释放池:
方法一:使用NSAutoreleasePool
来创建
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc]init];
方法二:使用@autoreleasepool
创建
@autoreleasepool { //这里写代码 }
自动释放池创建后,就会成为活动的池子,释放池子后,池子将释放其所包含的所有对象。
以上两种方法推荐第一种,因为将内存交给ObjC管理更高效
自动释放池什么时候创建?
app使用过程中,会定期自动生成和销毁自动释放池,一般是在程序事件处理之前创建,当然我们也可以自行创建自动释放池,来达到我们一些特定的目的。
自动释放池什么时候销毁?
自动释放池的销毁时间是确定的,一般是在程序事件处理之后释放,或者由我们自己手动释放。