内存管理(一)MRC
管的谁
在Objective-C中创建的对象都分配在堆区,内存管理针对的也是这块区域。
引用计数
Objective-C内存管理的核心其实引用计数,引用计数(Reference Count)是一个简单而有效的管理对象生命周期的方式。当我们创建一个新对象的时候,它的引用计数为 1,当有一个新的指针指向这个对象时,我们将其引用计数加 1,当某个指针不再指向这个对象时,我们将其引用计数减 1,当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存
Objective-C有两种内存管理机制:手动管理(MRC)和自动管理(ARC)。目前基本上开发用的都是ARC。最开始学习iOS的时候也用过MRC,先介绍下MRC的机制。
MRC
操作对象的四种方式:
- 生成并持有对象:alloc/new/copy/mutableCopy等, retainCount :+1
- 持有对象:retain,retainCount :+1
- 释放对象:release,retainCount :-1
- 废弃对象:dealloc, 自动释放内存
内存管理的四个法则:
- 自己生成的对象,自己持有
- 非自己生成的对象,自己也能持有
- 不再需要自己持有对象的时候释放对象
- 非自己持有的对象无法释放
示例代码
自己生成的对象,自己持有:
以 alloc/new/copy/mutableCopy 等方法创建的对象归调用者持有
- (void)test1 {
id obj0 = [NSObject alloc]; // 创建一个NSObject对象返回给变量obj, 并且归调用者持有
NSLog(@"obj0 引用计数%ld",CFGetRetainCount((__bridge CFTypeRef)obj0));
}
输出
obj0 引用计数1
分析
创建一个NSObject对象返回给变量obj, 并且归调用者持有
alloc创建对象的过程看我这篇文章
非自己生成的对象,自己也能持有:
alloc/new/copy/mutableCopy 等方法以外的方式创建的对象不归调用者持有
- (void)test2 {
id obj = [NSMutableArray array];
id obj2 = [obj retain];
NSLog(@"obj2 引用计数%ld",CFGetRetainCount((__bridge CFTypeRef)obj2));
}
输出
obj2 引用计数2
分析
非自己生成的对象,且该对象存在是通过autorelease来实现的。autorelease提供了一种使得对象在超出生命周期后能正确的被释放(通过调用release方法)机制,以便于将对象返回给调用者,让调用者持有后再释放对象。否则对象还没来得及被调用者持有就被系统释放了。调用autorelease后对象不会立刻被释放,而是被注册到autoreleasepool中,然后当autoreleasepool结束被销毁的时候,才会调用对象的release方法释放对象
不在需要自己持有的对象时释放
- (void)test3 {
Person *p = [[Person alloc] init];
[p release];
}
非自己持有的对象无法释放:
由于当前的调用者并不持有该对象,不能进行释放操作,否则导致程序崩溃
- (void)test4 {
// 由于当前的调用者并不持有改对象,不能进行释放操作,否则导致程序崩溃。
// 如果要释放该对象,需要先对对象进行retain操作。
id obj = [NSMutableArray array];
[obj release];
}
代码改成这样
注意:如果返回给obj的是NSMutableArray对象,会导致程序崩溃,但是如果是NSArray就不会
- (void)test5 {
id obj = [NSMutableArray array];
[obj retain]; // 当前调用者obj持有NSMutableArray对象
NSLog(@"obj 引用计数%ld",CFGetRetainCount((__bridge CFTypeRef)obj));
[obj release];
}
分析
如果要释放该对象,需要先对对象进行retain操作
属性的引用计数情况
定义一个Person类
代码如下
@interface Person : NSObject
@property(nonatomic, copy) NSString *name;
+ (Person *)create;
@end
@implementation Person
+ (Person *)create {
Person *per = [[Person alloc] init];
[per autorelease];
return per;
}
- (void)dealloc {
NSLog(@"%s",__func__);
[super dealloc];
}
@end
VC代码
@interface VC2 ()
@property (nonatomic, copy) NSArray *array;
@property (nonatomic, retain) Person *per;
@end
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
[self test6];
}
- (void)test6 {
self.array = [[NSArray alloc] initWithObjects:@1, nil];
NSLog(@"array 引用计数%ld",CFGetRetainCount((__bridge CFTypeRef)_array));
}
输出
array 引用计数2
上面不是说过 alloc创建一个对象并且返回给调用者持有 引用计数为1吗?
这里为什么此时对象的引用计数是2呢?
分析
了解属性的细节 看我这篇文章
OC语言中 .语法 其实就是调用setter方法
@property (nonatomic, retain) Person *per;
定义per 属性时候 是retain关键字 所以生成的标准setter方法内部会进行retain操作
- (void)setArray:(NSArray *)array {
if(_array != array) {
[_array release];
_array = [array retain]; // retain新值
}
}
所以此时对象的内存引用情况是:alloc创建时retainCount为1,setter方法中retain了一次引用计数加1,所以此时retainCount变为了2
类似于如下操作:
NSArray *temp = [[NSArray alloc] initWithObjects:@1, nil]; 引用计数+1
self.array = temp; 引用计数+1
所以一般在使用属性赋值的时候一般这么写:
用autorelease抵消一次retain操作
self.array = [[[NSArray alloc] initWithObjects:@2, nil] autorelease];
或者这样
NSArray *temp = [[NSArray alloc] initWithObjects:@1, nil];
self.array = temp;
[temp release];
理解了上面,我们用Person类 观察下有没有内存泄漏
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
[self test7];
}
- (void)test7 {
self.per = [[[Person alloc] init] autorelease];
// alloc1次 setter方法一次 所以是2
// autorelease在NSLog时候 还没有释放Person对象 所以还是2,但是终究会 -1
NSLog(@"per 引用计数%ld",CFGetRetainCount((__bridge CFTypeRef)_per));
}
// autorelease1次 VC delloc中 1次 所以Person对象最终会被释放
- (void)dealloc {
[_per release];
NSLog(@"%s",__func__);
[super dealloc];
}
输出
per 引用计数2
[Person dealloc]
[VC2 dealloc]
分析
都能够正常销毁,不存在内存泄漏
autorelease延迟释放抵消一次 放弃Person所有权
生成的标准的setter方法,不会自动的在dealloc中生成release的代码,所以要手动的重写dealloc方法,加上release的代码
VC delloc中 在发送一条 release 所以Person对象最终会被销毁
为了防止野指针, 可以加上nil
- (void)dealloc {
[_per release];
_per = nil;
NSLog(@"%s",__func__);
[super dealloc];
}
大家一定要搞清楚什么是野指针 本篇简单说下野指针和僵尸对象 详细的底层原理可以看我这篇文章
野指针和僵尸对象
野指针
指向一个已经被删除的对象或者访问受限内存区域的指针就是野指针,野指针不是nil指针,而是指向了垃圾内存的指针
野指针的场景:
1、 对象释放后,指针没有置空
- 使用unsafe_unretained修饰符,对象释放后,没有手动置为nil
- KVO没有移除观察者
2、对象提前释放
- 异步函数中block使用的self没有强引用,导致外部已经释放掉,但是里面还在使用
3、对象多次释放
- 多个线程同时对某个对象赋值但没有加锁,就可能多次release
僵尸对象
一个已经被释放掉的对象就是僵尸对象
一个OC对象的引用计数为0,调动dealloc后销毁之后,就是僵尸对象。
一个对象虽然被销毁掉了,但是数据依然在内存中,所以如果通过野指针去访问僵尸对象,一旦这个僵尸对象的内存已经被分配给其他人了,就会出错。
为什么不开启僵尸对象检测?
这样每次通过指针访问对象的时候都会检查是否为僵尸对象,这样很影响效率
为什么每次不去把内存上的数据清零?
没必要,影响效率, 数据每次都是覆盖。
例子:
@interface Person : NSObject
@property(nonatomic, strong)NSString *name;
@end
@implementation Person
- (void)dealloc {
NSLog(@"%s",__func__);
[super dealloc];
}
@end
@implementation ViewController
// crash
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
Person *__unsafe_unretained wp = person;
person.name = @"yang";
[person release];
NSLog(@"wp==%@",wp.name);
}
输出崩溃
Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
Printing description of person:
<Person: 0x600000e683e0>
(lldb) x/5gx 0x600000e683e0
0x600000e683e0: 0x000000010a944530 0x000000010a93f040
0x600000e683f0: 0x0000000000000000 0x0000000000000000
0x600000e68400: 0x0000000000000000
(lldb) p person
(Person *) $0 = 0x0000600000e683e0
(lldb) p wp
(Person *) $1 = 0x0000600000e683e0
-[Person dealloc]
(lldb) p wp
(Person *) $2 = 0x0000600000e683e0
(lldb) 0x600000e683e0对象已经释放了 但是wp指针变量还指向0x600000e683e0对象 0x600000e683e0现在就属于僵尸对象 wp就是野指针 会造成程序崩溃
分析
wp指针弱引用[Person alloc] 对象 不影响对象释放
0x600000e683e0对象已经释放了 但是wp指针变量还指向0x600000e683e0对象 0x600000e683e0现在就属于僵尸对象 wp就是野指针 会造成程序崩溃
把指针设为nil之后
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
Person *__unsafe_unretained wp = person;
person.name = @"yang";
[person release];
person= nil;
wp = nil;
NSLog(@"wp==%@",wp.name);
}
输出
-[Person dealloc]
wp==(null)
或者使用 __weak
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
Person *__weak wp = person;
person.name = @"yang";
[person release];
person= nil;
NSLog(@"wp==%@",wp.name);
}
输出
(lldb) p wp
(Person *) $0 = 0x00006000018487e0
-[Person dealloc]
(lldb) p wp
(Person *) $1 = nil
(lldb)
两种方式都可以保证程序正常运行
思考个问题
Q: 既然 __weak 更安全,那么为什么已经有了 __weak 还要保留 __unsafe_unretained ?
1、__weak仅在ARC中才能使用,而MRC只能使用__unsafe_unretained
2、__weak对性能会有一定的消耗,当一个对象dealloc时,需要遍历对象的weak表,把表里的所有weak指针变量值置为nil,指向对象的weak指针越多,性能消耗就越多。所以__unsafe_unretained比__weak快。当明确知道对象的生命周期时,选择__unsafe_unretained会有一些性能提升。
比如,MyViewController 持有 MyView,MyView 需要调用 MyViewController 的接口。MyView 中就可以存储__unsafe_unretained MyViewController *_viewController
对于__weak底层源码分析可以看我这篇文章
下篇文章我们继续总结ARC
后记
记得在《寻梦环游记》里对于一个人的死亡是这样定义的:当这个这个世界上最后一个人都忘记你时,就迎来了终极死亡。类比于引用计数,就是每有一个人记得你时你的引用计数加1,每有一个人忘记你时,你的引用计数减1,当所有人都忘记你时,你就消失了,也就是从内存中释放了。
如果再深一层,包含我们后面要介绍的ARC中的强引用和弱引用的话,那这个记住的含义就不一样了。强引用就是你挚爱的亲人,朋友等对你比较重要的人记得你,你的引用计数才加1。
而弱引用就是那种路人,一面之缘的人,他们只是对你有一个印象,他们记得你是没有用的,你的引用计数不会加1。当你挚爱的人都忘记你时,你的引用计数归零,你就从这个世界上消失了,而这些路人只是感觉到自己记忆中忽然少了些什么而已。
外链
1、iOS属性的底层原理
2、__weak实现底层原理
3、野指针僵尸对象底层原理