0%

OC内存管理变迁

OC的内存管理大致可以分为MRC阶段和ARC阶段。

MRC:手动引用计数,“手动”很好理解就是亲自动手写代码,“引用计数”就是数数,数一下这个对象的引用次数。当我们需要用这个对象的时候先调用一下retain,引用计数就加1,不用了就调用一下release,引用计数就减1,减到0系统就会帮我们释放这个对象的内存。

为什么会有“引用计数”这么个东西?
试想一下如果没有引用计数会怎样,比如你创建了一个对象,这个对象传来传去在很多个地方使用,那么什么时候调用free销毁对象成了一个难题。如下:类A创建了一个对象obj,然后obj作为参数传给类B。

1
2
3
4
5
6
7
- (BOOL)function
{
id obj = [xxx new];
[B someMethod:obj];
...
return true
}

obj传给类B后,B有可能保存也可能不保存obj。这时类A对obj的处理就非常尴尬了,如果贸然调用free,万一类B还在用就会导致野指针访问崩溃,不调用吧,万一类B又没在用就会内存泄露。但是通过引用计数,我们就不需要考虑什么时候调用free了,系统会根据引用计数是否为0来决定是否销毁对象,你只需要retain和release。

举个生活中的例子:

场景1:张三去厕所拉屎,里面一片漆黑,于是张三打开灯,找了个坑位关上门开始造。造完后,出来关上灯。完美!

场景2:还是张三去厕所拉屎,里面一片漆黑,于是张三打开灯,找了个坑位关上门开始造。这时,李四也来拉屎,由于灯是亮的于是李四径直找了个坑位关上门开始造。过了一会张三拉完了,由于张三并不知道李四进来了,于是出门把灯关了。只听见在漆黑里的李四一声卧槽!

从上面的例子可以看出什么时候关灯确实是个问题,怎么解决这个问题呢?方法有很多比如进去之后大喊一声俺来了让里面所有人都知道有人来了。但是最简单的办法还是大家都遵守一套规则:进门前请按加号键,出门后请按减号键。这就相当于引用计数了。

场景3:张三去厕所拉屎,里面一片漆黑,外面告示写着进门前请按加号键,出门后请按减号键。这时张三按了一下墙壁上的加号键,灯亮了,于是找了个坑位关上门开始造。这时李四也来拉屎,他也按照告示按了下墙壁上的加号键,接着他找了个坑位开始安心拉屎。这时张三拉完屎,出门时按照告示按下了减号键,灯没有灭,这时张三明白了期间又来人了,虽然不知道在哪个坑位,但肯定是有人正在拉。当李四拉完后,也按了下墙壁上的减号键,这时确实没人在拉屎了,于是灯灭了,非常的完美。在这个系统里,你不需要知道中途进来了多少人,也不需要任何交流,只需要遵守外面的告示,就解决了灯何时灭的问题。

retain代表你拥有了这个对象,不再需要时要调用release,将引用计数减1。除了retain方法代表你拥有这个对象,有一些方法也代表你拥有这个对象,不用了也需要调用release,究竟是哪些方法呢?人们定义了一套命名规则,规定以alloc/new/copy/mutableCopy开头的方法创建的对象是你自己拥有的对象,不用了也需要调用release。除此之外获得的对象则不是自己拥有的对象,不能调用release。这就是手动引用计数。

举个例子:

XQCat 为ARC,ViewController为MRC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
- (void)viewDidLoad {
[super viewDidLoad];

[self test_creat_obj];
}

- (void)test_creat_obj {
{
XQCat *cat1 = [XQCat newCat];
NSLog(@"cat1:%@", cat1);
}
{
XQCat *cat2 = [XQCat newerCat];
NSLog(@"cat2:%@", cat2);
}
{
XQCat *cat3 = [XQCat allocCat];
NSLog(@"cat3:%@", cat3);
}
{
XQCat *cat4 = [XQCat allocationCat];
NSLog(@"cat4:%@", cat4);
}
}

@implementation XQCat

- (void)dealloc
{
NSLog(@"%@销毁",self);
}

+ (instancetype)newCat {
id cat = [[[self class] alloc] init];
return cat;
}

+ (instancetype)newerCat {
id cat = [[[self class] alloc] init];
return cat;
}

+ (instancetype)allocCat {
id cat = [[[self class] alloc] init];
return cat;
}

+ (instancetype)allocationCat {
id cat = [[[self class] alloc] init];
return cat;
}

@end

打印:

1
2
3
4
5
6
2022-10-12 15:27:25.267602+0800 MRCDemo[4032:1406589] cat1:<XQCat: 0x600003110be0>
2022-10-12 15:27:25.267668+0800 MRCDemo[4032:1406589] cat2:<XQCat: 0x60000310c140>
2022-10-12 15:27:25.267708+0800 MRCDemo[4032:1406589] cat3:<XQCat: 0x600003108340>
2022-10-12 15:27:25.267749+0800 MRCDemo[4032:1406589] cat4:<XQCat: 0x600003100f20>
2022-10-12 15:27:25.281462+0800 MRCDemo[4032:1406589] <XQCat: 0x600003100f20>销毁
2022-10-12 15:27:25.281524+0800 MRCDemo[4032:1406589] <XQCat: 0x60000310c140>销毁

你会发现cat1,cat3没被释放,cat2和cat4被释放了,说明系统是遵守这个命名规则的。cat1,cat3是我们自己拥有的,需要我们自己调用release释放,而cat2和cat4不是按照命名规则创建的,说明不是我们拥有的对象,而是注册到自动释放池里的对象。所以我们不能再调用release。

然而,每次手动调用retain和release也是挺麻烦的,稍有不慎多调用了一次release,那就野指针崩溃,少调用了一次就内存泄露。于是人们便想有没有办法让系统自动在合适的地方添加retain和release?答案自然是有。变量的作用域就是一个很好的时机。变量初始化的时候就插入一条retain,当变量超出作用域的时候就插入一条release。这样就不需要我们手动调用了,编译器会在合适的地方自动添加retain和release。上面的例子,人们手动按按钮改为红外线自动感应入和出,这就是自动引用计数了。

关于ARC还有一些问题,比如并不是每次变量初始化都是插入retain,有可能我们需要的是weak,那么编译器是怎么知道的呢?为了解决这个问题,所有权修饰符便出现了。通过不同类型的所有权修饰符,编译器就知道是添加retain还是添加weak了。ARC 有效时,id 类型和对象类型必须附加所有权修饰符,有如下几种:

  • __strong 修饰符
  • __weak 修饰符
  • __unsafe_unretained 修饰符
  • __autoreleasing 修饰符

其中 __strong 修饰符是OC对象类型的默认修饰符,此时编译器会自动插入retain/release。关于所有权修饰符背后的故事可以参考其他文章。

参考

理解 iOS 的内存管理

觉得文章有帮助可以打赏一下哦!