【OC内存管理】iOS的内存管理

目录
一、iOS的内存管理方式
  1、小对象的内存管理 -- Tagged Pointer
   1.1 Tagged Pointer是什么
   1.2 Tagged Pointer是怎么存储数据的
   1.3 Tagged Pointer是怎么调用方法的
  2、普通对象的内存管理 -- 引用计数
   2.1 引用计数是什么
   2.2 引用计数存储在哪里
   2.3 iOS具体是怎么通过引用计数来进行对象的内存管理的
二、MRC和ARC
  1、MRC是什么,我们需要做什么
  2、ARC是什么,帮我们做了什么


一、iOS的内存管理方式


iOS的内存管理方式有哪些?iOS针对不同的场景有不同的内存管理方式,主要有以下两种:

  • 小对象的内存管理 -- Tagged Pointer
  • 普通对象的内存管理 -- 引用计数

1、小对象的内存管理 -- Tagged Pointer

1.1 Tagged Pointer是什么

64位操作系统以后,iOS引入了Tagged Pointer来优化NSNumberNSStringNSDate等小对象的内存管理,所谓Tagged Pointer就是指小对象的数据和类型直接存储在指针里,而非存储在指针指向的那块内存里。那这就引出两个问题:

  • Tagged Pointer具体是怎么存储数据的?
  • Tagged Pointer又是怎么调用方法的?
1.2 Tagged Pointer是怎么存储数据的

引入Tagged Pointer之前,小对象和普通对象一样需要在堆区开辟内存,并把对象的内存地址赋值给相应的指针,然后维护对象的引用计数、释放内存等。比如我们创建一个int类型的NSNumber对象:

NSNumber *number = @11;

系统就需要在堆区开辟16个字节的内存来存储“11”这个值,还需要开辟8个字节的内存来存储这个对象的地址,你看本来最多占4个字节的int类型数据就至少占了24个字节,太浪费内存了,而且这还没考虑维护对象的引用计数、释放内存等开销。

引入Tagged Pointer之前

引入Tagged Pointer之后,小对象就不再需要像普通对象那样在堆区开辟内存、维护引用计数、释放内存了,而是直接把值存进number指针里,也就是说number指针这8个字节里存储的不再是一个地址了,而是:Tag + Data,Tag用来标记小对象是什么类型(如NSNumberNSStringNSDate等),Data就是小对象的值,指针什么时候创建小对象就什么时候创建,指针什么时候销毁小对象就什么时候销毁。除非小对象的值大到指针存不下,小对象才会恢复为原来普通对象那样的内存管理方式。这样系统就只需要开辟8个字节的内存来存储小对象的类型和值,很大程度上节省了内存,也减小了维护对象的引用计数、释放内存等开销。

引入Tagged Pointer之后

我们举例来验证一下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSNumber *number1 = @1;
    NSNumber *number2 = @2;
    NSNumber *number3 = @3;
    NSNumber *number4 = @10;
    NSNumber *number5 = @11;
    NSNumber *number6 = @12;
    NSNumber *number7 = @(0xFFFFFFFFFFFFFFF);
    
    NSLog(@"number1:%p", number1);
    NSLog(@"number2:%p", number2);
    NSLog(@"number3:%p", number3);
    NSLog(@"number4:%p", number4);
    NSLog(@"number5:%p", number5);
    NSLog(@"number6:%p", number6);
    NSLog(@"number7:%p", number7);
}


// 控制台打印:
number1:0xb000000000000012
number2:0xb000000000000022
number3:0xb000000000000032
number4:0xb0000000000000a2
number5:0xb0000000000000b2
number6:0xb0000000000000c2
number7:0x174226480

可见number1 ~ number6指针是Tagged Pointer,它们5 ~ 8位的“1”、“2”、“3”、“a”、“b”、“c”分别存储着小对象的值,而61 ~ 64位的“b”和1 ~ 4位的“2”可能就是标记小对象是个NSNumber类型。而number7因为值大到指针存不下,恢复为原来普通对象那样的内存管理方式了。(如果一个对象的指针的最高位——即第64位——是1,那么它就是个Tagged Pointer,否则就不是。

1.3 Tagged Pointer是怎么调用方法的

从上面的分析我们知道小对象已经不是一个普通对象了,因为它不像普通对象那样拥有isa指针来指向它所属的类,那小对象是怎么调用方法的呢?

NSNumber *number = @11;
NSLog(@"%d", [number intValue]);

首先因为number指针是NSNumber类型的,所以它调用intValue方法在编译时是不会报错的。然后在运行时objc_msgSend函数内部会做判断,如果发现对象是个小对象,就会直接从指针里把数据抽出来或直接把数据存进指针里,而不是走方法调用流程,这也在一定程度上减小了方法调用的开销。

我们举例来验证一下:

// 开辟多个线程去修改一个copy修饰的name属性

for (int i = 0; i < 1000; i++) {
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        self.name = [[NSString alloc] initWithFormat:@"abcdefghijk"];
    });
}

for (int i = 0; i < 1000; i++) {
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        self.name = [[NSString alloc] initWithFormat:@"abc"];
    });
}

第一段代码会崩掉,因为@"abcdefghijk"这个NSString小对象大到指针存不下,恢复为原来普通对象那样的内存管理方式了,所以那句代码就是正常地调用setter方法来修改name属性,而setter方法的内部实现为:

- (void)setName:(NSString *)name {

    if (_name != name) { // 新旧对象不一样时
        
        [_name release]; // 释放旧对象
        _name = [name copy]; // 复制新对象
    }
}

所以在不加锁的情况下,就很有可能有多条线程几乎同时调用[_name release],那对一个已经销毁了的_name再次调用release,程序当然会崩掉了。

而第二段代码可以正常运行,因为@"abc"这个NSString小对象不够大,把它赋值给name指针时,objc_msgSend函数内部会直接把它存进name指针里,而不是走setter方法。

2、普通对象的内存管理 -- 引用计数

2.1 引用计数是什么

iOS是通过引用计数来进行对象的内存管理的。所谓引用计数是指我们每创建一个对象,系统就会为该对象分配一个整数,用来表征当前有多少人想使用该对象。那这就引出两个问题:

  • 创建对象时,系统为对象分配的这个整数存储在哪里呢?也就是说引用计数存储在哪里呢?因为我们知道OC对象内部只有一个固定的成员变量isa,并没有一个引用计数的成员变量啊!
  • iOS具体是怎么通过引用计数来进行对象的内存管理的?
2.2 引用计数存储在哪里

64位操作系统以前,对象的isa指针还没经过内存优化,对象的引用计数是直接存储在引用计数表里的。(因为现在都是64位操作系统了,所以这种情况我们知道一下但不做过多额外的分析)

64位操作系统以后,对象的isa指针经过了内存优化,它不再直接是一个指针,而是一个共用体,64位中只有33位用来存储对象所属类的地址信息,还有19位用来存储(对象的引用计数 - 1),还有1位用来标记引用计数表里是否有当前对象的引用计数。具体地说,对象的引用计数会首先存储在它的isa共用体里——extra_rc变量,但是isa共用体里引用计数的存值范围为0~255,一旦对象的引用计数超过了255,这个变量就会溢出,此时系统会把这个变量置为128,会把引用计数表里是否有当前对象的引用计数的标记——has_sidetable_rc变量——置为1,并把另外的128个引用计数挪到引用计数表里存储。那下一次再增加对象的引用计数时,就依旧增加的是isa共用体里的引用计数(因为它已经被置为128了,不再是溢出状态了),直到再次溢出,系统再挪128个引用计数到引用计数表里存储,如此循环。因此我们就看到在这种情况下,系统其实是不会直接去操作引用计数表里的引用计数的,而总是在操作isa共用体里的引用计数,直到溢出时才从isa共用体里挪128个到引用计数表里存储。

  • isa共用体
struct objc_object {
    // 固定的成员变量
    isa_t isa;

    // 自定义的成员变量
    NSSring *_name;
    NSSring *_sex;
    int _age;
}

// 共用体isa_t
//
// 共用体也是C语言的一种数据类型,和结构体差不多,
// 都可以定义很多的成员变量,但两者的主要区别就在于内存的使用。
//
// 一个结构体占用的内存等于它所有成员变量占用内存之和,而且要遵守内存对齐规则,而一个共用体占用的内存等于它最宽成员变量占用的内存。
// 结构体里所有的成员变量各自有各自的内存,而共用体里所有的成员变量共用这一块内存。
// 所以共用体可以更加节省内存,但是我们要把数据处理好,否则很容易出现数据覆盖。
union isa_t {
    Class cls;
    
    unsigned long bits; // 8个字节,64位
    struct { // 其实所有的数据都存储在成员变量bits里面,因为外界只访问它,而这个结构体则仅仅是用位域来增加代码的可读性,让我们看到bits里面相应的位上存储着什么数据
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
        unsigned long nonpointer        : 1;
        unsigned long has_assoc         : 1;
        unsigned long has_cxx_dtor      : 1;
        unsigned long shiftcls          : 33; // 当前对象所属类的地址信息
        unsigned long magic             : 6;
        unsigned long weakly_referenced : 1; // 当前对象是否有弱引用
        unsigned long deallocating      : 1;
        unsigned long has_sidetable_rc  : 1; // 引用计数表里是否有当前对象的引用计数
        unsigned long extra_rc          : 19; // 对象的引用计数 - 1,存不下了就会放到引用计数表里
# endif
    };
};
  • SideTables --> SideTable --> 引用计数表、弱引用表 --> 引用计数、弱指针数组
我们捎带也把弱引用表说一下吧,因为它和引用计数表存在一块儿
static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}

struct SideTable {
    spinlock_t slock; // 自旋锁
    RefcountMap refcnts; // 引用计数表
    weak_table_t weak_table;
}

typedef objc::DenseMap<objc_object */*对象的内存地址*/, unsigned long/*对象的引用计数*/> RefcountMap;

struct weak_table_t {
    weak_entry_t *weak_entries; // 这个其实才是弱引用表,表中元素为weak_entry_t结构体
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};
struct weak_entry_t {
    objc_object *obj; // 对象的内存地址
    weak_referrer_t *referrers; // 指向该对象的弱指针数组——即所有指向该对象的弱指针(其实存储的是弱指针对应那块内存的地址,但是我们直接理解为弱指针是没有问题的)
}
// 例如:
id obj = [[NSObject alloc] init];
__weak id weakObj1 = obj;
__weak id weakObj2 = obj;
__weak id weakObj3 = obj;

// NSObject对象的weak_entry为:
struct weak_entry_t {
    objc_object *obj; // 对象的内存地址
    weak_referrer_t *referrers; // 指向该对象的弱指针数组
} NSObjectWeakEntry = {
    obj;
    [weakObj1, weakObj2, weakObj3]
}

首先SideTables是一个全局的HashMap,它里面的元素就是一个一个的、共64个SideTable结构体——一个成员变量是对象的内存地址(用作哈希算法的唯一标识),另一个成员变量是指向该对象的引用计数表,另一个成员变量是指向该对象的弱引用表,所以我们可以通过一个对象的内存地址经过特定的哈希算法得到它在SideTables里对应的SideTable结构体,得到了一个对象的SideTable结构体,就相当于得到了该对象的引用计数表和弱引用表。

得到了一个对象的引用计数表和弱引用表之后,因为引用计数表也是一个HashMap,它里面的元素就是一个一个的字典——对象的内存地址为key(用作哈希算法的唯一标识),对象的引用计数为value,所以我们又可以通过一个对象的内存地址经过特定的哈希算法得到它在引用计数表里对应的引用计数;同样弱引用表也是一个HashMap,它里面的元素就是一个一个的结构体——一个成员变量是对象的内存地址(用作哈希算法的唯一标识),另一个成员变量是指向该对象的弱指针数组,所以我们可以通过一个对象的内存地址经过特定的哈希算法得到它在弱引用表里对应的弱指针数组。

所以如果我们想要找到对象的引用计数和弱指针数组,就要首先把对象的内存地址通过特定的哈希算法得到一个index,就可以在SideTables里找到对象的引用计数和弱指针数组所在的SideTable结构体,这也就是找到了对象的引用计数和弱指针数组所在的引用计数表和弱引用表,然后再次把对象的内存地址通过某种散列算法得到一个index,就可以在引用计数表里找到对象的引用计数,在弱引用表里找到对象的弱指针数组了。

关键词:散列表、表中元素、表中元素唯一标识、散列算法、index

散列表(Hash Table,也叫哈希表),就是把表中元素的唯一标识通过某种算法得到一个index,然后通过这个index直接访问表中元素的一种数据结构,这样就不用遍历了,因此可以大大提高数据查找的效率。实现这个算法的函数叫作散列函数,存储数据的数组叫作散列表(但这个数组不是普通的数组,它的元素可以不连续存储,因此散列表就有可能造成内存的空闲,它是一个典型的“以空间换时间”的例子)。散列表的核心就在于散列算法。

2.3 iOS具体是怎么通过引用计数来进行对象的内存管理的

主要是通过allocnewcopymutableCopyretainreleaseautoreleasedealloc这几个方法操作引用计数,来进行对象的内存管理的,即:

  • 我们调用allocnewcopymutableCopy新创建一个对象,系统会给它分配一定的内存,对象的引用计数为1。
// NSObject.mm
+ (id)alloc {
    return _objc_rootAlloc(self);
}

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}


id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

static id callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    return class_createInstance(cls, 0);
}

id class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

static id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, bool cxxConstruct = true, size_t *outAllocatedSize = nil)
{
    // 计算对象需要占用的内存
    size_t size = cls->instanceSize(extraBytes);
    // 调用calloc函数给对象分配1块size大小的内存
    id obj = (id)calloc(1, size);
    
    // 初始化对象的isa
    obj->initInstanceIsa(cls, hasCxxDtor);

    // 返回对象的内存地址
    return obj;
}

size_t instanceSize(size_t extraBytes) {
    // alignedInstanceSize = 对象(结构体)经过内存对齐后的实际大小,extraBytes = 0
    size_t size = alignedInstanceSize() + extraBytes;
    // 系统要求所有的对象至少占用16个字节
    if (size < 16) size = 16;
    return size;
}

void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    initIsa(cls, true, hasCxxDtor);
}

void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
    isa_t newisa(0);
    
    /*
     isa赋初值
     
     #define ISA_MAGIC_MASK  0x0000 03f0 0000 0001
     
     转换成二进制0b
     64~49位:0000 0000 0000 0000
     48~33位:0000 0011 1111 0000
     32~17位:0000 0000 0000 0000
     16~01位:0000 0000 0000 0001
     */
    newisa.bits = ISA_MAGIC_VALUE;
    
    // 标记当前对象是否使用过C++析构函数
    newisa.has_cxx_dtor = hasCxxDtor;
    
    // 把cls的值——即对象所属类的地址——右移三位,初始化进isa共用体的shiftcls中
    newisa.shiftcls = (uintptr_t)cls >> 3;
    
    isa = newisa;
    
    // 可见用alloc、new、copy、mutableCopy创建对象时,系统其实并不会把isa共用体里的引用计数置为1(extra_rc在isa的64位~46位上,为0的),所以我们说对象的isa共用体里存储的是(对象的引用计数 - 1)
}
// copy和mutableCopy则需要我们自己遵守NSCopying和NSMutableCopying协议,并实现copyWithZone:和mutableCopyWithZone:方法,它们内部还是会调用alloc那一套方法,不过copy和mutableCopy针对的是一个新对象
// 但其实我们自己定义的类不存在什么mutableCopy,mutableCopy仅仅是给系统NSString、NSArray、NSDictionary等部分类留的

@interface INEPerson () <NSCopying>

@property (nonatomic, copy) NSString *name;

@end

@implementation INEPerson

- (id)copyWithZone:(NSZone *)zone {
    
    INEPerson *person = [[INEPerson allocWithZone:zone] init];
    // 一些属性的赋值......
    person.name = self.name;
    
    return person;
}

@end
  • 如果有别的人想使用该对象,就调用retain方法把对象的引用计数+1,即持有该对象。
// NSObject.mm
- (id)retain {
    return self->rootRetain();
}

id objc_object::rootRetain()
{
    return rootRetain(false, false);
}

id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    if (isTaggedPointer()) { // 如果是Tagged Pointer,它不是通过引用计数来进行内存管理的,所以不需要对引用计数+1,直接返回对象自己
        
        return (id)this;
    }
        
    // 拿到对象的isa共用体
    isa_t newisa = LoadExclusive(&isa.bits);
    
    if (!newisa.nonpointer) { // 如果对象的isa是没经过内存优化的,那么它的引用计数就直接存储在引用计数表里

        // 去引用计数表里让它的引用计数+1
        sidetable_retain();
    }
    
    // 否则就表明对象的isa是经过内存优化的,那么它的引用计数就首先存储在isa共用体里
    
    // 用来标识extra_rc是否上溢(extra_rc的存值范围为0~255,即最多存储256个引用计数)
    uintptr_t carry;
    // 首先去isa共用体里,让对象的引用计数+1
    newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
    
    if (carry) { // 所以如果extra_rc上溢了
        
        // 就保留一半的引用计数——即128个,RC_HALF = 128——在共用体里,另外的128个准备复制到引用计数表里存储
        newisa.extra_rc = RC_HALF;
        
        // 并把引用计数表里是否有当前对象的引用计数标识has_sidetable_rc置为1
        newisa.has_sidetable_rc = true;
        
        // 更新一下对象的isa共用体
        isa = newisa;
        
        // 把另外的128个引用计数复制到引用计数表里存储(由此可见对象下一次调用retain时,加得还是共用体里的引用计数,因为它已经不再处于溢出状态了,直到再次溢出再复制128个引用计数到引用计数表里存储,如此循环)
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    // 否则就表明extra_rc没有溢出,一切正常
    
    // 返回对象自己
    return (id)this;
}

// 去引用计数表里让它的引用计数+1
id objc_object::sidetable_retain()
{
    SideTable& table = SideTables()[this];
    RefcountMap refcnts = table.refcnts;
    
    table.lock();
    size_t& refcntStorage = refcnts[this];
    refcntStorage += SIDE_TABLE_RC_ONE;
    table.unlock();

    return (id)this;
}

// 把另外的128个引用计数复制到引用计数表里存储
bool objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
    // 首先把当前对象的内存地址通过某种散列算法得到一个index,就可以在SideTables里找到对象的引用计数所在的SideTable结构体
    SideTable& table = SideTables()[this];
    // 这也就是找到了对象的引用计数所在的引用计数表
    RefcountMap refcnts = table.refcnts;
    
    // 然后再次把当前对象的内存地址通过某种散列算法得到一个index,就可以在引用计数表里找到对象的引用计数
    size_t& refcntStorage = refcnts[this];
    size_t oldRefcnt = refcntStorage;

    // 引用计数表里的引用计数有64位,但是最低1位是用来标记当前对象是否有弱引用,最低2位是用来标记当前对象是否正在释放,所以一共有62位用来存储引用计数,这足够大了!!!
    assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);

    if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;
    
    uintptr_t carry;
    // 让引用计数+128
    size_t newRefcnt =
        addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
    // 引用计数表这里也判断了是否溢出
    if (carry) {
        refcntStorage =
            SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
        return true;
    }
    else {
        refcntStorage = newRefcnt;
        return false;
    }
}
  • 如果有人不想使用该对象了,就调用releaseautorelease方法把对象的引用计数-1,即释放该对象。
// NSObject.mm
- (void)release {
    self->rootRelease();
}

bool objc_object::rootRelease()
{
    return rootRelease(true, false);
}

bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    if (isTaggedPointer()) return false;

    isa_t newisa = LoadExclusive(&isa.bits);

    if (!newisa.nonpointer) {
        
        // 去引用计数表里让它的引用计数-1
        return sidetable_release(performDealloc);
    }
    

    // 用来标识extra_rc是否下溢——即是否减为-1(因为extra_rc存储的是(引用计数 - 1),所以减为0的时候说明引用计数为1,还有人引用它,没事儿)
    uintptr_t carry;
    // 首先去isa共用体里,让对象的引用计数-1
    newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
    if (carry) { // 所以如果extra_rc下溢了:
    
        goto underflow; // 跳转到underflow处执行
    }

    // 表明没有下溢,结束
    return false;

 underflow: // extra_rc下溢了:

    newisa = LoadExclusive(&isa.bits);

    if (newisa.has_sidetable_rc) { // 如果引用计数表里有当前对象的引用计数,说明还有人使用该对象

        // 尝试从引用计数表搬回来128个引用计数
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
        if (borrowed) { // 如果搬成功了
            
            // 存进去
            newisa.extra_rc = borrowed - 1;
            // 更新一下对象的isa共用体
            isa = newisa;
        } else { // 搬失败了,说明引用计数表里的引用计数也为0了(可能是被上一次搬完了)
            
            // 走dealloc方法销毁该对象
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
        }
    } else { // 引用计数表里没有当前对象的引用计数,说明没人使用该对象了
        
        // 走dealloc方法销毁该对象
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    
    return true;
}

// 尝试从引用计数表搬回来128个引用计数
size_t objc_object::sidetable_subExtraRC_nolock(size_t delta_rc)
{
    SideTable& table = SideTables()[this];
    RefcountMap::iterator it = table.refcnts.find(this);
    
    if (it == table.refcnts.end()  ||  it->second == 0) { // 引用计数表里的引用计数被上一次搬完了

        return 0;
    }
    
    // 引用计数表里的引用计数-128,搬出去
    size_t oldRefcnt = it->second;
    size_t newRefcnt = oldRefcnt - (delta_rc << SIDE_TABLE_RC_SHIFT);
    it->second = newRefcnt;
    
    return delta_rc;
}

关于autoreleaseautoreleasepool就暂时理解这么一点,更底层的东西有空再说:

  • release会立即使对象的引用计数-1,而autorelease则不会,它仅仅是把该对象注册到了RunLoop某一次循环的autoreleasepool中,当autoreleasepool销毁时系统会自动让池中所有的对象都调用一下release,这时对象的引用计数才-1。
  • autoreleasepool又是在RunLoop休眠或退出时销毁的,当然如果是我们自己写的@autoreleasepool{...},出了大括号——即出了@autoreleasepool{...}的生命周期,它就会销毁。
  • 只要不是用allocnewcopymutableCopy方法创建的对象,而是用类方法创建的对象,方法内部都调用了autorelease,都是autorelease对象。
  • 如果对象的引用计数减为0了,就代表没有人想使用该对象,系统就会调用dealloc方法销毁它,并释放它对应的内存,对象一经销毁就绝对不能再访问了,因为它的内存随时可能被移作它用。
// NSObject.mm
- (void)dealloc {
    _objc_rootDealloc(self);
}

void _objc_rootDealloc(id obj)
{
    obj->rootDealloc();
}

void objc_object::rootDealloc()
{
    if (
        !isa.has_cxx_dtor  && // 如果当前对象没使用过C++析构函数
        !isa.has_assoc  && // 如果当前对象没有关联对象
        !isa.weakly_referenced  && // 如果弱引用表里没有当前对象的弱指针数组
        !isa.has_sidetable_rc // 如果引用计数表里没有当前对象的引用计数
        )
    {
        // 就直接销毁对象,并释放它对应的内存,即我们之前说的对象销毁时会更快
        free(this);
    } else {
        
        // 否则就慢慢销毁
        object_dispose(this);
    }
}

id object_dispose(id obj)
{
    objc_destructInstance(obj);
    // 销毁对象,并释放它对应的内存,
    free(obj);

    return nil;
}

void *objc_destructInstance(id obj) 
{
    if (obj) {
        
        // 如果当前对象使用过C++析构函数
        bool cxx = obj->hasCxxDtor();
        // 如果当前对象有关联对象
        bool assoc = obj->hasAssociatedObjects();

        // 要按顺序销毁哦
        if (cxx) object_cxxDestruct(obj); // 销毁C++析构函数相关的东西
        if (assoc) _object_remove_assocations(obj); // 移除关联对象
        obj->clearDeallocating();
    }

    return obj;
}

void objc_object::clearDeallocating()
{
    clearDeallocating_slow();
}

void objc_object::clearDeallocating_slow()
{
    // 获取SideTable
    SideTable& table = SideTables()[this];

    if (isa.weakly_referenced) { // 如果弱引用表里有当前对象的弱指针数组
        
        // 把弱引用表里所有指向该对象的弱指针都置为nil,并移除,从此弱引用表里就没有该对象的弱指针数组(关于弱指针(弱引用)更多详细的内容,见下面__weak指针的实现原理)
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    
    if (isa.has_sidetable_rc) { // 如果引用计数表里有当前对象的引用计数
        
        // 从引用计数表里把该对象的引用计数给抹掉,从此引用计数表里就没有该对象的引用计数了
        table.refcnts.erase(this);
    }
}


二、MRC和ARC


1、MRC是什么,我们需要做什么

MRC是指手动管理引用计数,即需要我们程序员自己手动调用上面那几个allocnewcopymutableCopyretainreleaseautoreleasedealloc方法来操作引用计数,从而完成对象的内存管理。具体地说,MRC下我们需要做到以下三点:

  • 1️⃣但凡调用了allocnewcopymutableCopy创建对象的地方,在不想使用对象时,要调用releaseautorelease来释放对象;但凡调用了retain使对象引用计数+1的地方,在不想使用对象时,要调用releaseautorelease来使对象的引用计数-1。
- (void)viewDidLoad {
    [super viewDidLoad];
  
    // 调用了alloc、new、copy、mutableCopy创建对象
    NSArray *arr = [[NSArray alloc] init];
    NSArray *arr1 = [NSArray new];
    NSArray *arr2 = [arr copy];
    NSMutableArray *arr3 = [arr1 mutableCopy];
    
    // 调用release、autorelease来释放对象
    [arr release];
    [arr1 release];
    [arr2 autorelease];
    [arr3 autorelease];
}


- (void)viewDidLoad {
    [super viewDidLoad];
  
    NSArray *arr = [NSArray array];
    // 调用了retain使对象引用计数+1
    [arr retain];

    // 要调用release、autorelease来使对象的引用计数-1
    [arr release];
//    [arr autorelease];
}
  • 2️⃣我们还要处理好setter方法内部的内存管理,并在dealloc方法里释放当前类及父类所有对象类型的成员变量。
@implementation INEPerson {
    int _age;
    NSString *_name;
    INEDog *_dog;
}

- (void)setAge:(int)age {
    
    _age = age; // 直接赋值
}

- (void)setName:(NSString *)name {

    if (_name != name) { // 新旧对象不一样时
        
        [_name release]; // 释放旧对象
        _name = [name copy]; // 复制新对象
    }
}

- (void)setDog:(INEDog *)dog {
    
    if (_dog != dog) { // 新旧对象不一样时
        
        [_dog release]; // 释放旧对象
        _dog = [dog retain]; // 持有新对象
    }
}

- (void)dealloc {
    
    // 释放当前类所有对象类型的成员变量
    [self setName:nil];
    [self setDog:nil];

    // 释放父类所有对象类型的成员变量,放在最后
    [super dealloc];
}

@end
  • 3️⃣我们还要处理好循环引用问题。
// INEMan.h
#import <Foundation/Foundation.h>
@class INEWoman;

@interface INEMan : NSObject

- (void)setWoman:(INEWoman *)woman;

@end


// INEMan.m
#import "INEMan.h"
#import "INEWoman.h"

@implementation INEMan {
    
    INEWoman *_woman;
}

- (void)setWoman:(INEWoman *)woman {

    if (_woman != woman) { // 新旧对象不一样时

        [_woman release]; // 释放旧对象
        _woman = [woman retain]; // 持有新对象
    }
}

- (void)dealloc {
  
    NSLog(@"%s", __func__);
    
    [self setWoman:nil];
    
    [super dealloc];
}

@end
// INEWoman.h
#import <Foundation/Foundation.h>
@class INEMan;

@interface INEWoman : NSObject

- (void)setMan:(INEMan *)man;

@end


// INEWoman.m
#import "INEWoman.h"
#import "INEMan.h"

@implementation INEWoman {
    
    INEMan *_man;
}

- (void)setMan:(INEMan *)man {

    _man = man; // 为了避免循环引用,这里不能retain
}

- (void)dealloc {
  
    NSLog(@"%s", __func__);
    
    [self setMan:nil];
    
    [super dealloc];
}

@end
- (void)viewDidLoad {
    [super viewDidLoad];

    INEMan *man = [[INEMan alloc] init];
    INEWoman *woman = [[INEWoman alloc] init];
    
    [man setWoman:woman];
    [woman setMan:man];
    
    [man release];
    [woman release];
}

// 控制台打印:两个对象都可以正常销毁
-[INEWoman dealloc]
-[INEMan dealloc]

2、ARC是什么,帮我们做了什么

ARC是指自动管理引用计数,即编译器会在合适的地方自动帮我们插入retainreleaseautorelease等方法的调用,从而完成对象的内存管理。但实际上除了编译器之外,ARC还用到了Runtime,比如weak指针的清空。具体地说,针对MRC的三点,ARC帮我们做了如下三点:

  • 利用__strong指针修饰符,编译器会在合适的地方自动帮我们插入retainreleaseautorelease等方法的调用;
  • 利用属性修饰符,编译器帮我们生成特定的setter方法并处理好其内部的内存管理,还会自动在dealloc方法里释放父类及当前类所有对象类型的成员变量;
  • 利用__weak指针修饰符和Runtime,来处理循环引用问题。
2.1 指针修饰符
  • __strong指针修饰符

但凡是用__strong修饰的指针,在超出其作用域时,编译器会自动帮我们插入一次releaseautorelease的调用。

// ARC下
{
    __strong id obj = [[NSObject alloc] init];
    __strong id arr = [NSArray array];
}

等价于:

// MRC下
{
    id obj = [[NSObject alloc] init];
    id arr = [[NSArray alloc] init];
    
    [obj release];
    [arr autorelease];
}

而在指针赋值时,编译器会自动帮我们插入一次retain的调用。

// ARC下
{
    __strong id obj = [[NSObject alloc] init];
    __strong id obj1 = obj;
}

等价于:

// MRC下
{
    id obj = [[NSObject alloc] init];
    id obj1 = [obj retain];
    
    [obj release];
    [obj1 release];
}

所以说正是利用__strong指针修饰符,编译器才会在合适的地方自动帮我们插入retainreleaseautorelease等方法的调用,而所有的指针默认都是用__strong修饰的。

  • __weak指针修饰符

看起来有了__strong,编译器就可以很好地进行内存管理了呀!但遗憾的是,__strong无法解决引用计数式内存管理必然会导致的“循环引用”问题。

// INEMan.h
#import <Foundation/Foundation.h>
@class INEWoman;

@interface INEMan : NSObject {
    
    __strong INEWoman *_woman; // 强引用
}

- (void)setWoman:(INEWoman *)woman;

@end


// INEMan.m
#import "INEMan.h"

@implementation INEMan 

- (void)setWoman:(INEWoman *)woman {

    _woman = woman;
}

- (void)dealloc {
  
    NSLog(@"%s", __func__);
}

@end
// INEWoman.h
#import <Foundation/Foundation.h>
@class INEMan;

@interface INEWoman : NSObject {
    
    __strong INEMan *_man; // 强引用
}

- (void)setMan:(INEMan *)man;

@end


// INEWoman.m
#import "INEWoman.h"

@implementation INEWoman

- (void)setMan:(INEMan *)man {

    _man = man;
}

- (void)dealloc {
  
    NSLog(@"%s", __func__);
}

@end
// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];

    INEMan *man = [[INEMan alloc] init];
    INEWoman *woman = [[INEWoman alloc] init];
    
    [man setWoman:woman];
    [woman setMan:man];
}

// 控制台打印:无

viewDidLoad执行完,Man对象和Woman对象的dealloc方法都没走,也就是说它们俩都没销毁,这就是因为它们俩形成了循环引用,导致了内存泄漏。

但是只要我们把循环引用中的一个强指针的换成弱指针,就可以解决问题。

@interface INEMan : NSObject {
    
     __weak INEWoman *_woman; // 弱引用
}

或者:

@interface INEWoman : NSObject {
    
    __weak INEMan *_man; // 强引用
}

为什么能解决呢?这就要来看看__weak指针的实现原理:

  • __weak指针是专门用来解决循环引用问题的,它不是通过引用计数来管理对象的,而是通过弱引用表。具体地说:
  • 当我们把一个强指针赋值给一个弱指针时,编译器并不会自动帮我们插入retain使对象的引用计数+1,而是把这个弱指针和当前对象的内存地址捆绑在一起,通过两次散列算法找到相应弱引用表里的弱指针数组,把这个弱指针存储到弱指针数组里。这样我们通过这些弱指针既可以正常使用该对象,又无需顾虑是不是要在什么时候再把对象的引用计数-1、以免对象一直有引用计数而销毁不掉,它压根儿就没参与引用计数那一套嘛。
  • 而当对象销毁时会走dealloc方法,又会去通过两次散列算法找到相应弱引用表里弱指针数组,把所有指向该对象的弱指针都置为nil并移除。

源码就不贴了,可以自己去看,有了上面那些源码分析,看起来应该是不费力气的。这里只提供个源码入口,我们定义一个__weak指针,系统其实调用了objc_initWeak函数。

id obj = [[NSObject alloc] init];
__weak id obj1 = obj;

等价于:

id obj = [[NSObject alloc] init];
id obj1;
objc_initWeak(&obj1, obj);
// NSObject.mm
// weakObj:弱指针(其实是弱指针对应那块内存的地址,但我们直接理解为弱指针是没有问题的)
// obj:当前对象的内存地址
id objc_initWeak(id *weakObj, id obj)
{
    // ......
}
2.2 属性修饰符

属性修饰符一共有三对儿:原子性、读写权限和内存管理语义,属性修饰符主要影响就是编译器为属性生成的settergetter方法上。(虽然只有内存管理语义和内存管理相关,此处不妨一起回顾一下)

原子性:atomicnonatomic

  • atomic:默认为atomic,使用atomic修饰的属性,编译器为该属性生成的settergetter方法内部是加了锁的。
@property (atomic, strong) NSMutableArray *array;

- (void)setArray:(NSMutableArray *)array {
    
    // 加锁
    _array = array;
    // 解锁
}

- (NSMutableArray *)array {
    // 加锁
    return _array;
    // 解锁
}

但这仅仅是保证我们调用settergetter方法访问属性这一步是线程安全的,它没法保证我们使用属性的线程安全,比如我们调用[self.array addObject:xxx]self.array访问属性这一步是线程安全的,但addObject:使用属性这一步是线程不安全的。

// 线程1
[self.array addObject:@"11"];
// 线程2
[self.array addObject:@"12"];

等价于

// 线程1
[[self array] addObject:@"11"];
// 线程2
[[self array] addObject:@"12"];

所以为了保证使用属性的线程安全,我们还得在需要的地方自己加锁,这样一来使用atomic修饰属性就是多此一举了,而且settergetter方法的调用通常是很频繁的,内部加锁的话会很耗性能。

// 线程1
// 加锁
[self.array addObject:@"11"];
// 解锁

// 线程2
// 加锁
[self.array addObject:@"12"];
// 解锁
  • nonatomic:因此我们在实际开发中总是使用nonatomic

读写权限:readwritereadonly

  • readwrite:默认为readwrite,代表该属性可读可写,编译器会为该属性生成settergetter方法的声明与实现。

  • readonly:代表该属性只能读取不能写入,编译器会为该属性生成settergetter方法的声明与getter方法的实现。

内存管理语义:

  • MRC下有:assignretaincopy
  • ARC下又新增了:strongweakunsafe_unretained
  • assignassign一般用来修饰基本数据类型。使用assign修饰的属性,编译器为该属性生成的setter方法内部只会进行简单的赋值操作。
- (void)setAge:(int)age {
    
    // 简单的赋值操作
    _age = age;
}
  • retainretain一般用来修饰对象类型。使用retain修饰的属性,编译器为该属性生成的setter方法内部会调用一下retain方法,使对象的引用计数+1。
- (void)setDog:(INEDog *)dog {
    
    if (_dog != dog) { // 新旧对象不一样时
        
        [_dog release]; // 释放旧对象
        _dog = [dog retain]; // 持有新对象
    }
}
  • copycopy一般用来修饰不可变属性和block。使用copy修饰的属性,编译器为该属性生成的setter方法内部会调用一下copy方法,生成一个新对象,新对象的引用计数为1,而旧对象的引用计数不变。
- (void)setName:(NSString *)name {

    if (_name != name) { // 新旧对象不一样时
        
        [_name release]; // 释放旧对象
        _name = [name copy]; // 复制新对象
    }
}
  • strong:默认为strong,大多数情况下和retain的效果是一样的,修饰block时和copy的效果是一样的,strong一般用来修饰对象类型。
  • weakweak一般用来修饰代理对象和NSTimer,以免造成循环引用;还用来修饰xib或sb拖出来的控件,因为这些控件已经被添加到界面上了,被subviews这个属性持有了,不必再用变量持有。
  • unsafe_unretained:和assign的效果是一样的,如果你非要用它们来修饰对象类型,那也只好说它们和weak的功能类似,但weak修饰的属性在对象销毁时会被被置为nil,比较安全,而unsafe_unretainedassign修饰的属性则不会,所以容易出现野指针。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容