iOS-[内存管理]

805 阅读1小时+

问题出发

我们知道,在面向对象编程中,每个对象都存在对应的构造函数和析构函数,当对象被创建和销毁时,构造、析构函数会被分别调用。对象从创建到销毁的整个过程,称之为对象的生命周期,为了保证在(1)使用对象时对象是有效的,没有被过早释放;(2)不需要使用对象时,对象被正常销毁而不是一直驻留在内存中,我们需要对象的生命周期进行管理。那么在iOS系统中,对象生命周期管理策略是怎样的呢? 为了更好地逐步的了解iOS的内存管理机制,先抛出一些问题: (1)iOS使用引用计数来管理对象的生命周期,什么是引用计数? (2)引用计数怎么存储? (3)什么是MRC,MRC下的内存管理策略是什么,有什么需要注意的? (4)ARC的作用是什么,ARC有哪些新规则, ARC会带来什么问题? (5)ARC管理的对象和MRC管理的对象之间如何进行转换,转换时需要注意什么? (6)什么是autoreleasepool,它的原理是什么?他和runloop的关系是什么? (7)什么是TaggedPointer,哪些对象使用了TaggedPointer技术?

原理探究

(1)关于iOS内存管理

应用程序内存管理是在程序运行时分配内存,使用它并在使用完后释放它的过程。编写良好的程序将使用尽可能少的内存。在 Objective-C 中,它也可以看作是在许多数据和代码之间分配有限内存资源所有权的一种方式。掌握内存管理知识,我们就可以很好地管理对象生命周期并在不再需要它们时释放它们,从而管理应用程序的内存。

虽然通常在单个对象级别上考虑内存管理,但实际上我们的目标是管理对象图,要保证在内存中只保留需要用到的对象,确保没有发生内存泄漏。

下图是苹果官方文档给出的 “内存管理对象图”,很好地展示了一个对象 “创建——持有——释放——销毁” 的过程。 在这里插入图片描述

(2)iOS使用引用计数来管理对象的生命周期,什么是引用计数 ?

引用计数的原理

采用引用计数来管理对象的生命周期,当需要持有一个对象时,使它的引用计数 +1;当不需要持有一个对象的时候,使它的引用计数 -1;当一个对象的引用计数为 0,该对象就会被销毁。

在《Objective-C 高级编程:iOS 与 OS X 多线程和内存管理》这本书中举了一个 “办公室里的照明问题” 的例子,很好地说明了引用计数机制。

假设办公室里的照明设备只有一个。上班进入办公室的人需要照明,所以要把灯打开。而对于下班离开办公室的人来说,已经不需要照明了,所以要把灯关掉。 在这里插入图片描述

若是很多人上下班,每个人都开灯或者关灯,那么办公室的情况又将如何呢?最早下班的人如果关了灯,那就会像下图那样,办公室里还没走的所有人都将处于一片黑暗之中。 在这里插入图片描述

解决这一问题的办法就是使办公室在还有至少一人的情况下保持开灯状态,而在无人时保持关灯状态。 (1)最早进入办公室的人开灯。 (2)之后进入办公室的人,需要照明。 (3)下班离开办公室的人,不需要照明。 (4)最后离开办公室的人关灯(此时已无人需要照明)。 为判断是否还有人在办公室里,这里导入计数功能来计算 “需要照明的人数”。下面让我们来看看这一功能是如何运作的吧。 (1)第一个人进入办公室,“需要照明的人数” 加 1。计数值从 0 变成了 1,因此要开灯。 (2)之后每当有人进入办公室,“需要照明的人数” 就加 1。如计数值从 1 变成 2。 (3)每当有人下班离开办公室,“需要照明的人数” 就减 1。如计数值从 2 变成 1。 (4)最后一个人下班离开办公室,“需要照明的人数” 减 1。计数值从 1 变成了 0,因此要关灯。 这样就能在不需要照明的时候保持关灯状态。办公室中仅有的照明设备得到了很好的管理,如下图所示: 在这里插入图片描述

在 Objective-C 中,“对象” 相当于办公室里的照明设备。在现实世界中办公室里的照明设备只有一个,但在 Objective-C 的世界里,虽然计算机的资源有限,但一台计算机可以同时处理好几个对象。 此外,“对象的使用环境” 相当于上班进入办公室的人。虽然这里的 “环境” 有时也指在运行中的程序代码、变量、变量作用域、对象等,但在概念上就是使用对象的环境。上班进入办公室的人对办公室照明设备发出的动作,与 Objective-C 中的对应关系则如下表所示: 在这里插入图片描述

使用计数功能计算需要照明的人数,使办公室的照明得到了很好的管理。同样,使用引用计数功能,对象也就能够得到很好的管理,这就是 Objective-C 的内存管理。如下图所示: 在这里插入图片描述

引用计数的存储

以上我们对 “引用计数” 这一概念做了初步了解,Objective-C 中的 “对象” 通过引用计数功能来管理它的内存生命周期。那么,对象的引用计数是如何存储的呢?它存储在哪个数据结构里?

首先,不得不提一下isa。

isa指针用来维护 “对象” 和 “类” 之间的关系,并确保对象和类能够通过isa指针找到对应的方法、实例变量、属性、协议等。 在这里插入图片描述 在 arm64 架构之前,isa就是一个普通的指针,直接指向objc_class,存储着Class、Meta-Class对象的内存地址。instance对象的isa指向class对象,class对象的isa指向meta-class对象;

// objc.h
struct objc_object {
    Class isa;  // 在 arm64 架构之前
};

从 arm64 架构开始,对isa进行了优化,用nonpointer表示,变成了一个共用体(union)结构,还使用位域来存储更多的信息。将 64 位的内存数据分开来存储着很多的东西,其中的 33 位才是拿来存储class、meta-class对象的内存地址信息。要通过位运算将isa的值& ISA_MASK掩码,才能得到class、meta-class对象的内存地址。

// objc-private.h
struct objc_object {
private:
    isa_t isa;  // 在 arm64 架构开始
};

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

#if SUPPORT_PACKED_ISA

    // extra_rc must be the MSB-most field (so it matches carry/overflow flags)
    // nonpointer must be the LSB (fixme or get rid of it)
    // shiftcls must occupy the same bits that a real class pointer would
    // bits + RC_ONE is equivalent to extra_rc + 1
    // RC_HALF is the high bit of extra_rc (i.e. half of its range)

    // future expansion:
    // uintptr_t fast_rr : 1;     // no r/r overrides
    // uintptr_t lock : 2;        // lock for atomic property, @synch
    // uintptr_t extraBytes : 1;  // allocated with extra bytes

# if __arm64__  // 在 __arm64__ 架构下
#   define ISA_MASK        0x0000000ffffffff8ULL  // 用来取出 Class、Meta-Class 对象的内存地址
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;  // 0:代表普通的指针,存储着 Class、Meta-Class 对象的内存地址
                                          // 1:代表优化过,使用位域存储更多的信息
        uintptr_t has_assoc         : 1;  // 是否有设置过关联对象,如果没有,释放时会更快
        uintptr_t has_cxx_dtor      : 1;  // 是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
        uintptr_t shiftcls          : 33; // 存储着 Class、Meta-Class 对象的内存地址信息
        uintptr_t magic             : 6;  // 用于在调试时分辨对象是否未完成初始化
        uintptr_t weakly_referenced : 1;  // 是否有被弱引用指向过,如果没有,释放时会更快
        uintptr_t deallocating      : 1;  // 对象是否正在释放
        uintptr_t has_sidetable_rc  : 1;  // 如果为1,代表引用计数过大无法存储在 isa 中,那么超出的引用计数会存储在一个叫 SideTable 结构体的 RefCountMap(引用计数表)散列表中
        uintptr_t extra_rc          : 19; // 里面存储的值是对象本身之外的引用计数的数量,retainCount - 1
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
......  // 在 __x86_64__ 架构下
};

如果isa非nonpointer,即 arm64 架构之前的isa指针。由于它只是一个普通的指针,存储着Class、Meta-Class对象的内存地址,所以它本身不能存储引用计数,所以以前对象的引用计数都存储在一个叫SideTable结构体的RefCountMap(引用计数表)散列表中。

如果isa是nonpointer,则它本身可以存储一些引用计数。从以上union isa_t的定义中我们可以得知,isa_t中存储了两个引用计数相关的东西:extra_rc和has_sidetable_rc。

  • extra_rc:里面存储的值是对象本身之外的引用计数的数量,这 19 位如果不够存储,has_sidetable_rc的值就会变为 1;
  • has_sidetable_rc:如果为 1,代表引用计数过大无法存储在isa中,那么超出的引用计数会存储SideTable的RefCountMap中。

所以,如果isa是nonpointer,则对象的引用计数存储在它的isa_t的extra_rc中以及SideTable的RefCountMap中。

(3)什么是MRC,MRC下的内存管理策略是什么,有什么需要注意的?

内存管理策略

(一)创建并持有对象 使用 alloc/new/copy/mutableCopy 等方法(或者以这些方法名开头的方法)创建的对象我们直接持有,其RC(引用计数,以下统一使用RC)初始值为 1,我们直接使用即可,在不需要使用的时候调用一下release方法进行释放。

    id obj = [NSObject alloc] init]; // 创建并持有对象,RC = 1
    /*
     * 使用该对象,RC = 1
     */
    [obj release]; // 在不需要使用的时候调用 release,RC = 0,对象被销毁

如果我们想要通过自定义方法创建,并持有对象,则方法名应该以 alloc/new/copy/mutableCopy 开头,且应该遵循驼峰命名法规则,返回的对象也应该由alloc/new/copy/mutableCopy等方法创建,如:

- (id)allocObject
{
    id obj = [NSObject alloc] init];
    return obj;
}

如果我们想通过自定义方法创建,但并不持有对象,则方法名就不应该以 alloc/new/copy/mutableCopy 开头,且返回对象前应该要先通过autorelease方法将该对象加入自动释放池。如:

- (id)getObject
{
    id obj = [NSObject alloc] init];
    [obj autorelease];
    return obj;
}

这样调用方在使用该方法创建对象的时候,通过方法名他就会知道他不持有该对象,于是他会在使用该对象前进行retain,并在不需要该对象时进行release。

(二)可以使用 retain 持有对象 我们可以使用retain对一个对象进行持有。使用一些自定义的方法,如上述getObject方法,创建的对象,我们并不持有,其RC初始值也为 1。但是需要注意的是,如果要使用该对象,需要先进行retain,否则可能会导致程序Crash。原因是在getObject方法内部,将对象加入到了自动释放池中,自动释放池会在合适的时机给释放池中的对象发送release消息,一旦对象的RC降为0,就会被销毁。 ① 情况一:iOS 程序中不手动指定@autoreleasepool 在主线程中,当RunLoop迭代结束时,会自动给自动释放池中的对象调用release方法。所以如果我们在使用前不进行retain,那么当RunLoop迭代结束,对象就会收到release消息,如果此时该对象RC值降为 0 就会被销毁。而我们这时候再去访问已经被销毁的对象,程序就会Crash。

    id obj = [NSMutableArray array]; // 创建对象但并不持有,array方法内部将对象加入自动释放池,RC = 1

    [obj retain]; // 使用之前进行 retain,对对象进行持有,RC = 2
    /*
     * 使用该对象,RC = 2
     */
    [obj release]; // 在不需要使用的时候调用 release,RC = 1
    /*
     * RunLoop 可能在某一时刻迭代结束,给自动释放池中的对象调用 release,RC = 0,对象被销毁
     * 如果这时候 RunLoop 还未迭代结束,该对象还可以被访问,不过这是非常危险的,容易导致 Crash
     */

② 情况二:手动指定@autoreleasepool 这种情况就更加明显了,如果@autoreleasepool作用域结束,就会自动给autorelease对象调用release方法。如果这时候我们再访问该对象,程序就会Crash。

    /* 错误的用法 */

    id obj;
    @autoreleasepool {
        obj = [NSMutableArray array]; // 创建对象但并不持有,对象加入自动释放池,RC = 1
    } // @autoreleasepool 作用域结束,对象 release,RC = 0,对象被销毁
    NSLog(@"%@",obj); // EXC_BAD_ACCESS

    /* 正确的用法 */

    id obj;
    @autoreleasepool {
        obj = [NSMutableArray array]; // 创建对象但并不持有,对象加入自动释放池,RC = 1
        [obj retain]; // RC = 2
    } // @autoreleasepool 作用域结束,对象 release,RC = 1
    NSLog(@"%@",obj); // 正常访问
    /*
     * 使用该对象,RC = 1
     */
    [obj release]; // 在不需要使用的时候调用 release,RC = 0,对象被销毁

(三)不再需要自己持有的对象时释放 在不需要使用(持有)对象的时候,需要调用一下release或者autorelease方法进行释放(或者称为 “放弃对象使用权”),使其RC-1,防止内存泄漏。当对象的RC为 0 时,就会调用dealloc方法销毁对象。

(四)不能释放非自己持有的对象 从以上我们可以得知,持有对象有两种方式,一是通过 alloc/new/copy/mutableCopy 等方法创建对象,二是通过retain方法。如果自己是持有者,那么在不需要该对象的时候需要调用一下release方法进行释放。但是,如果自己不是持有者,就不能对对象进行release,否则会导致程序Crash。另外,向业已回收的对象发送消息也是不安全的,如下两种情况:

    id obj = [[NSObject alloc] init]; // 创建并持有对象,RC = 1
    [obj release]; // 如果自己是持有者,在不需要使用的时候调用 release,RC = 0
    /*
     * 此时对象已被销毁,不应该再对其进行访问
     */
    [obj release]; // EXC_BAD_ACCESS,这时候自己已经不是持有者,再 release 就会 Crash
    /*
     * 再次 release 已经销毁的对象(过度释放),或是访问已经销毁的对象都会导致崩溃
     */

    id obj = [NSMutableArray array]; // 创建对象,但并不持有对象,RC = 1
    [obj release]; // EXC_BAD_ACCESS 虽然对象的 RC = 1,但是这里并不持有对象,所以导致 Crash

实用的内存管理技巧

使用 autorelease 发送延迟 release

String对象是使用alloc方法创建的,因此在不需要该对象时发送一条release消息。

- (NSString *)fullName {
    NSString *string = [[NSString alloc] initWithFormat:@"%@ %@",                                       self.firstName, self.lastName]; 
    [string release];
    return string;
}

根据内存管理规则,你通过alloc方法创建并持有对象,要在不需要该对象时发送一条release消息。但是如果你在方法中使用release,则return之前就会销毁 NSString 对象,该方法将返回无效对象。 当你需要发送延迟release消息时,可以使用autorelease,通常用在从方法返回对象时。例如,

- (NSString *)fullName {
    NSString *string = [[NSString alloc] initWithFormat:@"%@ %@",
                                          self.firstName, self.lastName];
   [string autorelease];
   return string;
}

你还可以这样实现fullName对象方法

- (NSString *)fullName {
    NSString *string = [NSString stringWithFormat:@"%@ %@",
                                 self.firstName, self.lastName];
    return string;
}

根据内存管理规则,你不持有 NSString 对象,因此你不用担心它的释放,直接return即可。stringWithFormat 方法内部会给 NSString 对象调用autorelease方法。

相比之下,以下实现是错误的:

- (NSString *)fullName {
    NSString *string = [[NSString alloc] initWithFormat:@"%@ %@",
                                         self.firstName, self.lastName];
    return string;
}

原因是在 fullName 方法内部我们通过alloc方法创建对象并持有,然而并没有释放对象。而该方法名不以 alloc/new/copy/mutableCopy 等开头。在调用方看来,通过该方法获得的对象并不持有,因此他会进行retain并在他不需要该对象时release,在他看来这样使用该对象没有内存问题。然而这时候该对象的引用计数为 1,并没有销毁,就发生了内存泄漏。

你不持有通过引用返回的对象

Cocoa 中的一些方法指定通过引用返回对象(它们采用ClassName **或id *类型的参数)。常见的就是使用NSError对象,该对象包含有关错误的信息(如果发生错误),如initWithContentsOfURL:options:error:(NSData)和initWithContentsOfFile:encoding:error:(NSString)方法等。 在这些情况下,也遵从内存管理规则。当你调用这些方法时,你不会创建该NSError对象,因此你不持有该对象,也无需释放它,如以下示例所示:

    NSString *fileName = <#Get a file name#>;
    NSError *error;
    NSString *string = [[NSString alloc] initWithContentsOfFile:fileName
                            encoding:NSUTF8StringEncoding error:&error];
    if (string == nil) {
        // Deal with error...
    }
    // ...
    [string release];

实现 dealloc 以放弃对象的所有权

NSObject 类定义了一个dealloc方法,该方法会在一个对象没有所有者(RC=0)并且它的内存被回收时由系统自动调用 —— 在 Cocoa 术语中称为freed或deallocated。 dealloc方法的作用是销毁对象自身的内存,并释放它持有的任何资源,包括任何实例变量的所有权。 以下举了一个在 Person 类中实现 dealloc方法的示例:

@interface Person : NSObject
@property (retain) NSString *firstName;
@property (retain) NSString *lastName;
@property (assign, readonly) NSString *fullName;
@end
 
@implementation Person
// ...
- (void)dealloc
    [_firstName release];
    [_lastName release];
    [super dealloc];
}

注意: 切勿直接调用另一个对象dealloc的方法; 你必须在实现结束时调用[super dealloc]; 你不应该将系统资源的管理与对象生命周期联系在一起,请参阅 不要使用dealloc 管理稀缺资源 章节; 当应用程序终止时,可能不会向对象发送dealloc消息。因为进程的内存在退出时会自动清除,所以让操作系统清理资源比调用所有对象的dealloc方法更有效。

使用访问器方法让内存管理更轻松

如果类中有对象类型的属性,则你必须确保在使用过程中该属性赋值的对象不被释放。因此,在赋值对象时,你必须持有对象的所有权,让其引用计数加 1。还必须要把当前持有的旧对象的引用计数减 1。 有时它可能看起来很乏味或繁琐,但如果你始终使用访问器方法,那么内存管理出现问题的可能性会大大降低。如果你在整个代码中对实例变量使用retain和release,这肯定是错误的做法。 以下在 Counter 类中定义了一个NSNumber对象属性。

@interface Counter : NSObject
@property (nonatomic, retain) NSNumber *count;
@end;

@property会自动生成setter和getter方法的声明,通常,你应该使用@synthesize让编译器合成方法。但如果我们了解访问器方法的实现是有益的。

@synthesize会自动生成setter和getter方法的实现以及下划线实例变量,但是如果setter和getter方全被重写,则不会生成下划线实力变量,这点需要注意;

getter方法只需要返回合成的实例变量,所以不用进行retain和release。

- (NSNumber *)count {
    return _count;
}

setter方法中,如果其他所有人都遵循相同的规则,那么其他人很可能随时让新对象 newCount 的引用计数减 1,从而导致 newCount 被销毁,所以你必须对其retain使其引用计数加 1。你还必须对旧对象release以放弃对它的持有。所以,先对新对象进行retain,再对旧对象进行release,然后再进行赋值操作。(在Objective-C中允许给nil发送消息,且这样会直接返回不做任何事情。所以就算是第一次调用,_count 变量为nil,对其进行 release也没事。可以参阅《深入浅出 Runtime(三):消息机制》

注意: 你必须先对新对象进行retain,再对旧对象进行release。顺序颠倒的话,如果新旧对象是同一对象,则可能会发生意外导致对象dealloc。

- (void)setCount:(NSNumber *)newCount {
    [newCount retain];
    [_count release];
    // Make the new assignment.
    _count = newCount;
}

以上是苹果官方的做法,该做法在性能上略有不足,如果新旧对象是同一个对象,就存在不必要的方法调用。 更好的做法如下:先判断新旧对象是否是同一个对象,如果是的话就什么都不做;如果新旧对象不是同一个对象,则对旧对象进行release,对新对象进行retain并赋值给合成的实例变量。

- (void)setCount:(NSNumber *)newCount {
    if (_count != newCount) {
        [_count release];
        _count = [newCount retain];
    }
}
  • 使用访问器方法设置属性值

假设我们要重置以上count属性的值。有以下两种方法:

- (void)reset {
    NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
    [self setCount:zero];
    [zero release];
}
- (void)reset {
    NSNumber *zero = [NSNumber numberWithInteger:0];
    [self setCount:zero];
}

对于简单的情况,我们还可以像下面这样直接操作_count变量,但这样做迟早会发生错误(例如,当你忘记retain或release,或者实例变量的内存管理语义(即属性关键字)发生更改时)。

- (void)reset {
    NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
    [_count release];
    _count = zero;
}

另外请注意,如果使用KVO,则以这种方式更改变量不会触发KVO监听方法。关于KVO我做了比较全面的总结,可以参阅《iOS - 关于 KVO 的一些总结》

  • 不要在初始化方法和 dealloc 中使用访问器方法

你不应该在初始化方法和 dealloc 中使用访问器方法来设置实例变量,而是应该直接操作实例变量。

例如,我们要在初始化 Counter 对象时,初始化它的count属性。正确的做法如下:

- (instancetype)init {
    self = [super init];
    if (self) {
        _count = [[NSNumber alloc] initWithInteger:0];
    }
    return self;
}

由于 Counter 类具有实例变量,因此还必须实现dealloc方法。在该方法中通过向它们发送release消息来放弃任何实例变量的所有权,并在最后调用super的实现:

- (void)dealloc {
    [_count release];
    [super dealloc];
}

以上是苹果官方的做法。推荐做法如下,在release之后再对 _count 赋值nil。

备注:先解释一下nil和release的作用: nil是将一个对象的指针置为空,只是切断了指针和内存中对象的联系,并没有释放对象内存;而release才是真正释放对象内存的操作。之所以在release之后再对 _count 赋值nil,是为了防止 _count 在被销毁之后再次被访问而导致Crash。

- (void)dealloc {
    [_count release];
    _count = nil;
    [super dealloc];
}

我们也可以在dealloc通过self.count = nil;一步到位,因为通常它相当于[_count release];和_count = nil;两步操作。但尽量不要这么做。

- (void)dealloc {
    self.count = nil;
    [super dealloc];
}

Why? 为什么初始化方法中需要self = [super init]?

  • 先大概解释一下self和super。self是对象指针,指向当前消息接收者。super是编译器指令,使用super调用方法是从当前消息接收者类的父类中开始查找方法的实现,但消息接收者还是子类。有关self和super的详细解释可以参阅《深入浅出Runtime(四):super 的本质》

  • 调用[super init],是子类去调用父类的init方法,先完成父类的初始化工作。要注意调用过程中,父类的init方法中的self还是子类。

  • 执行self= [super init],如果父类初始化成功,接下来就进行子类的初始化;如果父类初始化失败,则[super init]会返回nil并赋值给self,接下来if (self)语句的内容将不被执行,子类的init方法也返回nil。这样做可以防止因为父类初始化失败而返回了一个不可用的对象。如果你不是这样做,你可能你会得到一个不可用的对象,并且它的行为是不可预测的,最终可能会导致你的程序发生Crash。

Why? 为什么不要在初始化方法和 dealloc 中使用访问器方法?

  • 在初始化方法和dealloc中,对象的存在与否还不确定,它可能还未初始化完毕,所以给对象发消息可能不会成功,或者导致一些问题的发生。
  1. 进一步解释,假如我们在init中使用setter方法初始化实例变量。在init中,我们会调用self = [super init]对父类的东西先进行初始化,即子类先调用父类的init方法(注意: 调用的父类的init方法中的self还是子类对象)。如果父类的init中使用setter方法初始化实例变量,且子类重写了该setter方法,那么在初始化父类的时候就会调用子类的setter方法。而此时只是在进行父类的初始化,子类初始化还未完成,所以可能会发生错误。
  2. 在销毁子类对象时,首先是调用子类的dealloc,最后调用[super dealloc](这与init相反)。如果在父类的dealloc中调用了setter方法且该方法被子类重写,就会调用到子类的setter方法,但此时子类已经被销毁,所以这也可能会发生错误。
  3. 在 《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》书中的第 31 条 —— 在 dealloc 方法中只释放引用并解除监听 一文中也提到:在 dealloc 里不要调用属性的存取方法,因为有人可能会覆写这些方法,并于其中做一些无法在回收阶段安全执行的操作。此外,属性可能正处于 “键值观测”(Key-Value Observation,KVO)机制的监控之下,该属性的观察者(observer)可能会在属性值改变时 “保留” 或使用这个即将回收的对象。这种做法会令运行期系统的状态完全失调,从而导致一些莫名其妙的错误。
  4. 综上,错误的原因通常由继承和子类重写访问器方法引起。在初始化方法和 dealloc 中使用访问器方法的话,如果存在继承且子类重写了访问器方法,且在方法中做了一些其它操作,就很有可能发生错误。虽然一般情况下我们可能不会同时满足以上条件而导致错误,但是为了避免错误的发生,我们还是规范编写代码比较好。
  • 性能下降。特别是,如果属性是atomic的。
  • 可能产生副作用。如使用KVO的话会触发KVO等。

不过,有些情况我们必须破例。比如:

  • 待初始化的实例变量声明在父类中,而我们又无法在子类中访问此实例变量的话,那么我们在初始化方法中只能通过setter来对实例变量赋值。

使用弱引用来避免 Retain Cycles

retain对象会创建对该对象的强引用(即引用计数 +1)。一个对象在release它的所有强引用之后(即引用计数 =0)才会dealloc。如果两个对象相互retain强引用,或者多个对象,每个对象都强引用下一个对象直到回到第一个,就会出现 “Retain Cycles(循环引用)” 问题。循环引用会导致它们中的任何对象都无法dealloc,就产生了内存泄漏。 举个例子,Document 对象中有一个属性 Page 对象,每个 Page 对象都有一个属性,用于存储它所在的 Document。如果 Document 对象具有对 Page 对象的强引用,并且 Page 对象具有对 Document 对象的强引用,则它们都不能被销毁。 “Retain Cycles” 问题的解决方案是使用弱引用。弱引用是非持有关系,对象do not retain它引用的对象。

但是,为了保持对象图完好无损,必须在某处有强引用(如果只有弱引用,则 Page 对象和 Paragraph 对象可能没有任何所有者,因此将被销毁)。因此,Cocoa 建立了一个约定,即父对象应该对其子对象保持强引用(retain),而子对象应该对父对象保持弱引用(do not retain)。 因此,Document 对象具有对其 Page 对象的强引用,但 Page 对象对 Document 对象是弱引用,如下图所示: 在这里插入图片描述 Cocoa 中弱引用的示例包括但不限于 table data sourcesoutline view itemsnotification observers 以及其他 targetsdelegates

当你向只持有弱引用的对象发送消息时,需要小心。如果在对象销毁后向其发送消息就会Crash。你必须定义好什么时候对象是有效的。在大多数情况下,弱引用对象知道其它对象对它的弱引用,就像循环引用的情况一样,你要负责在弱引用对象销毁时通知其它对象。例如,当你向通知中心注册对象时,通知中心会存储对该对象的弱引用,并在发布相应的通知时向其发送消息。在对象要销毁时,你需要在通知中心注销它,以防止通知中心向已销毁的对象发送消息。同样,当 delegate 对象销毁时,你需要向委托对象发送setDelegate: nil消息来删除 delegate 引用。这些消息通常在对象的 dealloc 方法中发送。

避免导致你正在使用的对象被销毁

Cocoa 的所有权策略指定,对象作为方法参数传入,其在调用的方法的整个范围内保持有效,也可以作为方法的返回值返回,而不必担心它被释放。对于应用程序来说,对象的 getter 方法返回缓存的实例变量或计算值并不重要。重要的是对象在你需要的时间内保持有效。

此规则偶尔会有例外情况,主要分为两类。

  1. 从一个基本集合类中删除对象时。
    heisenObject = [array objectAtIndex:n];
    [array removeObjectAtIndex:n];
    // heisenObject could now be invalid.

当一个对象从一个基本集合类中移除时,它将被发送一条release(而不是autorelease)消息。如果集合是移除对象的唯一所有者,则移除的对象(示例中的 heisenObject)将立即被销毁。

  1. 当 “父对象” 被销毁时。
    id parent = <#create a parent object#>;
    // ...
    heisenObject = [parent child] ;
    [parent release]; // Or, for example: self.parent = nil;
    // heisenObject could now be invalid.

在某些情况下,你通过父对象获得子对象,然后直接或间接release父对象。如果release父对象导致它被销毁,并且父对象是子对象的唯一所有者,则子对象(示例中的 heisenObject)将同时被销毁(假设在父对象的dealloc方法中,子对象被发送一个release而不是一个autorelease消息)。 为了防止这些情况发生,在得到 heisenObject 时retain它,并在完成后release它。例如:

    heisenObject = [[array objectAtIndex:n] retain];
    [array removeObjectAtIndex:n];
    // Use heisenObject...
    [heisenObject release];

不要使用 dealloc 来管理稀缺资源

你通常不应该在dealloc方法中管理稀缺资源,如文件描述符,网络连接和缓冲区或缓存等。特别是,你不应该设计类,以便在你想让系统调用dealloc时就调用它。由于bug或应用程序崩溃,dealloc的调用可能会被延迟或未调用。

相反,如果你有一个类的实例管理稀缺的资源,你应该在你不再需要这些资源时让该实例释放这些资源。然后,你通常会release该实例,紧接着它dealloc。如果该实例的dealloc没有被及时调用或者未调用,你也不会遇到稀缺资源不被及时释放或者未释放的问题,因为此前你已经释放了资源。 如果你尝试在dealloc上进行资源管理,则可能会出现问题。例如:

  1. 依赖对象图的释放机制。 对象图的释放机制本质上是无序的。尽管通常你希望可以按照特定的顺序释放,但是会让程序变得很脆弱。如果对象被autorelease而不是release,则释放顺序可能会改变,这可能会导致意外的结果。

  2. 不回收稀缺资源。 内存泄漏是应该被修复的bug,但它们通常不会立即致命。然而,如果在你希望释放稀缺资源时没有释放,则可能会遇到更严重的问题。例如,如果你的应用程序用完了文件描述符,则用户可能无法保存数据。

  3. 释放资源的操作被错误的线程执行。 如果一个对象在一个意外的时间调用了autorelease,它将在它碰巧进入的任何一个线程的自动释放池块中被释放。对于只能从一个线程触及的资源来说,这很容易致命。

集合持有它们包含的对象

将对象添加到集合(例如array,dictionary或set)时,集合将获得对象的所有权。当从集合中移除对象或集合本身被销毁时,集合将放弃对象的所有权。因此,例如,如果要创建一个存储numbers的数组,可以执行以下任一操作:

    NSMutableArray *array = <#Get a mutable array#>;
    NSUInteger i;
    // ...
    for (i = 0; i < 10; i++) {
        NSNumber *convenienceNumber = [NSNumber numberWithInteger:i];
        [array addObject:convenienceNumber];
    }

在这种情况下,NSNumber对象不是通过alloc等创建,因此无需调用release。也不需要对NSNumber对象进行retain,因为数组会这样做。

    NSMutableArray *array = <#Get a mutable array#>;
    NSUInteger i;
    // ...
    for (i = 0; i < 10; i++) {
        NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger:i];
        [array addObject:allocedNumber];
        [allocedNumber release];
    }

在这种情况下,你就需要对NSNumber对象进行release。数组会在addObject: 时对NSNumber对象进行retain,因此在数组中它不会被销毁。

要理解这一点,可以站在实现集合类的人的角度。你要确保在集合中它们不会被销毁,所以你在它们添加进集合时给它们发送一个retain消息。如果删除了它们,则必须给它们发送一个release消息。在集合的dealloc方法中,应该向集合中所有剩余的对象发送一条release消息。

所有权策略是通过使用 Retain Counts 实现的

所有权策略通过引用计数实现的,引用计数也称为“retain count”。每个对象都有一个retain count。

  • 创建对象时,其retain count为 1。
  • 向对象发送retain消息时,其retain count将 +1。
  • 向对象发送release消息时,其retain count将 -1。
  • 向对象发送autorelease消息时,其retain count在当前自动释放池块结束时 -1。
  • 如果对象的retain count减少到 0,它将dealloc。

重要提示: 不应该显式询问对象的retain count是多少。结果往往会产生误导,因为你可能不知道哪些系统框架对象retain了你关注的对象。在调试内存管理问题时,你只需要遵守内存管理规则就行了。

备注: 关于这些方法的具体实现,可以参阅《iOS - 老生常谈内存管理(四):源码分析内存管理方法》

Core Foundation 使用相似但不同的规则

Core Foundation 对象有类似的内存管理规则(请参阅 《 Core Foundation 内存管理编程指南》)。但是,Cocoa 和 Core Foundation 的命名约定不同。特别是 Core Foundation 的创建对象的规则(请参阅 《The Create Rule》)。

(4)ARC的作用是什么,ARC有哪些新规则, ARC会带来什么问题?

ARC的工作原理是在编译时添加相关代码,以确保对象能够在必要时存活,但不会一直存活。从概念上讲,它通过为你添加适当的内存管理方法调用来遵循与MRC相同的内存管理规则。

为了让编译器生成正确的代码,ARC限制了一些方法的使用以及你使用桥接(toll-free bridging)的方式,请参阅 Managing Toll-Free Bridging 章节。ARC还为对象引用和属性声明引入了新的生命周期修饰符。

ARC在Xcode 4.2 for OS X v10.6 and v10.7 (64-bit applications) 以及iOS 4 and iOS 5应用程序中提供支持。但OS X v10.6 and iOS 4不支持weak弱引用。 Xcode 提供了一个迁移工具,可以自动将MRC代码转换为ARC代码(如删除retain和release调用),而不用重新再创建一个项目(选择 Edit > Convert > To Objective-C ARC)。迁移工具会将项目中的所有文件转换为使用ARC的模式。如果对于某些文件使用MRC更方便的话,你可以选择仅在部分文件中使用ARC。

ARC会分析对象的生存期需求,并在编译时自动插入适当的内存管理方法调用的代码,而不需要你记住何时使用retain、release、autorelease方法。编译器还会为你生成合适的dealloc方法。一般来说,如果你使用ARC,那么只有在需要与使用MRC的代码进行交互操作时,传统的 Cocoa 命名约定才显得重要。

ARC新规则

ARC引入了一些在使用其他编译器模式时不存在的新规则。这些规则旨在提供完全可靠的内存管理模型。有时候,它们直接地带来了最好的实践体验,也有时候它们简化了代码,甚至在你丝毫没有关注内存管理问题的时候帮你解决了问题。在ARC下必须遵守以下规则,如果违反这些规则,就会编译错误。

  • 不能使用 retain / release / retainCount / autorelease
  • 不能使用 NSAllocateObject / NSDeallocateObject
  • 须遵守内存管理的方法命名规则
  • 不能显式调用 dealloc
  • 使用 @autoreleasepool 块替代 NSAutoreleasePool
  • 不能使用区域(NSZone)
  • 对象型变量不能作为 C 语言结构体(struct / union)的成员
  • 显式转换 “id” 和 “void *” —— 桥接

不能使用 retain / release / retainCount / autorelease

在ARC下,禁止开发者手动调用这些方法,也禁止使用@selector(retain),@selector(release) 等,否则编译不通过。但你仍然可以对 Core Foundation 对象使用CFRetain、CFRelease等相关函数(请参阅《Managing Toll-Free Bridging》章节)。

不能使用 NSAllocateObject / NSDeallocateObject

在ARC下,禁止开发者手动调用这些函数,否则编译不通过。 你可以使用alloc创建对象,而Runtime会负责dealloc对象。

须遵守内存管理的方法命名规则

在MRC下,通过 alloc / new / copy / mutableCopy 方法创建对象会直接持有对象,我们定义一个 “创建并持有对象” 的方法也必须以 alloc / new / copy / mutableCopy 开头命名,并且必须返回给调用方所应当持有的对象。如果在ARC下需要与使用MRC的代码进行交互,则也应该遵守这些规则。 为了允许与MRC代码进行交互操作,ARC对方法命名施加了约束: 访问器方法的方法名不能以new开头。这意味着你不能声明一个名称以new开头的属性,除非你指定一个不同的getterName:

// Won't work:
@property NSString *newTitle;
// Works:
@property (getter = theNewTitle) NSString *newTitle;

不能显式调用 dealloc

无论在MRC还是ARC下,当对象引用计数为 0,系统就会自动调用dealloc方法。大多数情况下,我们会在dealloc方法中移除通知或观察者对象等。 在MRC下,我们可以手动调用dealloc。但在ARC下,这是禁止的,否则编译不通过。 在MRC下,我们实现dealloc,必须在实现末尾调用[super dealloc]。

// MRC
- (void)dealloc
{
    // 其他处理
    [super dealloc];
}

而在ARC下,ARC会自动对此处理,因此我们不必也禁止写[super dealloc],否则编译错误。

// ARC
- (void)dealloc
{
    // 其他处理
    [super dealloc]; // 编译错误:ARC forbids explicit message send of 'dealloc'
}

使用 @autoreleasepool 块替代 NSAutoreleasePool

在ARC下,自动释放池应使用@autoreleasepool,禁止使用NSAutoreleasePool,否则编译错误。

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 
// error:'NSAutoreleasePool' is unavailable: not available in automatic reference counting mode

关于@autoreleasepool的原理,可以参阅《iOS - 聊聊 autorelease 和 @autoreleasepool

不能使用区域(NSZone)

对于现在的运行时系统(编译器宏 __ OBJC2 __ 被设定的环境),不管是MRC还是ARC下,区域(NSZone)都已单纯地被忽略。

对象型变量不能作为 C 语言结构体(struct / union)的成员

C 语言的结构体(struct / union)成员中,如果存在 Objective-C 对象型变量,便会引起编译错误。

备注: Xcode10 开始支持在 ARC 模式下在 C Struct 里面引用 Objective-C 对象。之前可以用 Objective-C++。

struct Data {
    NSMutableArray *mArray;
};
// error:ARC forbids Objective-C objs in struct or unions NSMutableArray *mArray;

虽然是 LLVM 编译器 3.0,但不论怎样,C 语言的规约上没有方法来管理结构体成员的生存周期。因为ARC把内存管理的工作分配给编译器,所以编译器必须能够知道并管理对象的生存周期。例如 C 语言的自动变量(局部变量)可使用该变量的作用域管理对象。但是对于 C 语言的结构体成员来说,这在标准上就是不可实现的。因此,必须要在结构体释放之前将结构体中的对象类型的成员释放掉,但是编译器并不能可靠地做到这一点,所以对象型变量不能作为 C 语言结构体的成员。 这个问题有以下三种解决方案:

① 使用 Objective-C 对象替代结构体。这是最好的解决方案。如果你还是坚持使用结构体,并把对象型变量加入到结构体成员中,可以使用以下两种方案:

② 将 Objective-C 对象通过Toll-Free Bridging强制转换为void *类型,请参阅《Managing Toll-Free Bridging》章节。

③ 对 Objective-C 对象附加__unsafe_unretained修饰符。

struct Data {
    NSMutableArray __unsafe_unretained *mArray;
};

附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。如果管理时不注意赋值对象的所有者,便有可能遭遇内存泄漏或者程序崩溃。这点在使用时应多加注意。

struct x { NSString * __unsafe_unretained S; int X; }

__unsafe_unretained指针在对象被销毁后是不安全的,但它对诸如字符串常量之类的从一开始就确定永久存活的对象非常有用。

不能显式转换 “id” 和 “void *”

在MRC下,我们可以直接在 id 和 void * 变量之间进行强制转换。

    id obj = [[NSObject alloc] init];
    void *p = obj;
    id o = p;
    [o release];

但在ARC下,这样会引起编译报错:在Objective-C指针类型id和C指针类型void *之间进行转换需要使用Toll-Free Bridging,请参阅 Managing Toll-Free Bridging 章节。

    id obj = [[NSObject alloc] init];
    void *p = obj; // error:Implicit conversion of Objective-C pointer type 'id' to C pointer type 'void *' requires a bridged cast
    id o = p;      // error:Implicit conversion of C pointer type 'void *' to Objective-C pointer type 'id' requires a bridged cast
    [o release];   // error:'release' is unavailable: not available in automatic reference counting mode

所有权修饰符

ARC为对象引入了几个新的生命周期修饰符(我们称为 “所有权修饰符”)以及弱引用功能。弱引用weak不会延长它指向的对象的生命周期,并且该对象没有强引用(即dealloc)时自动置为nil。

你应该利用这些修饰符来管理程序中的对象图。特别是,ARC不能防止强引用循环(以前称为Retain Cycles,请参阅《从 MRC 说起 —— 使用弱引用来避免 Retain Cycles》章节)。明智地使用弱引用weak将有助于确保你不会创建循环引用。

在ARC中,对象类型的变量都附有所有权修饰符,总共有以下 4 种。

__strong
__weak
__unsafe_unretained
__autoreleasing
  • __strong是默认修饰符。只要有强指针指向对象,对象就会保持存活。
  • __weak指定一个不使引用对象保持存活的引用。当一个对象没有强引用时,弱引用weak会自动置为nil。
  • __unsafe_unretained指定一个不使引用对象保持存活的引用,当一个对象没有强引用时,它不会置为nil。如果它引用的对象被销毁,就会产生悬垂指针。
  • __autoreleasing用于表示通过引用(id *)传入,并在返回时(autorelease)自动释放的参数。

在对象变量的声明中使用所有权修饰符时,正确的格式为:

  ClassName * qualifier variableName;

例如:

MyClass * __weak myWeakReference;
MyClass * __unsafe_unretained myUnsafeReference;

其它格式在技术上是不正确的,但编译器会 “原谅”。也就是说,以上才是标准写法。

__strong

__strong修饰符为强引用,会持有对象,使其引用计数 +1。该修饰符是对象类型变量的默认修饰符。如果我们没有明确指定对象类型变量的所有权修饰符,其默认就为__strong修饰符。

id obj = [NSObject alloc] init];
// -> id __strong obj = [NSObject alloc] init];

__weak

如果单单靠__strong完成内存管理,那必然会发生循环引用的情况造成内存泄漏,这时候__weak就出来解决问题了。

__weak修饰符为弱引用,不会持有对象,对象的引用计数不会增加。__weak可以用来防止循环引用。

以下单纯地使用__weak修饰符修饰变量,编译器会给出警告,因为NSObject的实例创建出来没有强引用,就会立即释放。

id __weak weakObj = [[NSObject alloc] init]; // ⚠️Assigning retained object to weak variable; object will be released after assignment
NSLog(@"%@", obj);
//  (null)

以下NSObject的实例已有强引用,再赋值给__weak修饰的变量就不会有警告了。

id __strong strongObj = [[NSObject alloc] init];
id __weak weakObj = strongObj;

当对象被dealloc时,指向该对象的__weak变量会被赋值为nil。(具体的执行过程可以参阅:《iOS - 老生常谈内存管理(四):源码分析内存管理方法》)

备注:__weak仅在ARC中才能使用,在MRC中是使用__unsafe_unretained修饰符来代替。

__unsafe_unretained

__unsafe_unretained修饰符的特点正如其名所示,不安全且不会持有对象。

注意: 尽管ARC内存管理是编译器的工作,但是附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。这一点在使用时要注意。

“不会持有对象” 这一特点使它和__weak的作用相似,可以防止循环引用。 “不安全“ 这一特点是它和__weak的区别,那么它不安全在哪呢? 我们来看代码:

    id __weak weakObj = nil;
    id __unsafe_unretained uuObj = nil;
    {
        id __strong strongObj = [[NSObject alloc] init];
        weakObj = strongObj;
        unsafeUnretainedObj = strongObj;
        NSLog(@"strongObj:%@", strongObj);
        NSLog(@"weakObj:%@", weakObj);
        NSLog(@"unsafeUnretainedObj:%@", unsafeUnretainedObj);
    }
    NSLog(@"-----obj dealloc-----");
    NSLog(@"weakObj:%@", weakObj);
    NSLog(@"unsafeUnretainedObj:%@", unsafeUnretainedObj); // Crash:EXC_BAD_ACCESS

/*
strongObj:<NSObject: 0x6000038f4340>
weakObj:<NSObject: 0x6000038f4340>
unsafeUnretainedObj:<NSObject: 0x6000038f4340>
-----obj dealloc-----
weakObj:(null)
(lldb) 
*/

以上代码运行崩溃。原因是__unsafe_unretained修饰的对象在被销毁之后,指针仍然指向原对象地址,我们称它为 “悬垂指针”。这时候如果继续通过指针访问原对象的话,就会导致Crash。而__weak修饰的对象在被释放之后,会将指向该对象的所有__weak指针变量全都置为nil。这就是__unsafe_unretained不安全的原因。所以,在使用__unsafe_unretained修饰符修饰的对象时,需要确保它未被销毁。

Q: 既然 __weak 更安全,那么为什么已经有了 __weak 还要保留 __unsafe_unretained ?

  • __weak仅在ARC中才能使用,而MRC只能使用__unsafe_unretained;

  • __unsafe_unretained主要跟 C 代码交互;

  • __weak对性能会有一定的消耗,当一个对象dealloc时,需要遍历对象的weak表,把表里的所有weak指针变量值置为nil,指向对象的weak指针越多,性能消耗就越多。所以__unsafe_unretained比__weak快。当明确知道对象的生命周期时,选择__unsafe_unretained会有一些性能提升。

A 持有 B 对象,当 A 销毁时 B 也销毁。这样当 B 存在,A 就一定会存在。而 B 又要调用 A 的接口时,B 就可以存储 A 的__unsafe_unretained指针。 比如,MyViewController 持有 MyView,MyView 需要调用 MyViewController 的接口。MyView 中就可以存储__unsafe_unretained MyViewController *_viewController。

虽然这种性能上的提升是很微小的。但当你很清楚这种情况下,__unsafe_unretained也是安全的,自然可以快一点就是一点。而当情况不确定的时候,应该优先选用__weak。

__autoreleasing

自动释放池

首先讲一下自动释放池,在ARC下已经禁止使用NSAutoreleasePool类创建自动释放池,而用@autoreleasepool替代。

MRC下可以使用NSAutoreleasePool或者@autoreleasepool。建议使用@autoreleasepool,苹果说它比NSAutoreleasePool快大约六倍。

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Code benefitting from a local autorelease pool.
[pool release]; // [pool drain]

Q: 释放NSAutoreleasePool对象,使用[pool release]与[pool drain]的区别 Objective-C语言本身是支持 GC 机制的,但有平台局限性,仅限于 MacOS 开发中,iOS 开发用的是 RC 机制。在 iOS 的 RC环境下[pool release]和[pool drain]效果一样,但在 GC 环境下drain会触发 GC 而release不做任何操作。 使用[pool drain]更佳,一是它的功能对系统兼容性更强,二是这样可以跟普通对象的release区别开。(注意:苹果已在 OS X Mountain Lion v10.8 中弃用GC机制,而使用ARC替代)

ARC下只能使用@autoreleasepool。

@autoreleasepool {
    // Code benefitting from a local autorelease pool.
}

关于@autoreleasepool的底层原理,可以参阅《iOS - 聊聊 autorelease 和 @autoreleasepool》

__autoreleasing 使用

在MRC中我们可以给对象发送autorelease消息来将它注册到autoreleasepool中。而在ARC中autorelease已禁止调用,我们可以使用__autoreleasing修饰符修饰对象将对象注册到autoreleasepool中。

@autoreleasepool {
    id __autoreleasing obj = [[NSObject alloc] init];
}

以上代码在MRC中等价于:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
// 或者
@autoreleasepool {
    id obj = [[NSObject alloc] init];
    [obj autorelease];
}
__autoreleasing 是二级指针类型的默认修饰符

前面我们说过,对象指针的默认所有权修饰符是__strong。 而二级指针类型(ClassName **或id *)的默认所有权修饰符是__autoreleasing。如果我们没有明确指定二级指针类型的所有权修饰符,其默认就会附加__autoreleasing修饰符。

比如,我们经常会在开发中使用到NSError打印错误信息,我们通常会在方法的参数中传递NSError对象的指针。如NSString的stringWithContentsOfFile类方法,其参数NSError **使用的是__autoreleasing修饰符。

NSString *str = [NSString stringWithContentsOfFile:<#(nonnull NSString *)#>
                                          encoding:<#(NSStringEncoding)#> 
                                          error:<#(NSError *__autoreleasing  _Nullable * _Nullable)#>];

示例:我们声明一个参数为NSError **的方法,但不指定其所有权修饰符。

- (BOOL)performOperationWithError:(NSError **)error;

接着我们尝试调用该方法,发现智能提示中的参数NSError **附有__autoreleasing修饰符。可见,如果我们没有明确指定二级指针类型的所有权修饰符,其默认就会附加上__autoreleasing修饰符。

NSError *error = nil;
BOOL result = [self performOperationWithError:<#(NSError *__autoreleasing *)#>];

注意 需要注意的是,赋值给二级指针类型时,所有权修饰符必须一致,否则会编译错误。

NSError *error = nil;
NSError **error1 = &error;                 // error:Pointer to non-const type 'NSError *' with no explicit ownersh
NSError *__autoreleasing *error2 = &error; // error:Initializing 'NSError *__autoreleasing *' with an expression of type 'NSError *__strong *' changes retain/release properties of pointer
NSError *__weak *error3 = &error;          // error:Initializing 'NSError *__weak *' with an expression of type 'NSError *__strong *' changes retain/release properties of pointer
NSError *__strong *error4 = &error;        // 编译通过
NSError *__weak error = nil;
NSError *__weak *error1 = &error;          // 编译通过
NSError *__autoreleasing error = nil;
NSError *__autoreleasing *error1 = &error; // 编译通过

我们前面说过,二级指针类型的默认修饰符是__autoreleasing。那为什么我们调用方法传入__strong修饰的参数就可以编译通过呢?

NSError *__strong error = nil;
BOOL result = [self performOperationWithError:<#(NSError *__autoreleasing *)#>];

其实,编译器自动将我们的代码转化成了以下形式:

NSError *__strong error = nil;
NSError *__autoreleasing tmp = error;
BOOL result = [self performOperationWithError:&tmp];
error = tmp;

可见,当局部变量声明(__strong)和参数(__autoreleasing)之间不匹配时,会导致编译器创建临时变量。你可以将显示地指定局部变量所有权修饰符为__autoreleasing,或者显示地指定参数所有权修饰符为__strong,来避免编译器创建临时变量。

(BOOL)performOperationWithError:(NSError *__strong *)error;

但是在MRC引用计数内存管理规则中:使用alloc/new/copy/mutableCopy等方法创建的对象,创建并持有对象;其他情况创建对象但并不持有对象。由谁创建就由谁负责释放,很明显这里的 NSError为了在使用参数获得对象时,遵循此规则,我们应该指定二级指针类型参数修饰符为__autoreleasing。

在《从 MRC 说起 —— 你不持有通过引用返回的对象》章节中也说到,Cocoa 中的一些方法指定通过引用返回对象(即,它们采用ClassName **或id *类型的参数),常见的就是使用NSError对象。当你调用这些方法时,你不会创建该NSError对象,因此你不持有该对象,也无需释放它。而__strong代表持有对象,因此应该使用__autoreleasing。

另外,我们在显示指定__autoreleasing修饰符时,必须注意对象变量要为自动变量(包括局部变量、函数以及方法参数),否则编译不通过。

static NSError __autoreleasing *error = nil; // Global variables cannot have __autoreleasing ownership

属性内存管理关键字

说到属性,不得不提一下@synthesize和@dynamic这两个指令。 @synthesize 和 @dynamic

@property:帮我们自动生成属性的setter和getter方法的声明。 @synthesize:帮我们自动生成setter和getter方法的实现以及下划线成员变量。

以前我们需要手动对每个@property添加@synthesize,而在 iOS 6 之后 LLVM 编译器引入了 “property autosynthesis”,即属性自动合成。换句话说,就是编译器会自动为每个@property添加@synthesize。

Q: @synthesize现在有什么作用呢? 如果我们同时重写了setter和getter方法,则编译器就不会为这个@property添加@synthesize,这时候就不存在下划线成员变量,所以我们需要手动添加@synthesize。

@synthesize propertyName = _propertyName;

有时候我们不希望编译器为我们@synthesize,我们希望在程序运行过程中再去决定该属性存取方法的实现,就可以使用@dynamic。

@dynamic:告诉编译器不用自动进行@synthesize,等到运行时再添加方法实现,但是它不会影响@property生成的setter和getter方法的声明。@dynamic是OC 为动态运行时语言的体现。动态运行时语言与编译时语言的区别:动态运行时语言将函数决议推迟到运行时,编译时语言在编译器进行函数决议。

@dynamic propertyName;

属性“内存管理”关键字与所有权修饰符的对应关系: 在这里插入图片描述 更多关于属性关键字的内容,可以参阅《OC - 属性关键字和所有权修饰符》

循环引用问题的避免

delegate 避免循环引用

delegate避免循环引用,就是在委托方声明delegate属性时,使用weak关键字。

@property (nonatomic, weak) id<protocolName> delegate;

block 避免循环引用

Q: 为什么 block 会产生循环引用? ① 相互循环引用: 如果当前block对当前对象的某一成员变量进行捕获的话,可能会对它产生强引用。根据block的变量捕获机制,如果block被拷贝到堆上,且捕获的是对象类型的auto变量,则会连同其所有权修饰符一起捕获,所以如果对象是__strong修饰,则block会对它产生强引用(如果block在栈上就不会强引用)。而当前block可能又由于当前对象对其有一个强引用,就产生了相互循环引用的问题; ② 大环引用: 我们如果使用__block的话,在ARC下可能会产生循环引用(MRC则不会)。由于__block修饰符会将变量包装成一个对象,如果block被拷贝到堆上,则会直接对__block变量产生强引用,而__block如果修饰的是对象的话,会根据对象的所有权修饰符做出相应的操作,形成强引用或者弱引用。如果对象是__strong修饰(如__block id x),则__block变量对它产生强引用(在MRC下则不会),如果这时候该对象是对block持有强引用的话,就产生了大环引用的问题。在ARC下可以通过断环的方式去解除循环引用,可以在block中将指针置为nil(MRC不会循环引用,则不用解决)。但是有一个弊端,如果该block一直得不到调用,循环引用就一直存在。

ARC下的解决方式:

  • 方式一:用__weak或者__unsafe_unretained解决:
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        NSLog(@"%p",weakSelf);
    };
    __unsafe_unretained id uuSelf = self;
    self.block = ^{
        NSLog(@"%p",uuSelf);
    };

对于 non-trivial cycles,我们需要这样做:

    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if(!strongSelf) return;
        NSLog(@"%p",weakSelf);
    };

在这里插入图片描述

  • 方式二:用__block解决(必须要调用block):

缺点:必须要调用block,而且block里要将指针置为nil。如果一直不调用block,对象就会一直保存在内存中,造成内存泄漏。

    __block id blockSelf = self;
    self.block = ^{
        NSLog(@"%p",blockSelf);
        blockSelf = nil;
    };
    self.block();

在这里插入图片描述 MRC 下的解决方式:

用__unsafe_unretained解决:同ARC。 用__block解决(在MRC下使用__block修饰对象类型,在block内部不会对该对象进行retain操作,所以在MRC环境下可以通过__block解决循环引用的问题)

	__block id blockSelf = self;
	self.block = ^{
	    NSLog(@"%p",blockSelf);
	};

更多关于block的内容,可以参阅《OC - Block 详解》

ARC下栈变量初始化为 nil

使用ARC,strong、weak和autoreleasing的栈变量现在会默认初始化为nil。例如:

- (void)myMethod {
    NSString *name;
    NSLog(@"name: %@", name);
}

打印name的值为null,而不是程序Crash。

使用编译器标志启用和禁用 ARC

使用-fobjc-arc编译器标志启用ARC。如果对你来说,某些文件使用MRC更方便,那你可以仅对部分文件使用ARC。对于使用ARC作为默认方式的项目,可以使用-fno-objc-arc编译器标志为指定文件禁用ARC。如下图所示: 在这里插入图片描述

(5)ARC管理的对象和MRC管理的对象之间如何进行转换?

桥接(Toll-Free Bridging)

在项目中可能会使用到Core Foundation样式的对象,它可能来自Core Foundation框架或者采用Core Foundation约定标准的其它框架如Core Graphics。

编译器不会自动管理Core Foundation对象的生命周期,你必须根据Core Foundation内存管理规则调用CFRetain和CFRelease。请参阅《Memory Management Programming Guide for Core Foundation》

在MRC下,我们可以直接在Objective-C指针类型id和C指针类型void *之间进行强制转换,如Foundation对象和Core Foundation对象进行转换。由于都是手动管理内存,无须关心内存管理权的移交问题。

在ARC下,进行Foundation对象和Core Foundation对象的类型转换,需要使用Toll-Free Bridging(桥接)告诉编译器对象的所有权语义。你要选择使用__bridge、__bridge_retained、__bridge_transfer这三种桥接方案中的一种来确定对象的内存管理权移交问题,它们的作用分别如下: 在这里插入图片描述

__bridge(常用):不改变对象的内存管理权所有者。

本来由ARC管理的Foundation对象,转换成Core Foundation对象后继续由ARC管理; 本来由开发者手动管理的Core Foundation对象,转换成Foundation对象后继续由开发者手动管理。 下面以NSMutableArray对象和CFMutableArrayRef对象为例:

// 本来由 ARC 管理
NSMutableArray *mArray = [[NSMutableArray alloc] init];
// 转换后继续由 ARC 管理            
CFMutableArrayRef cfMArray = (__bridge CFMutableArrayRef)(mArray); 

// 本来由开发者手动管理
CFMutableArrayRef cfMArray = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL); 
// 转换后继续由开发者手动管理
NSMutableArray *mArray = (__bridge NSMutableArray *)(cfMArray); 
...
// 在不需要该对象时需要手动释放
CFRelease(cfMArray); 

__bridge的安全性与赋值给__unsafe_unretained修饰符相近甚至更低,如果使用不当没有注意对象的释放,就会因悬垂指针而导致Crash。

__bridge转换后不改变对象的引用计数,比如我们将id类型转换为void *类型,我们在使用void *之前该对象被销毁了,那么我们再使用void *访问该对象肯定会Crash。所以void *指针建议立即使用,如果我们要保存这个void *指针留着以后使用,那么建议使用__bridge_retain。

而在使用__bridge将void *类型转换为id类型时,一定要注意此时对象的内存管理还是由开发者手动管理,记得在不需要对象时进行释放,否则内存泄漏!

以下给出几个 “使用__bridge将void *类型转换为id类型” 的示例代码,要注意转换后还是由开发者手动管理内存,所以即使离开作用域,该对象还保存在内存中。

// 使用 __strong
CFMutableArrayRef cfMArray = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
NSMutableArray *mArray = (__bridge NSMutableArray *)(cfMArray);

NSLog(@"%ld", CFGetRetainCount(cfMArray));     // 2, 因为 mArray 是 __strong, 所以增加引用计数
NSLog(@"%ld", _objc_rootRetainCount(mArray));  // 1, 但是使用 _objc_rootRetainCount 打印出来是 1 ?
 
// 在不需要该对象时进行释放
CFRelease(cfMArray); 
NSLog(@"%ld", CFGetRetainCount(cfMArray));     // 1, 因为 __strong 作用域还没结束,还有强指针引用着
NSLog(@"%ld", _objc_rootRetainCount(mArray));  // 1

// 在 __strong 作用域结束前,还可以访问该对象
// 等 __strong 作用域结束,该对象就会销毁,再访问就会崩溃
// 使用 __strong
CFMutableArrayRef cfMArray;
{
    cfMArray = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
    NSMutableArray *mArray = (__bridge NSMutableArray *)(cfMArray);
    NSLog(@"%ld", CFGetRetainCount(cfMArray));  // 2, 因为 mArray 是 __strong, 所以增加引用计数
}
NSLog(@"%ld", CFGetRetainCount(cfMArray));      // 1, __strong 作用域结束
CFRelease(cfMArray); // 释放对象,否则内存泄漏
// 可以使用 CFShow 函数打印 CF 对象
CFShow(cfMArray);    // 再次访问就会崩溃
// 使用 __weak
CFMutableArrayRef cfMArray = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
NSMutableArray __weak *mArray = (__bridge NSMutableArray *)(cfMArray);

NSLog(@"%ld", CFGetRetainCount(cfMArray));     // 1, 因为 mArray 是 __weak, 所以不增加引用计数
NSLog(@"%ld", _objc_rootRetainCount(mArray));  // 1

/*
* 使用 mArray
*/

// 在不需要该对象时进行释放
CFRelease(cfMArray);
NSLog(@"%@",mArray);  // nil, 这就是使用 __weak 的好处,在指向的对象被销毁的时候会自动置指针为 nil,再次访问也不会崩溃
// 使用 __weak
NSMutableArray __weak *mArray;
{
    CFMutableArrayRef cfMArray = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
    mArray = (__bridge NSMutableArray *)(cfMArray);
    NSLog(@"%ld", CFGetRetainCount(cfMArray));  // 1, 因为 mArray 是 __weak, 所以不增加引用计数
}
CFMutableArrayRef cfMArray =  (__bridge CFMutableArrayRef)(mArray);
NSLog(@"%ld", CFGetRetainCount(cfMArray)); // 1, 可见即使出了作用域,对象也还没释放,因为内存管理权在我们

CFRelease(cfMArray); // 释放对象,否则内存泄漏

__bridge_retained:用在Foundation对象转换成Core Foundation对象时,进行ARC内存管理权的剥夺。

本来由ARC管理的Foundation对象,转换成Core Foundation对象后,ARC不再继续管理该对象,需要由开发者自己手动释放该对象,否则会发生内存泄漏。

// 本来由 ARC 管理
NSMutableArray *mArray = [[NSMutableArray alloc] init];       
// 转换后由开发者手动管理              
CFMutableArrayRef cfMArray = (__bridge_retained CFMutableArrayRef)(mArray); 
//    CFMutableArrayRef cfMArray = (CFMutableArrayRef)CFBridgingRetain(mArray); // 另一种等效写法
...
CFRelease(cfMArray);  // 在不需要该对象的时候记得手动释放

__bridge_retained顾名思义会对对象retain,使转换赋值的变量也持有该对象,对象的引用计数 +1。由于转换后由开发者进行手动管理,所以再不需要该对象的时候记得调用CFRelease释放对象,否则内存泄漏。

id obj = [[NSObject alloc] init];
void *p = (__bridge_retained void *)(obj);

以上代码如果在MRC下相当于:

id obj = [[NSObject alloc] init];

void *p = obj;
[(id)p retain];

__bridge_transfer:用在Core Foundation对象转换成Foundation对象时,进行内存管理权的移交。

本来由开发者手动管理的Core Foundation对象,转换成Foundation对象后,将内存管理权移交给ARC,开发者不用再关心对象的释放问题,不用担心内存泄漏。

    // 本来由开发者手动管理
    CFMutableArrayRef cfMArray = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
    // 转换后由 ARC 管理
    NSMutableArray *mArray = (__bridge_transfer NSMutableArray *)(cfMArray);
//    NSMutableArray *mArray = CFBridgingRelease(cfMArray); // 另一种等效写法

__bridge_transfer作用如其名,移交内存管理权。它的实现跟__bridge_retained相反,会release被转换的变量持有的对象,但同时它在赋值给转换的变量时会对对象进行retain,所以引用计数不变。也就是说,对于Core Foundation引用计数语义而言,对象是释放的,但是ARC保留了对它的引用。

id obj = (__bridge_transfer void *)(p);

以上代码如果在MRC下相当于:

id obj = (id)p;
[obj retain];
[(id)p release];

下面也给出一个示例代码:

CFMutableArrayRef cfMArray;
{
    cfMArray = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
    NSMutableArray *mArray = (__bridge_transfer NSMutableArray *)(cfMArray);
    NSLog(@"%ld", CFGetRetainCount(cfMArray));    // 1, 因为 cfMArray 指针指向的对象存在,所以仍然可以通过该指针访问
    NSLog(@"%ld", _objc_rootRetainCount(mArray)); // 1, mArray 为 __strong
}
// __strong 作用域结束,ARC 对该对象进行了释放
NSLog(@"%ld", CFGetRetainCount(cfMArray)); // 再次访问就会崩溃

如果将上面的代码由__bridge_transfer改为使用__bridge会怎样? 其实在__bridge讲解中已经给出了示例代码,如果不释放就会造成内存泄漏。 以上提到了可以替代__bridge_retained和__bridge_transfer的两个函数:CFBridgingRetain和CFBridgingRelease,下面我们来看一下函数实现:

/* Foundation - NSObject.h */
#if __has_feature(objc_arc)  // ARC

// After using a CFBridgingRetain on an NSObject, the caller must take responsibility for calling CFRelease at an appropriate time.
NS_INLINE CF_RETURNS_RETAINED CFTypeRef _Nullable CFBridgingRetain(id _Nullable X) {
    return (__bridge_retained CFTypeRef)X;
}

NS_INLINE id _Nullable CFBridgingRelease(CFTypeRef CF_CONSUMED _Nullable X) {
    return (__bridge_transfer id)X;
}

#else // MRC

// This function is intended for use while converting to ARC mode only.
NS_INLINE CF_RETURNS_RETAINED CFTypeRef _Nullable CFBridgingRetain(id _Nullable X) {
    return X ? CFRetain((CFTypeRef)X) : NULL;
}

// Casts a CoreFoundation object to an Objective-C object, transferring ownership to ARC (ie. no need to CFRelease to balance a prior +1 CFRetain count). NS_RETURNS_RETAINED is used to indicate that the Objective-C object returned has +1 retain count.  So the object is 'released' as far as CoreFoundation reference counting semantics are concerned, but retained (and in need of releasing) in the view of ARC. This function is intended for use while converting to ARC mode only.
NS_INLINE id _Nullable CFBridgingRelease(CFTypeRef CF_CONSUMED _Nullable X) NS_RETURNS_RETAINED {
    return [(id)CFMakeCollectable(X) autorelease];
}

#endif

可以看到在ARC下,这两个函数就是使用了__bridge_retained和__bridge_transfer。

小结: 在ARC下,必须恰当使用Toll-Free Bridging(桥接)在Foundation对象和Core Foundation对象之间进行类型转换,否则可能会导致内存泄漏。

建议:

  • 将Foundation对象转为Core Foundation对象时,如果我们立即使用该Core Foundation对象,使用__bridge;如果我们想保存着以后使用,使用__bridge_retained,但是要记得在使用完调用CFRelease释放对象。
  • 将Core Foundation对象转为Foundation对象时,使用__bridge_transfer。

(6)什么是autoreleasepool,它的原理是什么?他和runloop的关系是什么?

iOS开发中的Autorelease机制是为了延时释放对象。自动释放的概念看上去很像ARC,但实际上这更类似于C语言中自动变量的特性。

自动变量:在超出变量作用域后将被废弃; 自动释放池:在超出释放池生命周期后,向其管理的对象实例的发送release消息。

MRC下使用自动释放池

在MRC环境中使用自动释放池需要用到NSAutoreleasePool对象,其生命周期就相当于C语言变量的作用域。对于所有调用过autorelease方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。用源代码表示如下:

//MRC环境下的测试:
//第一步:生成并持有释放池NSAutoreleasePool对象;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

//第二步:调用对象的autorelease实例方法;
id obj = [[NSObject alloc] init];
[obj autorelease];

//第三步:废弃NSAutoreleasePool对象;
[pool drain];   //向pool管理的所有对象发送消息,相当于[obj release]

//obi已经释放,再次调用会崩溃(Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT))
NSLog(@"打印obj:%@", obj); 

理解NSAutoreleasePool对象的生命周期,如下图所示: 在这里插入图片描述 ARC下使用自动释放池

ARC环境不能使用NSAutoreleasePool类也不能调用autorelease方法,代替它们实现对象自动释放的是@autoreleasepool块和__autoreleasing修饰符。比较两种环境下的代码差异如下图:

在这里插入图片描述

如图所示,@autoreleasepool块替换了NSAutoreleasePoool类对象的生成、持有及废弃这一过程。而附有__autoreleasing修饰符的变量替代了autorelease方法,将对象注册到了Autoreleasepool;由于ARC的优化,__autorelease是可以被省略的,所以简化后的ARC代码如下:

//ARC环境下的测试:
@autoreleasepool {
    id obj = [[NSObject alloc] init];
    NSLog(@"打印obj:%@", obj); 
}

显式使用__autoreleasing修饰符的情况非常少见,这是因为ARC的很多情况下,即使是不显式的使用__autoreleasing,也能实现对象被注册到释放池中。主要包括以下几种情况:

  1. 编译器会进行优化,检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回对象注册到Autoreleasepool;
  2. 访问附有__weak修饰符的变量时,实际上必定要访问注册到Autoreleasepool的对象,即会自动加入Autoreleasepool;
  3. id的指针或对象的指针(id*,NSError **),在没有显式地指定修饰符时候,会被默认附加上__autoreleasing修饰符,加入Autoreleasepool

注意:如果编译器版本为LLVM.3.0以上,即使ARC无效@autoreleasepool块也能够使用;如下源码所示:

//MRC环境下的测试:
@autoreleasepool{
    id obj = [[NSObject alloc] init];
    [obj autorelease];
}

AutoreleasePool的底层原理

1、使用@autoreleasepool{}

我们在main函数中写入自动释放池相关的测试代码如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello, World!");
    }
    return 0;
}

为了探究释放池的底层实现,我们在终端使用clang -rewrite-objc + 文件名命令将上述OC代码转化为C++源码:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */
    {
        __AtAutoreleasePool __autoreleasepool;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_d37e0d_mi_0);
     }//大括号对应释放池的作用域
     
     return 0;
}

在经过编译器clang命令转化后,我们看到的所谓的@autoreleasePool块,其实对应着__AtAutoreleasePool的结构体。

2、分析结构体__AtAutoreleasePool的具体实现

在源码中找到__AtAutoreleasePool结构体的实现代码,具体如下:

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

__AtAutoreleasePool结构体包含了:构造函数、析构函数和一个边界对象; 构造函数内部调用:objc_autoreleasePoolPush()方法,返回边界对象atautoreleasepoolobj 析构函数内部调用:objc_autoreleasePoolPop()方法,传入边界对象atautoreleasepoolobj

分析main函数中__autoreleasepool结构体实例的生命周期是这样的: __autoreleasepool是一个自动变量,其构造函数是在程序执行到声明这个对象的位置时调用的,而其析构函数则是在程序执行到离开这个对象的作用域时调用。所以,我们可以将上面main函数的代码简化如下:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kb_06b822gn59df4d1zt99361xw0000gn_T_main_d39a79_mi_0);
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

3、objc_autoreleasePoolPush与objc_autoreleasePoolPop

进一步观察自动释放池构造函数与析构函数的实现,其实它们都只是对AutoreleasePoolPage对应静态方法push和pop的封装

void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}

4、理解AutoreleasePoolPage

AutoreleasePoolPage是一个C++中的类,打开Runtime的源码工程,在NSObject.mm文件中可以找到它的定义,摘取其中的关键代码如下:

//大致在641行代码开始
class AutoreleasePoolPage {
\#   define EMPTY_POOL_PLACEHOLDER ((id*)1)  //空池占位
\#   define POOL_BOUNDARY nil                //边界对象(即哨兵对象)
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);
    magic_t const magic;                  //校验AutoreleasePagePoolPage结构是否完整
    id *next;                             //指向新加入的autorelease对象的下一个位置,初始化时指向begin()
    pthread_t const thread;               //当前所在线程,AutoreleasePool是和线程一一对应的
    AutoreleasePoolPage * const parent;   //指向父节点page,第一个结点的parent值为nil
    AutoreleasePoolPage *child;           //指向子节点page,最后一个结点的child值为nil
    uint32_t const depth;                 //链表深度,节点个数
    uint32_t hiwat;                       //数据容纳的一个上限
    //......
};

其实,每个自动释放池都是是由若干个AutoreleasePoolPage组成的双向链表结构,如下图所示:

在这里插入图片描述

AutoreleasePoolPage中拥有parent和child指针,分别指向上一个和下一个page;当前一个page的空间被占满(每个AutorelePoolPage的大小为4096字节)时,就会新建一个AutorelePoolPage对象并连接到链表中,后来的 Autorelease对象也会添加到新的page中;

另外,当next== begin()时,表示AutoreleasePoolPage为空;当next == end(),表示AutoreleasePoolPage已满。

5、理解哨兵对象/边界对象(POOL_BOUNDARY)的作用

在AutoreleasePoolPage的源码中,我们很容易找到边界对象(哨兵对象)的定义:

#define POOL_BOUNDARY nil

边界对象其实就是nil的别名,而它的作用事实上也就是为了起到一个标识的作用。

每当自动释放池初始化调用objc_autoreleasePoolPush方法时,总会通过AutoreleasePoolPage的push方法,将POOL_BOUNDARY放到当前page的栈顶,并且返回这个边界对象;

而在自动释放池释放调用objc_autoreleasePoolPop方法时,又会将边界对象以参数传入,这样自动释放池就会向释放池中对象发送release消息,直至找到第一个边界对象为止。

6、理解objc_autoreleasePoolPush方法

经过前面的分析,objc_autoreleasePoolPush最终调用的是 AutoreleasePoolPage的push方法,该方法的具体实现如下:

static inline void *push() {
   return autoreleaseFast(POOL_BOUNDARY);
}

static inline id *autoreleaseFast(id obj)
{
   AutoreleasePoolPage *page = hotPage();
   if (page && !page->full()) {
       return page->add(obj);
   } else if (page) {
       return autoreleaseFullPage(obj, page);
   } else {
      return autoreleaseNoPage(obj);
   }
}

//压栈操作:将对象加入AutoreleaseNoPage并移动栈顶的指针
id *add(id obj) {
    id *ret = next;
    *next = obj;
    next++;
    return ret;
}

//当前hotPage已满时调用
static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}

//当前hotpage不存在时调用
static id *autoreleaseNoPage(id obj) {
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);

    if (obj != POOL_SENTINEL) {
        page->add(POOL_SENTINEL);
    }

    return page->add(obj);
}

观察上述代码,每次调用push其实就是创建一个新的AutoreleasePool,在对应的AutoreleasePoolPage中插入一个POOL_BOUNDARY ,并且返回插入的POOL_BOUNDARY 的内存地址。push方法内部调用的是autoreleaseFast方法,并传入边界对象(POOL_BOUNDARY)。hotPage可以理解为当前正在使用的AutoreleasePoolPage。

自动释放池最终都会通过page->add(obj)方法将边界对象添加到释放池中,而这一过程在autoreleaseFast方法中被分为三种情况:

  1. 当前page存在且不满,调用page->add(obj)方法将对象添加至page的栈中,即next指向的位置
  2. 当前page存在但是已满,调用autoreleaseFullPage初始化一个新的page,调用page->add(obj)方法将对象添加至page的栈中
  3. 当前page不存在时,调用autoreleaseNoPage创建一个hotPage,再调用page->add(obj) 方法将对象添加至page的栈中

7、理解 objc_autoreleasePoolPop方法

AutoreleasePool的释放调用的是objc_autoreleasePoolPop方法,此时需要传入边界对象作为参数。这个边界对象正是每次执行objc_autoreleasePoolPush方法返回的对象atautoreleasepoolobj;

同理,我们找到objc_autoreleasePoolPop最终调用的方法,即AutoreleasePoolPage的pop方法,该方法的具体实现如下:

static inline void pop(void *token)   //POOL_BOUNDARY的地址
{
    AutoreleasePoolPage *page;
    id *stop;

    page = pageForPointer(token);   //通过POOL_BOUNDARY找到对应的page
    stop = (id *)token;
    if (DebugPoolAllocation  &&  *stop != POOL_SENTINEL) {
        // This check is not valid with DebugPoolAllocation off
        // after an autorelease with a pool page but no pool in place.
        _objc_fatal("invalid or prematurely-freed autorelease pool %p; ", 
                    token);
    }

    if (PrintPoolHiwat) printHiwat();   // 记录最高水位标记

    page->releaseUntil(stop);   //向栈中的对象发送release消息,直到遇到第一个哨兵对象

    // memory: delete empty children
    // 删除空掉的节点
    if (DebugPoolAllocation  &&  page->empty()) {
        // special case: delete everything during page-per-pool debugging
        AutoreleasePoolPage *parent = page->parent;
        page->kill();
        setHotPage(parent);
    } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
        // special case: delete everything for pop(top) 
        // when debugging missing autorelease pools
        page->kill();
        setHotPage(nil);
    } 
    else if (page->child) {
        // hysteresis: keep one empty child if page is more than half full
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

上述代码中,首先根据传入的边界对象地址找到边界对象所处的page;然后选择当前page中最新加入的对象一直向前清理,可以向前跨越若干个page,直到边界所在的位置;清理的方式是向这些对象发送一次release消息,使其引用计数减一;

另外,清空page对象还会遵循一些原则:

  1. 如果当前的page中存放的对象少于一半,则子page全部删除;
  2. 如果当前当前的page存放的多余一半(意味着马上将要满),则保留一个子page,节省创建新page的开销;

8、autorelease方法

上述是对自动释放池整个生命周期的分析,现在我们来理解延时释放对象autorelease方法的实现,首先查看该方法的调用栈:

- [NSObject autorelease]
└── id objc_object::rootAutorelease()
    └── id objc_object::rootAutorelease2()
        └── static id AutoreleasePoolPage::autorelease(id obj)
            └── static id AutoreleasePoolPage::autoreleaseFast(id obj)
                ├── id *add(id obj)
                ├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
                │   ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                │   └── id *add(id obj)
                └── static id *autoreleaseNoPage(id obj)
                    ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                    └── id *add(id obj)

如上所示,autorelease方法最终也会调用上面提到的 autoreleaseFast方法,将当前对象加到AutoreleasePoolPage中。关于autoreleaseFast的分析这里不再累述,我们主要来考虑一下两次调用的区别:

autorelease函数和push函数一样,关键代码都是调用autoreleaseFast函数向自动释放池的链表栈中添加一个对象,不过push函数入栈的是一个边界对象,而autorelease函数入栈的是一个具体的Autorelease的对象。

AutoreleasePool与NSThread、NSRunLoop的关系

由于AppKit和UIKit框架的优化,我们很少需要显式的创建一个自动释放池块。这其中就涉及到AutoreleasePool与NSThread、NSRunLoop的关系。

RunLoop和NSThread的关系

RunLoop是用于控制线程生命周期并接收事件进行处理的机制,其实质是一个do-While循环。在苹果文档找到关于NSRunLoop的介绍如下:

Your application neither creates or explicitly manages NSRunLoop objects. Each NSThread object—including the application’s main thread—has an NSRunLoop object automatically created for it as needed. If you need to access the current thread’s run loop, you do so with the class method currentRunLoop.

总结RunLoop与NSThread(线程)之间的关系如下:

  1. RunLoop与线程是一一对应关系,每个线程(包括主线程)都有一个对应的RunLoop对象;其对应关系保存在一个全局的Dictionary里;
  2. 主线程的RunLoop默认由系统自动创建并启动;而其他线程在创建时并没有RunLoop,若该线程一直不主动获取,就一直不会有RunLoop;
  3. 苹果不提供直接创建RunLoop的方法;所谓其他线程Runloop的创建其实是发生在第一次获取的时候,系统判断当前线程没有RunLoop就会自动创建;
  4. 当前线程结束时,其对应的Runloop也被销毁;

RunLoop和AutoreleasePool的关系

在苹果文档中找到两者关系的介绍如下:

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event.

如上所述,主线程的NSRunLoop在监测到事件响应开启每一次event loop之前,会自动创建一个autorelease pool,并且会在event loop结束的时候执行drain操作,释放其中的对象。

Thread和AutoreleasePool的关系

在苹果文档中找到两者关系的介绍如下:

Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects (see Threads). As new pools are created, they get added to the top of the stack. When pools are deallocated, they are removed from the stack. Autoreleased objects are placed into the top autorelease pool for the current thread. When a thread terminates, it automatically drains all of the autorelease pools associated with itself.

如上所述, 包括主线程在内的所有线程都维护有它自己的自动释放池的堆栈结构。新的自动释放池被创建的时候,它们会被添加到栈的顶部,而当池子销毁的时候,会从栈移除。对于当前线程来说,Autoreleased对象会被放到栈顶的自动释放池中。当一个线程线程停止,它会自动释放掉与其关联的所有自动释放池。

AutoreleasePool在主线程上的释放时机

理解主线程上的自动释放过程

分析主线程RunLoop管理自动释放池并释放对象的详细过程,我们在如下Demo中的主线程中设置断点,并执行lldb命令:po [NSRunLoop currentRunLoop],具体效果如下: autoreleasepool在主线程的释放时机.png

我们看到主线程RunLoop中有两个与自动释放池相关的Observer,它们的 activities分别为0x1和0xa0这两个十六进制的数,转为二进制分别为1和10100000,对应CFRunLoopActivity的类型如下:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),          //0x1,启动Runloop循环
    kCFRunLoopBeforeTimers = (1UL << 1),            
    kCFRunLoopBeforeSources = (1UL << 2),        
    kCFRunLoopBeforeWaiting = (1UL << 5),  //0xa0,即将进入休眠     
    kCFRunLoopAfterWaiting = (1UL << 6),   
    kCFRunLoopExit = (1UL << 7),           //0xa0,退出RunLoop循环  
    kCFRunLoopAllActivities = 0x0FFFFFFFU
    };

结合RunLoop监听的事件类型,分析主线程上自动释放池的使用过程如下:

  1. App启动后,苹果在主线程RunLoop里注册了两个Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler();
  2. 第一个Observer监视的事件是Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush()创建自动释放池。order = -2147483647(即32位整数最小值)表示其优先级最高,可以保证创建释放池发生在其他所有回调之前;
  3. 第二个Observer监视了两个事件BeforeWaiting(准备进入休眠)时调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop()来释放自动释放池。order = 2147483647(即32位整数的最大值)表示其优先级最低,保证其释放池子发生在其他所有回调之后;
  4. 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop创建好的AutoreleasePool环绕着,所以不会出现内存泄漏,开发者也不必显示创建AutoreleasePool了;

最后,也可以结合图示理解主线程上自动释放对象的具体流程: 在这里插入图片描述

程序启动到加载完成后,主线程对应的RunLoop会停下来等待用户交互 用户的每一次交互都会启动一次运行循环,来处理用户所有的点击事件、触摸事件。 RunLoop检测到事件后,就会创建自动释放池; 所有的延迟释放对象都会被添加到这个池子中; 在一次完整的运行循环结束之前,会向池中所有对象发送release消息,然后自动释放池被销毁;

测试主线程上的对象自动释放过程

下面的代码创建了一个Autorelease对象string,并且通过weakString进行弱引用(不增加引用计数,所以不会影响对象的生命周期),具体如下:

@interface TestMemoryVC ()
@property (nonatomic,weak)NSString *weakString;
@end

@implementation TestMemoryVC
- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *string = [NSString stringWithFormat:@"%@",@"WUYUBEICHEN"];
    self.weakString = string;
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"viewWillAppear:%@", self.weakString);
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"viewDidAppear:%@", self.weakString);
}

@end

//打印结果:
//viewWillAppear:WUYUBEICHEN
//viewDidAppear:(null)

代码分析:自动变量的string在离开viewDidLoad的作用域后,会依靠当前主线程上的RunLoop迭代自动释放。最终string对象在viewDidAppear方法执行前被释放(RunLoop完成此次迭代)。

AutoreleasePool子线程上的释放时机

子线程默认不开启RunLoop,那么其中的延时对象该如何释放呢?其实这依然要从Thread和AutoreleasePool的关系来考虑:

Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects.

也就是说,每一个线程都会维护自己的 Autoreleasepool栈,所以子线程虽然默认没有开启RunLoop,但是依然存在AutoreleasePool,在子线程退出的时候会去释放autorelease对象。

前面讲到过,ARC会根据一些情况进行优化,添加__autoreleasing修饰符,其实这就相当于对需要延时释放的对象调用了autorelease方法。从源码分析的角度来看,如果子线程中没有创建AutoreleasePool ,而一旦产生了Autorelease对象,就会调用autoreleaseNoPage方法自动创建hotpage,并将对象加入到其栈中。所以,一般情况下,子线程中即使我们不手动添加自动释放池,也不会产生内存泄漏。

AutoreleasePool需要手动添加的情况

尽管ARC已经做了诸多优化,但是有些情况我们必须手动创建AutoreleasePool,而其中的延时对象将在当前释放池的作用域结束时释放。苹果文档中说明了三种情况,我们可能会需要手动添加自动释放池:

  1. 编写的不是基于UI框架的程序,例如命令行工具;
  2. 通过循环方式创建大量临时对象;
  3. 使用非Cocoa程序创建的子线程;

而在ARC环境下的实际开发中,我们最常遇到的也是第二种情况,以下面的代码为例:

- (void)viewDidLoad {
    [super viewDidLoad];
    for (int i = 0; i < 1000000; i++) {
        NSString *obj = [NSString stringWithFormat:@"%@",@"obj"];
        NSLog(@"打印obj:%@", obj);
    }
 }

上述代码中,obj因为离开作用域所以会被加入最近一次创建的自动释放池中,而这个释放池就是主线程上的RunLoop管理的;因为for循环在当前线程没有执行完毕,Runloop也就没有完成当前这一次的迭代,所以导致大量对象被延时释放。释放池中的对象将会在viewDidAppear方法执行前就被销毁。在此情况下,我们就有必要通过手动干预的方式及时释放不需要的对象,减少内存消耗;优化的代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    for (int i = 0; i < 1000000; i++) {
        @autoreleasepool{
             NSString *obj = [NSString stringWithFormat:@"%@",@"obj"];
             NSLog(@"打印obj:%@", obj);
        }
    }
 }

(7)什么是TaggedPointer,哪些对象使用了TaggedPointer技术?

具体参考:iOS - 老生常谈内存管理(五):Tagged Pointe

(8)示例分析

为了更好地熟悉以上内容,写了个小demo,通过分析输出结果来帮助更好的理解:

//
//  demo8ViewController.m
//  iOSPlayground
//
//  Created by bytedance on 2021/9/9.
//

#import "demo8ViewController.h"

@interface demo8ViewController ()

@property (nonatomic, strong) NSObject *strongProperty;

@property (nonatomic, weak) NSObject *weakProperty;


@property (nonatomic, weak) NSString *testString;

@end

@implementation demo8ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self testObjAsReturnValue];
}
- (void)viewWillAppear:(BOOL)animated {
}

- (void)viewDidAppear:(BOOL)animated {
}

- (void)setupString {
    NSString *str = [[NSString alloc] init];
    NSLog(@"将对象赋值给__weak 类型的指针前, 对象的retain Count=%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(str)));
    self.testString = str;
    NSLog(@"将对象赋值给__weak 类型的指针后, 对象的retain Count=%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(str)));
}

/**
 NSString 的引用计数特殊
 */
- (void)testNSStringReatinCount {
    NSString *strNoMoreThan10 = @"123456789";
    NSLog(@"strNoMoreThan10, retain Count=%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(strNoMoreThan10)));
    NSString *strMoreThan10 = @"1234567890";
    NSLog(@"strMoreThan10, retain Count=%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(strMoreThan10)));
    NSString *str1_No = [[NSString alloc] initWithString:strNoMoreThan10];
    NSLog(@"str1_No, retain Count=%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(str1_No)));
    NSString *str1 = [[NSString alloc] initWithString:strMoreThan10];
    NSLog(@"str1, retain Count=%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(str1)));
    NSString *str2_No = [NSString stringWithFormat:@"%@", strNoMoreThan10];
    NSLog(@"str2_No, retain Count=%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(str2_No)));
    NSString *str2 = [NSString stringWithFormat:@"%@", strMoreThan10];
    NSLog(@"str2, retain Count=%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(str2)));
}

- (void)testObjAsReturnValue {
    self.testString = [self createString];
//    NSString *__weak strValue = [self createString];
//    NSObject *__weak objValue = [self createObj];
    NSLog(@"对象作为返回值返回后,其retain Count=%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(self.testString)));
}

- (void)testObjAsParamter{
    NSObject *__strong copyObj = nil;
    @autoreleasepool {
        NSObject *tempObj = [[NSObject alloc] init];
        NSLog(@"对象使用init创建之后,对象的retain Count=%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(tempObj)));
        copyObj = tempObj;
        NSLog(@"使用__strong类型指针强引用对象时,对象的retain Count=%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(tempObj)));
        [self getRetainCountofObj:tempObj];
        NSLog(@"对象作为参数结束后,对象的retain Count=%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(tempObj)));
    }
    NSLog(@"对象离开autoreleasepool的作用域之后,对象的retain Count=%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(copyObj)));
}

- (long)getRetainCountofObj:(NSObject *__strong)objc {
    long retainCount =  (long)CFGetRetainCount((__bridge CFTypeRef)(objc));
    NSLog(@"将对象作为参数传入后,对象的retain Count=%ld", retainCount);
    return retainCount;
}

- (NSString *)createString{
    NSInteger i = 1;
    NSString *Str = [NSString stringWithFormat:@"测试字符串-%ld", i];
    NSLog(@"使用非init方法创建的对象,其reatin Count=%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(Str)));
//    NSString *Str = [[NSString alloc] init];
//    NSLog(@"使用init方法创建的对象,其reatin Count=%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(Str)));
    return Str;
}

- (NSObject *)createObj {
    NSObject *obj = [[NSObject alloc] init];
    NSLog(@"使用init方法创建的对象,其reatin Count=%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(obj)));
    return obj;
}

@end

代码分析: 先看testObjAsParamter方法的的输出结果: 在这里插入图片描述 从上述输出中可以看出,一开始使用alloc创建并持有了NSObject对象,所以tempObj的引用计数为1,将该对象赋值给__strong修饰的对象copyObj之后,对象被retain,所以其引用计数为2,再作为参数传给getRetainCountofObj方法后,再一次被reatin,所以引用计数为3,当离开getRetainCountofObj方法后,对象被发送release消息,所以其引用计数变为2,最后离开AutoreleasePool作用域后,因为对象处于自动释放池中,而自动释放池被销毁,会给池中所有的对象发送release消息,所以引用计数减1变成1。

参考文献

iOS - 老生常谈内存管理:导读 iOS内存管理-深入解析自动释放池