Bootstrap

【iOS】——MRC

一、引用计数

内存管理的核心是引用计数器,用一个整数来表示对象被引用的次数,系统需要根据引用计数器来判断对象是否需要被回收。

在每次 RunLoop 迭代结束后,都会检查对象的引用计数器,如果引用计数器等于 0,则说明该对象没有地方继续使用它了,可以将其释放掉。

引用计数器特点如下:

  • 每个对象都有引用计数器
  • 任何一个对象,刚创建的时候,初始的引用计数为 1
  • 没有任何对象持有该对象的引用计数时,也就是对象的引用计数为0,回收该对象
  • 如果对象的引用计数始终不为0,则始终不会被回收除非程序停止运行
对象操作对应方法
生成并持有对象alloc/new/copy/mutableCopy方法
持有对象retain方法
释放对象release方法
废弃对象dealloc方法
返回对象引用引用计数retainCount方法
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 只要创建一个对象默认引用计数器的值就是 1。
        Person *p = [[Person alloc] init];
        NSLog(@"retainCount = %lu", [p retainCount]); // 打印 1
 
        // 只要给对象发送一个 retain 消息, 对象的引用计数器就会 +1。
        [p retain];
 
        NSLog(@"retainCount = %lu", [p retainCount]); // 打印 2
        // 通过指针变量 p,给 p 指向的对象发送一条 release 消息。
        // 只要对象接收到 release 消息, 引用计数器就会 -1。
        // 只要对象的引用计数器为 0, 系统就会释放对象。
 
        [p release];
        // 需要注意的是: release 并不代表销毁 / 回收对象, 仅仅是将计数器 -1。
        NSLog(@"retainCount = %lu", [p retainCount]); // 1
 
        [p release]; // 0
        NSLog(@"--------");
    }

    return 0;
}

对象即将被销毁时会调用dealloc方法,因此可通过dealloc方法有没有被调用,就可以判断出对象是否被销毁

重写了 dealloc 方法,就必须调用 [super dealloc],并且放在最后面调用,重写dealloc方法应在类的实现中进行,不能直接调用 dealloc 方法

- (void)dealloc {
    NSLog(@"Person dealloc");
    // 注意:super dealloc 一定要写到所有代码的最后面
    [super dealloc]; 
}

二、野指针和空指针

被释放的对象称为僵尸对象,此时对象不能被使用

指向僵尸对象的指针称为野指针

给一个野指针发送消息就会报错(EXC_BAD_ACCESS 错误)

没有指向存储空间的指针称为空指针(里面存的是 nil, 也就是 0)

在这里插入图片描述

为了避免给野指针发送消息会报错,一般情况下,当一个对象被释放后我们会将这个对象的指针设置为空指针

三、内存管理思想

单个对象内存管理思想

  • 自己创建的对象自己持有

alloc\new\copy\mutableCopy 方法名开头来创建的对象意味着自己生成的对象只有自己持有。

id obj = [[NSObject alloc] init];

  • 不是自己创建的对象自己也能持有

用 alloc / new / copy / mutableCopy 以外的方法取得的对象,因为非自己生成并持有,所以自己不是该对象的持有者。(比如 NSMutableArray 类的 array方法),但是我们可以通过retain来手动持有对象。

//取得的对象存在但不持有
id obj = [NSMutableArray array];
//持有该对象
[obj retain];

  • 不再需要自己持有的对象就将其释放

释放自己生成并持有的对象

//自己持有对象
id obj = [[NSObject alloc] init];
//释放对象
//指向对象的指针仍然被保留在变量obj中,貌似可以访问,但对象一经释放绝对不可访问
[obj release];

释放非自己生成但持有的对象

id obj = [NSMutableArray array];
//持有该对象
[obj retain];

[obj release];

  • 无法释放不是自己持有的对象

如果不是自己持有的对象一定不能进行释放,倘若在应用程序中释放了非自己所持有的对象就会造成崩溃。

多个对象内存管理思想

多个对象之间往往是通过 setter 方法产生联系的,通过setter方法让一个对象持有另一个对象的引用计数,其内存管理的方法也是在 setter 方法、dealloc 方法中实现的。

多个对象内存管理分为以下几种情况:

1.一个对象不持有另外一个对象

此种情况两个实例对象没有任何关联因此可以将各自释放掉

2.一个对象持有另外一个对象

此种情况通过setter方法将对象赋值给另外一个对象的成员变量,因此会让被持有对象的引用次数加1,所以就需要在setter方法中使用retain方法,为了保证该对象释放同时另外一个对象也能释放,再将另外一个对象引用计数减一

- (void)setMyObject:(MyObject*)myObj { 
    // 引用计数器 +1
    [myObj retain];
    _myObj = myObj;
}

当前对象要释放时因为还持有别的对象所以还要将持有的对象进行销毁

- (void)dealloc {
    // 人释放了, 那么房间也需要释放
    [_myObj release];
    NSLog(@"%s", __func__);
 
    [super dealloc];
}

3.一个对象持有并释放掉一个对象后持有另外一个对象

此种情况如果还使用先前的getter方法,不难发现将第一个对象赋值给该对象时第一个对象的引用计数加一,再将第一个对象引用计数减一后将第二个对象赋值给该对象时第二个对象的引用计数加一,再将第二个对象引用计数减一,释放该对象会让第二个对象的引用计数减一但是第一个对象则不会减一,因此会造成第一个对象无法被释放掉。

因此就需要修改setter方法每当我们需要更换持有的对象时就让原来持有的对象的引用计数减一。

- (void)setMyObject:(MyObject*)myObj { 
		[_myObj release];
    // 引用计数器 +1
    [myObj retain];
    _myObj = myObj;
}
4. 一个对象持有一个对象并释放该对象后再次持有该对象

此种情况使用上一个getter方法不难发现,我们将那个对象赋值给该对象后该对象对那个对象的引用计数加一,再将那个对象引用计数减一,此时两个对象的引用计数相同,此时再次进行赋值会导致那个对象的引用计数先减一此时为0变成了野指针,此时再对野指针进行retain操作就会报错。

此时就需要在setter方法中判断是否重复赋值,如果是同一个实例对象,就不需要重复进行 releaseretain

- (void)setMyObject:(MyObject*)myObj { 
   if (_myObj != myObj) {
   [_myObj release];
    // 引用计数器 +1
    [myObj retain];
    _myObj = myObj;
   }
}

因为 retain 不仅仅会对引用计数器 +1, 而且还会返回当前对象,所以上述代码可最终简化成:

- (void)setMyObject:(MyObject*)myObj { 
   if (_myObj != myObj) {
   [_myObj release];
    // 引用计数器 +1
    _myObj = [myObj retain];
   }
}

四、 @property参数

  • 在成员变量前加上 @property,系统就会自动帮我们生成基本的 setter / getter 方法,但是不会生成内存管理相关的代码。
@property (nonatomic) int val;
  • 如果在 property 后边加上 assign,系统也不会帮我们生成 setter 方法内存管理的代码,仅仅只会生成普通的 getter / setter 方法,默认什么都不写就是 assign
@property(nonatomic, assign) int val;
  • 如果在 property 后边加上 retain,系统就会自动帮我们生成 getter / setter 方法内存管理的代码,但是仍需要我们自己重写 dealloc 方法。
@property(nonatomic, retain)MyObject *myObj;

五、自动释放池

当我们不再使用一个对象的时候应该将其空间释放,但是有时候我们不知道何时应该将其释放。为了解决这个问题,Objective-C 提供了 autorelease 方法。

autorelease 是一种支持引用计数的内存管理方式,主要用于延迟对象的释放,只要给对象发送一条 autorelease 消息,会将对象注册到一个自动释放池中,当自动释放池被销毁时,会对池子里面的所有对象做一次 release 操作。

autorelease方法会返回对象本身,且调用完autorelease方法后,对象的计数器不变。

Person *p = [Person new];
p = [p autorelease];
NSLog(@"count = %lu", [p retainCount]); // 计数还为 1

1.autoreleasepool的创建

autoreleasepool有两种创建方法

  • 第一种是使用 NSAutoreleasePool 创建
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 创建自动释放池
[pool release]; // [pool drain]; 销毁自动释放池

  • 第二种是使用 @autoreleasepool 创建
@autoreleasepool
{ // 开始代表创建自动释放池
 
} // 结束代表销毁自动释放池

2.autorelease 的使用方法

NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];
Person *p = [[[Person alloc] init] autorelease];
[autoreleasePool drain];

需要注意的是对象调用autorelease方法需要在创建autoreleasepool之后否则无法将其注册到autoreleasepool。

@autoreleasepool
{ // 创建一个自动释放池
        Person *p = [[Person new] autorelease];
        // 将代码写到这里就放入了自动释放池
} // 销毁自动释放池(会给池子中所有对象发送一条 release 消息)

在自动释放池的外部发送 autorelease 不会被加入到自动释放池中。就算对象创建在自动释放池内没调用autorelease方法也不会将其注册到autoreleasepool。

@autoreleasepool {
}
// 没有与之对应的自动释放池, 只有在自动释放池中调用autorelease才会放到释放池
Person *p = [[[Person alloc] init] autorelease];
[p run];
 
// 正确写法
@autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
 }
 
// 正确写法
Person *p = [[Person alloc] init];
@autoreleasepool {
    [p autorelease];
}

3.自动释放池的嵌套使用

  • 自动释放池是以栈的形式存在。

  • 由于栈只有一个入口,所以调用 autorelease 会将对象放到栈顶的自动释放池。

@autoreleasepool { // 栈底自动释放池
    @autoreleasepool {
        @autoreleasepool { // 栈顶自动释放池
            Person *p = [[[Person alloc] init] autorelease];
        }
        Person *p = [[[Person alloc] init] autorelease];
    }
}

4.自动释放池的注意事项

  • 自动释放池中不适宜放占用内存比较大的对象

因为自动释放池是延迟释放机制,只有清空池子时才会将注册到池子的对象销毁,此时如果有多个内存较大的对象就会造成短时间内内存暴涨。

  • 不要把大量循环操作放到同一个 @autoreleasepool 之间,这样会造成内存峰值的上升
// 内存暴涨
@autoreleasepool {
    for (int i = 0; i < 99999; ++i) {
        Person *p = [[[Person alloc] init] autorelease];
    }
}
// 内存不会暴涨
for (int i = 0; i < 99999; ++i) {
    @autoreleasepool {
        Person *p = [[[Person alloc] init] autorelease];
    }
}

两段代码的区别在于第一个自动释放池是在循环外部创建的,所有的Person对象都被添加到了同一个自动释放池中。它们并不会被立即释放。只有当自动释放池被排空时,也就当@autoreleasepool块执行完毕后,所有这些对象才会被release。因此,在循环结束前,所有创建的Person对象都存在于内存中,这会导致内存使用量显著增加,即所谓的“内存暴涨”。

第二个每次循环迭代时都会创建一个新的自动释放池。每一个Person对象都在创建它的那个迭代的自动释放池中被管理,并且在该迭代结束时,该自动释放池就会被排空,从而立即释放该迭代中创建的Person对象。由于每个Person对象都在其创建后的短时间内被释放,因此内存使用量不会像第一个例子那样累积,从而避免了“内存暴涨”的问题。

  • 不要连续调用 autorelease
@autoreleasepool {
 // 会导致过度释放
    Person *p = [[[[Person alloc] init] autorelease] autorelease];
 }

  • 调用 autorelease 后又调用 release
@autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
    [p release]; //过度释放
}

六、循环引用

对象A和对象B互相作为对方的成员变量时也就是互相持有对方时相互引用了对方作为自己的成员变量,只有当自己销毁时,才会将成员变量的引用计数减1,这就导致了A的销毁依赖于B的销毁,同样B的销毁依赖于A的销毁,这样就造成了循环引用问题。

循环引用也分为几种类型:

  1. 自循环引用
  2. 相互循环引用
  3. 多循环引用

1.自循环引用

假如有一个对象,内部强持有它的成员变量obj,若此时我们给obj赋值为原对象时,就是自循环引用。

2.相互循环引用

对象A内部强持有obj,对象B内部强持有obj,若此时对象A的obj指向对象B,同时对象B中的obj指向对象A,就是相互引用。

3.多循环引用

假如类中有对象1…对象N,每个对象中都强持有一个obj,若每个对象的obj都指向下个对象,就产生了多循环引用。

;