iOS内存管理一:Tagged Pointer&引用计数

从这篇文章开始探索iOS的内存管理,主要涉及的内容有
1. 内存布局;
2. 内存管理方案:Tagged Pointer、NONPOINTER_ISA、SiddeTables
3. ARC&MRC:retain和release以及retainCount
4. 自动释放池:autoreleasepool
5. 弱引用weak的实现原理

1、内存布局

iOS中内存布局区域大概分为五个区域:栈区、堆区、BSS段、数据段、代码段,他们在内存的分布如下图所示:

内存布局
  • 栈区:编译器自动分配,由系统管理,在不需要的时候自动清除。局部变量、函数参数存储在这里。栈区的内存地址一般是0x7开头。
  • 堆区:那些由newallocblock copy创建的对象存储在这里,是由开发者管理的,需要告诉系统什么时候释放内存。ARC下编译器会自动在合适的时候释放内存,而在MRC下需要开发者手动释放。堆区的内存地址一般是0x6开头。
  • BSS段:BSS段又称静态区,未初始化的全局变量,静态变量存放在这里。程序运行过程中内存中的数据一直存在,程序结束后由系统释放。
  • 数据段:数据段又称常量区,专门存放常量,程序结束后由系统释放。
  • 代码段:用于存放程序运行时的代码,代码会被编译成二进制存进内存的程序代码区。

这里有点值得一提的是静态变量的作用域与对象、类、分类没关系,只与文件有关系

static int age = 10;

@interface Person : NSObject
-(void)add;
+(void)reduce;
@end

@implementation Person

- (void)add {
    age++;
    NSLog(@"Person内部:%@-%p--%d", self, &age, age);
}

+ (void)reduce {
    age--;
    NSLog(@"Person内部:%@-%p--%d", self, &age, age);
}
@end


@implementation Person (DS)

- (void)ds_add {
    age++;
    NSLog(@"Person (DS)内部:%@-%p--%d", self, &age, age);
}

@end

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"vc:%p--%d", &age, age);
    age = 40;
    NSLog(@"vc:%p--%d", &age, age);
    [[Person new] add];
    NSLog(@"vc:%p--%d", &age, age);
    [Person reduce];
    NSLog(@"vc:%p--%d", &age, age);
    [[Person new] ds_add];
}

打印结果:
2020-03-23 16:53:35.671470+0800 ThreadDemo[40300:1619888] vc:0x103688cc0--10
2020-03-23 16:53:35.671611+0800 ThreadDemo[40300:1619888] vc:0x103688cc0--40
2020-03-23 16:53:35.671809+0800 ThreadDemo[40300:1619888] Person内部:<Person: 0x60000239c640>-0x103688d88--11
2020-03-23 16:53:35.671926+0800 ThreadDemo[40300:1619888] vc:0x103688cc0--40
2020-03-23 16:53:35.672071+0800 ThreadDemo[40300:1619888] Person内部:Person-0x103688d88--10
2020-03-23 16:53:35.672183+0800 ThreadDemo[40300:1619888] vc:0x103688cc0--40
2020-03-23 16:53:35.672332+0800 ThreadDemo[40300:1619888] Person (DS)内部:<Person: 0x6000023a7820>-0x103688cc4--11

从上面运行结果可以知道,在Person类、Person分类、Controller中针对静态变量age的操作,其值并不相互影响。

2、内存管理方案

OC中对内存优化管理的方案有如下几种形式:Tagged Ponter、NONPOINTER_ISA 、SideTable。下面对着三种方案逐一解释。

2.1、Tagged Ponter

在 2013 年 9 月,苹果推出了 iPhone5s,与此同时,iPhone5s 配备了首个采用 64 位架构的 A7 双核处理器,为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念。

  • Tagged Pointer是专⻔⽤来存储⼩的对象,例如NSNumberNSDate等。
  • Tagged Pointer指针的值不再是地址了,⽽是真正的值。所以,实际上它不再是⼀个对象了,它只是⼀个披着对象⽪的普通变量⽽已。所以,它的内存并不存储在堆中,也不需要mallocfree
  • 在内存读取上有着3倍的效率,创建时⽐以前快106倍。

那么Tagged Ponter对于内存优化的点在哪里呢?

2.1.1、Tagged Ponter内存优化

对于一个NSNumber对象,其值是一个整数。正常情况下,如果这个整数只是一个 NSInteger的普通变量,那么它在32位CPU下占 4 个字节,在 64 位CPU下占 8 个字节的。而NSNumber对象还有一个isa指针,它在32位CPU下为4个字节,在 64 位 CPU 下也是 8 个字节。所以从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种 NSNumber、NSDate 一类的对象所占用的内存会翻倍。如下图所示(图片摘自唐巧博客):

image

而实际上一个NSNumber、NSDate这一类的变量的值需要的内存空间常常不需要8个字节,那么如上述来进行数据的存储,内存空间的浪费是很大的。Tagged Ponter恰恰解决了这一块的问题。
Tagged Ponter将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了Tagged Pointer对象之后,64位CPU下NSNumber 的内存图变成了以下这样:

image.png

2.1.2、Tagged Ponter的底层探索

先来看一下关于Tagged Ponter的底层源码。

static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}

static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
    if (tag <= OBJC_TAG_Last60BitPayload) {
        uintptr_t result =
            (_OBJC_TAG_MASK | 
             ((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) | 
             ((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
        return _objc_encodeTaggedPointer(result);
    } else {
        uintptr_t result =
            (_OBJC_TAG_EXT_MASK |
             ((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
             ((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
        return _objc_encodeTaggedPointer(result);
    }
}

从上面的这代码可以看出来,系统调用了_objc_decodeTaggedPointer_objc_taggedPointersEnabled这两个方法对于taggedPointer对象的指针进行了编码和解编码,这两个方法都是将指针地址和objc_debug_taggedpointer_obfuscator进行异或操作,我们都知道将a和b异或操作得到c再和a进行异或操作便可以重新得到a的值,通常可以使用这个方式来实现不用中间变量实现两个值的交换。Tagged Pointer正是使用了这种原理。
在上面讲过,Tagged Pointer对象指针的值不再是地址了,⽽是真正的值,那我们需要知道的是Tagged Pointer的值的存储方式。看如下代码:

#define _OBJC_TAG_MASK (1UL << 63)
NSMutableString *mutableStr = [NSMutableString string];
NSString *immutable = nil;
char c = 'a';
do {
    [mutableStr appendFormat:@"%c", c++];
    immutable = [mutableStr copy];
    NSLog(@"0x%lx %@ %@", _objc_decodeTaggedPointer_(immutable), immutable, immutable.class);
} while (((uintptr_t)immutable & _OBJC_TAG_MASK) == _OBJC_TAG_MASK);

打印结果:
2020-03-25 17:05:22.784213+0800 taggedPointer[76305:3706620] 0xa000000000000611 a NSTaggedPointerString
2020-03-25 17:05:22.784368+0800 taggedPointer[76305:3706620] 0xa000000000062612 ab NSTaggedPointerString
2020-03-25 17:05:22.784481+0800 taggedPointer[76305:3706620] 0xa000000006362613 abc NSTaggedPointerString
2020-03-25 17:05:22.784594+0800 taggedPointer[76305:3706620] 0xa000000646362614 abcd NSTaggedPointerString
2020-03-25 17:05:22.784698+0800 taggedPointer[76305:3706620] 0xa000065646362615 abcde NSTaggedPointerString
2020-03-25 17:05:22.784791+0800 taggedPointer[76305:3706620] 0xa006665646362616 abcdef NSTaggedPointerString
2020-03-25 17:05:22.784874+0800 taggedPointer[76305:3706620] 0xa676665646362617 abcdefg NSTaggedPointerString
2020-03-25 17:05:22.784955+0800 taggedPointer[76305:3706620] 0xa0022038a0116958 abcdefgh NSTaggedPointerString
2020-03-25 17:05:22.785044+0800 taggedPointer[76305:3706620] 0xa0880e28045a5419 abcdefghi NSTaggedPointerString
2020-03-25 17:05:22.785173+0800 taggedPointer[76305:3706620] 0x409bac70e6d7a14b abcdefghij __NSCFString

从上面这段代码的运行结果可以看出当字符串的长度增加到10时,字符串的类型输出是__NSCFString,当长度小于10时,字符串类型输出是NSTaggedPointerString,而且其地址都是0xa开头。回过头来看上述的代码,while中循环条件是为了判断在64位数据中最高是否是1,以此来判断当前的对象是否是一个Tagged Pointer对象。我们将0xa转换为二进制1010,其中最高位1表示是对象是一个Tagged Pointer对象,余下010(十进制2)表示的是对象是一个NSString类型。那么对象的值存在哪里呢,拿0xa000000000000611来说,其中的61就是对应的ASII码中的a。其他的可以照此类推。

如下是系统提供的各种标志位的定义。

enum objc_tag_index_t : uint16_t
{
    // 60-bit payloads
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6,

    // 60-bit reserved
    OBJC_TAG_RESERVED_7        = 7, 

    // 52-bit payloads
    OBJC_TAG_Photos_1          = 8,
    OBJC_TAG_Photos_2          = 9,
    OBJC_TAG_Photos_3          = 10,
    OBJC_TAG_Photos_4          = 11,
    OBJC_TAG_XPC_1             = 12,
    OBJC_TAG_XPC_2             = 13,
    OBJC_TAG_XPC_3             = 14,
    OBJC_TAG_XPC_4             = 15,

    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263, 

    OBJC_TAG_RESERVED_264      = 264
};

系统提供了判断是否是Tagged Pointer的方法

#   define _OBJC_TAG_MASK (1UL<<63)
static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr) 
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

更加详细的资料请参阅Tagged Pointers

2.2、NONPOINTER_ISA

NONPOINTER_ISA同样是苹果公司对于内存优化的一种方案。用 64 bit 存储一个内存地址显然是种浪费,毕竟很少有那么大内存的设备。于是可以优化存储方案,用一部分额外空间存储其他内容isa 指针第一位为 1 即表示使用优化的 isa 指针,这里列出在__x86_64__架构下的 64 位环境中 isa 指针结构,__arm64__的架构会有所差别。

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

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
      uintptr_t nonpointer        : 1;                                         
      uintptr_t has_assoc         : 1;                                         
      uintptr_t has_cxx_dtor      : 1;                                         
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ 
      uintptr_t magic             : 6;                                         
      uintptr_t weakly_referenced : 1;                                         
      uintptr_t deallocating      : 1;                                         
      uintptr_t has_sidetable_rc  : 1;                                         
      uintptr_t extra_rc          : 8
    };
#endif
};
  • nonpointer:示是否对isa开启指针优化。0代表是纯isa指针,1代表除了地址外,还包含了类的一些信息、对象的引用计数等
  • has_assoc:关联对象标志位,0没有,1存在。
  • has_cxx_dtor:该对象是否有C++或Objc的析构器,如果有析构函数,则需要做一些析构的逻辑处理,如果没有,则可以更快的释放对象。
  • shiftcls:存在类指针的值,开启指针优化的情况下,arm64位中有33位来存储类的指针
  • magic:判断当前对象是真的对象还是一段没有初始化的空间
  • weakly_referenced:是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象释放的更快。
  • deallocating:标志是否正在释放内存。
  • has_sidetable_rc:是否有辅助的引用计数散列表。当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位。
  • extra_rc:表示该对象的引⽤计数值,实际上是引⽤计数值减 1,例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到下⾯的 has_sidetable_rc。

其结构如下图所示:


image

2.3、SideTable

SideTable在OC中扮演这一个很重要的角色。在runtime中,通过SideTable来管理对象的引用计数以及weak引用。同时,系统中维护了一个全局的SideTables,这是一个SideTable的集合。

image

来看看SideTable的定义:

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
}

SideTable的定义很清晰,有三个成员:

  • spinlock_t slock:自旋锁,用于上锁/解锁 SideTable。
  • RefcountMap refcnts:用来存储OC对象的引用计数的 hash表(仅在未开启isa优化或在isa优化情况下isa_t的引用计数溢出时才会用到)。
  • weak_table_t weak_table:存储对象弱引用指针的hash表。是OC中weak功能实现的核心数据结构。

关于更多的SideTable的内容请移步我之前的文章iOS底层原理:weak的实现原理,在这篇文章中详细介绍了SideTable。

3、引用计数

3.1、什么是引用计数

摘自百度百科引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。同时引用计数还可以指使用引用计数技术回收未使用资源的垃圾回收算法。

当一个对象创建并在堆区申请内存时,对象的引用计数为1;当其他的对象需要持有这个对象时,就需要将这个对象的引用计数加1;当其他的对象不再需要持有这个对象时,需要将对象的引用计数减1;当对象的引用计数为0时,对象的内存就会立即释放,对象销毁。

  • 调用alloc、new、copy、mutableCopy名称开头的方法创建的对象,该对象的引用计数加1。
  • 调用retain方法时,该对象的引用计数加1。
  • 调用release方法时,该对象的引用计数减1。
  • autorelease方法不改变该对象的引用计数器的值,只是将对象添加到自动释放池中。
  • retainCount方法返回该对象的引用计数值。

3.2、对象持有规则

对象的持有规则如下:

  1. 自己生成的对象,自己持有。
  2. 非自己生成的对象,自己也能持有。
  3. 不再需要自己持有的对象时释放。
  4. 非自己持有的对象无法释放。

对象的持有标准在于对象的引用计数的值,那么结合对象创建方式,对象的引用计数加减,对象的销毁大致如下的关系:

| 对象操作 | Objective-C方法 |
|:------:|:------------:|:------------:|:------------:|:------------:|
| 生成并持有对象 | alloc/new/copy/mutableCopy等方法 |
| 持有对象 | retain方法 |
| 释放对象 | release方法 |
|废弃对象 |dealloc方法 |

3.2.1、自己生成的对象,自己持有

使用以下名称开头的方法名意味着自己生成的对象只有自己持有:alloc、new、copy、mutableCopy
在OC中对象的创建可以通过allocnew这两种方式来创建一个对象。

NSObject *obj = [NSObject alloc];
NSObject *obj1 = [NSObject new];//等价于 NSObject *obj1 = [[NSObject alloc]init];

关于alloc和new的相关知识请移步之前的文章IOS底层原理之alloc、init和new,在这里就不多加描述。这里着重讲解一下copymutableCopy,它们意味着对象的拷贝。对象的拷贝需要遵循NSCopying协议和NSMutableCopying协议。

@interface Person : NSObject<NSCopying,NSMutableCopying>

@end

@implementation Person


- (nonnull id)copyWithZone:(nullable NSZone *)zone { 
    Person *person = [[self class] allocWithZone:zone];
    return person;
}

- (id)mutableCopyWithZone:(NSZone *)zone{
    Person *person = [[self class] allocWithZone:zone];
    return person;
}

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc]init];
    Person *person1 = [person copy];
    Person *person2 = [person mutableCopy];
    NSLog(@"person:%p--person1:%p--person2:%p",person,person1,person2);
}

打印结果:
2020-03-26 15:56:26.666859+0800 taggedPointer[89806:4395707] person:0x6000038342c0 retainCount:1
2020-03-26 15:56:26.667011+0800 taggedPointer[89806:4395707] person1:0x6000038342f0 retainCount:1
2020-03-26 15:56:26.667113+0800 taggedPointer[89806:4395707] person2:0x600003834300 retainCount:1

从上面的代码运行的结果可以看出使用copymutableCopy生成的person1和person2对象以及person对象,三者之间地址是不一样的,说明创建了新的对象。而且它们的引用计数都为1。copymutableCopy的区别在于,前者生成不可变更的对象,而后者生成可变更的对象。

需要说明的是alloc方法并没有对retainCount进行操作,这里的引用计数之所以为1,那是因为retainCount方法的底层是默认+1的。
inline uintptr_t 
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

3.2.1.1、浅拷贝和深拷贝

既然已经说到了copymutableCopy,那么就来说说浅拷贝和深拷贝。

浅拷贝:对象的指针拷贝,不会开辟新的内存。
深拷贝:拷贝对象本身,会创建一个新的对象,指向不同的内存地址。

对于不可变对象(如NSString、NSArray、NSDictionary)和可变对象(如NSMutableString、NSMutableArray、NSMutableDictionary)用copy和mutableCopy会有一些差别,大致如下表所示:

image
对于集合类的可变对象来说,深拷贝并非严格意义上的深复制,虽然新开辟了内存,但是对于存放在数组里面的元素来说仍然是浅拷贝。

3.2.2、非自己生成的对象,自己也能持有

alloc、new、copy、mutableCopy之外的方法获得的对象,因为并非自己生产持有,所以自己不是该对象的持有者。

//非自己生成的对象,暂时没有持有
id obj = [NSMutableArray array];

//通过retain持有对象
[obj retain];

上述代码中NSMutableArray通过类方法array生成了一个对象赋给变量obj,但变量obj自己并不持有该对象。使用retain方法可以持有对象。

3.2.3、不再需要自己持有的对象时释放

自己持有的对象,一旦该对象不再需要时,持有者有义务调用release方法释放该对象。当然在ARC环境下并不需要开发者主动调用方法,系统会自动调用该方法,但是在MRC环境下需要开发者手动在合适的地方做对象的retain 方法和release方法的调用。

3.2.4、非自己持有的对象无法释放

对于用alloc、new、copy、mutableCopy方法生成并持有的对象,或是用retain方法持有的对象,由于持有者是自己,所以在不需要该对象时需要将其释放。而由此以外所得到的对象绝对不能释放。倘若在程序中释放了非自己所持有的对象就会造成崩溃。

// 自己生成并持有对象
id obj = [[NSObject alloc] init];

//释放对象
[obj release];

//再次释放已经非自己持有的对象,应用程序崩溃
[obj release];

释放了非自己持有的对象,肯定会导致应用崩溃。因此绝对不要去释放非自己持有的对象。

3.3、alloc、retain、release、dealloc、autorelease实现

3.3.1、alloc实现

总结一句话就是alloc创建了对象并且申请了一块不少于16字节的内存空间。关于alloc的实现请移步之前的文章IOS底层原理之alloc、init和new,在这里就不多加描述。

3.3.2、retain实现

在前面的小节内容中,讲到在isabits中的extra_rc字段和SideTable结构中的RefcountMap refcnts都有存储引用计数,那么在这两者之间会有什么联系呢?下面通过retain的源码来分析引用计数的存储。

id objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}

首先是objc_retain方法,在该方法内部会现有一个判断当前对象是否是TaggedPointer,如果是则返回,否则调用retain方法。通过这里我们也可以看到 TaggedPointer对象并不做引用计数处理。

inline id objc_object::retain()
{
    assert(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        return rootRetain();
    }

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}

retain方法内部其实很简单,就是一个判断,然后调用rootRetain方法。其中fastpath是大概率发生的意思。

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    //如果是TaggedPointer 直接返回
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable =false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        // 如果isa未经过NONPOINTER_ISA优化
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();//引用计数存储于SideTable中
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        //检查对象是都正在析构
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        uintptr_t carry;
        //isa的bits中的extra_rc进行加1
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
        //如果bits的extra_rc已经存满了,则将其中的一半存储到sidetable中
        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;//extra_rc置空一半的数值
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        //将另外的一半引用计数存储到sidetable中
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}

rootRetain方法是retain引用计数的核心方法。我们可以看到方法做了如下几方面的工作:

  1. 判断当前对象是否一个TaggedPointer,如果是则返回。
  2. 判断isa是否经过NONPOINTER_ISA优化,如果未经过优化,则将引用计数存储在SideTable中。64位的设备不会进入到这个分支。
  3. 判断当前的设备是否正在析构。
  4. isabits中的extra_rc进行加1操作。
  5. 如果在extra_rc中已经存储满了,则调用sidetable_addExtraRC_nolock方法将一半的引用计数移存到SideTable中。

3.3.3、release实现

在上一章节中我们分析了引用计数的存储在bitsSideTable中的存储,那么作为释放对象的release又是怎么对引用计数进行减1操作的呢?

void 
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}

首先是objc_release方法,在该方法内部会现有一个判断当前对象是否是TaggedPointer,如果是则返回,否则调用release方法。

inline void
objc_object::release()
{
    assert(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        rootRelease();
        return;
    }

    ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}

release方法内部其实很简单,就是一个判断,然后调用rootRelease方法。其中fastpath是大概率发生的意思。

ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    //判断是否是TaggedPointer
    if (isTaggedPointer()) return false;

    bool sideTableLocked = false;

    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        //如果isa是未经过NONPOINTER_ISA优化,则对SideTable中的引用计数进行清理
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
        //isa的bits的extra_rc减1
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        //extra_rc已经置空
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));

    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;

 underflow:
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate

    // abandon newisa to undo the decrement
    newisa = oldisa;
    //isa的has_sidetable_rc表示是否有辅助的引用计数散列表
    if (slowpath(newisa.has_sidetable_rc)) {
        if (!handleUnderflow) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

        // Transfer retain count from side table to inline storage.

        if (!sideTableLocked) {
            ClearExclusive(&isa.bits);
            sidetable_lock();
            sideTableLocked = true;
            // Need to start over to avoid a race against 
            // the nonpointer -> raw pointer transition.
            goto retry;
        }

        // Try to remove some retain counts from the side table.
        //
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

        // To avoid races, has_sidetable_rc must remain set 
        // even if the side table count is now zero.

        if (borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            if (!stored) {
                // Inline update failed. 
                // Try it again right now. This prevents livelock on LL/SC 
                // architectures where the side table access itself may have 
                // dropped the reservation.
                isa_t oldisa2 = LoadExclusive(&isa.bits);
                isa_t newisa2 = oldisa2;
                if (newisa2.nonpointer) {
                    uintptr_t overflow;
                    newisa2.bits = 
                        addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                    if (!overflow) {
                        stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, 
                                                       newisa2.bits);
                    }
                }
            }

            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            // This decrement cannot be the deallocating decrement - the side 
            // table lock and has_sidetable_rc bit ensure that if everyone 
            // else tried to -release while we worked, the last one would block.
            sidetable_unlock();
            return false;
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

    // Really deallocate.

    if (slowpath(newisa.deallocating)) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
        // does not actually return
    }
    newisa.deallocating = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __sync_synchronize();
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return true;
}

rootRelease方法是release引用计数的核心方法。我们可以看到方法做了如下几方面的工作:

  1. 判断当前对象是否一个TaggedPointer,如果是则返回。
  2. 判断isa是否经过NONPOINTER_ISA优化,如果未经过优化,则清理在SideTable中的引用计数。64位的设备不会进入到这个分支。
  3. isabits中的extra_rc进行减1操作。
  4. 如果extra_rc已经置空,则清理SideTable中的引用计数。
  5. 尝试将SideTable中的引用计数移存到isabit中。

3.3.4、autorelease实现

说到Objective-C内存管理,就不能不提autorelease。 顾名思义,autorelease就是自动释放。这看上去很像ARC,但实际上它更类似于C语言中自动变量(局部变量)的特性。autorelease会像C语言的局部变量那样来对待对象实例。当其超出作用域时,对象实例的release实例方法被调用。另外,同C语言的局部变量不同的是,编程人员可以设置变量的作用域。
autorelease的具体使用方法如下:

  • 生成并持有NSAutoreleasePool对象。
  • 调用已分配对象的autorelease实例方法。
  • 废弃NSAutoreleasePool对象。
image

来看autorelease的代码实现。

id objc_autorelease(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->autorelease();
}

首先是objc_autorelease方法,在该方法内部会现有一个判断当前对象是否是TaggedPointer,如果是则返回,否则调用autorelease方法。

inline id 
objc_object::autorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (fastpath(!ISA()->hasCustomRR())) return rootAutorelease();

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_autorelease);
}

autorelease方法内部会再次判断当前对象是否是TaggedPointer,如果是则返回,否则调用rootAutorelease方法。其中fastpath是大概率发生的意思。

inline id objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}

id objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}

rootAutorelease的代码核心就是将当前对象添加到AutoreleasePool自动释放池中。

3.3.5、dealloc对象销毁

当对象的引用计数为0时,底层会调用_objc_rootDealloc方法对对象进行释放,而在_objc_rootDealloc方法里面会调用rootDealloc方法。如下是rootDealloc方法的代码实现。

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}
  1. 首先判断对象是否是Tagged Pointer,如果是则直接返回。
  2. 如果对象是采用了优化的isa计数方式,且同时满足对象没有被weak引用!isa.weakly_referenced、没有关联对象!isa.has_assoc、没有自定义的C++析构方法!isa.has_cxx_dtor、没有用到SideTable来引用计数!isa.has_sidetable_rc则直接快速释放。
  3. 如果不能满足2中的条件,则会调用object_dispose方法。

object_dispose方法很简单,主要是内部调用了objc_destructInstance方法。

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}

上面这一段代码很清晰,如果有自定义的C++析构方法,则调用C++析构函数。如果有关联对象,则移除关联对象并将其自身从AssociationManager的map中移除。调用clearDeallocating方法清除对象的相关引用。

inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

clearDeallocating中有两个分支,先是判断对象是否采用了优化isa引用计数,如果没有的话则需要清理对象存储在SideTable中的引用计数数据。如果对象采用了优化isa引用计数,则判断是都有使用SideTable的辅助引用计数(isa.has_sidetable_rc)或者有weak引用(isa.weakly_referenced),符合这两种情况中一种的,调用clearDeallocating_slow方法。

NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this]; // 在全局的SideTables中,以this指针为key,找到对应的SideTable
    table.lock();
    if (isa.weakly_referenced) { // 如果obj被弱引用
        weak_clear_no_lock(&table.weak_table, (id)this); // 在SideTable的weak_table中对this进行清理工作
    }
    if (isa.has_sidetable_rc) { // 如果采用了SideTable做引用计数
        table.refcnts.erase(this); // 在SideTable的引用计数中移除this
    }
    table.unlock();
}

clearDeallocating_slow方法中有两个分支,一是如果对象被弱引用,则调用weak_clear_no_lock方法在SideTableweak_table中对this进行清理工作。二是如果采用了SideTable做引用计数,则在 SideTable的引用计数中移除this。

3.4、ARC下的规则

  1. 不能显式的调用retain、release、retainCount、autorelease。
  2. 不能使用NSAllocateObject和NSDeallocateObject。
  3. 必须遵守内存管理的命名规则。
  4. 不要显式的调用dealloc。
  5. 使用@autoreleasepool代替NSAutoreleasePool。
  6. 不能使用区域NSZone。
  7. 对象变量不能作为C语言结构体的成员。
  8. 显式转换"id"和"void *"。

4、总结

  1. IOS中内存布局区域大概分为五个区域:栈区、堆区、BSS段、数据段、代码段
  2. OC中对内存优化管理的方案有如下几种形式:Tagged Ponter、NONPOINTER_ISA 、SideTable
  3. Tagged Pointer是专⻔⽤来存储⼩的对象,例如NSNumber,NSDate等。其指针的值不再是地址,而是真正的值。所以,它的内存并不存储在堆中,也不需要mallocfree。在内存读取上有着3倍的效率,创建时⽐以前快106倍。
  4. NONPOINTER_ISA就是用一部分额外空间存储其他内容,这样提高了内存的利用率。
  5. SideTable是一个hash表结构,主要是针对引用计数和弱引用表进行相关操作。
  6. 对象的持有规则:自己生成的对象,自己持有;非自己生成的对象,自己也能持有;不再需要自己持有的对象时释放;非自己持有的对象无法释放
  7. alloc/new/copy/mutableCopy等方法生成并持有对象,retain方法引用计数加1,release方法引用计数减1,dealloc方法销毁对象。
  8. autorelease方法不改变该对象的引用计数器的值,只是将对象添加到自动释放池中。
  9. ARC下不能显式的调用retain、release、retainCount、autorelease

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,360评论 5 461
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,195评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,543评论 0 323
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,420评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,242评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,224评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,626评论 3 384
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,301评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,589评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,660评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,423评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,286评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,662评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,951评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,237评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,613评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,804评论 2 335

推荐阅读更多精彩内容