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

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

一、内存布局

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

内存布局

  • 栈区:编译器自动分配,由系统管理,在不须要的时候自动清除。局部变量、函数参数存储在这里。栈区的内存地址通常是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的操做,其值并不相互影响。c#

二、内存管理方案

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

2.一、Tagged Ponter

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

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

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

2.1.一、Tagged Ponter内存优化

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

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

image.png

2.1.二、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.二、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。

其结构以下图所示:

2.三、SideTable

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

来看看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.一、什么是引用计数

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

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

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

3.二、对象持有规则

对象的持有规则以下:

  1. 本身生成的对象,本身持有。
  2. 非本身生成的对象,本身也能持有。
  3. 再也不须要本身持有的对象时释放。
  4. 非本身持有的对象没法释放。

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

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

3.2.一、本身生成的对象,本身持有

使用如下名称开头的方法名意味着本身生成的对象只有本身持有: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.一、浅拷贝和深拷贝

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

浅拷贝:对象的指针拷贝,不会开辟新的内存。
深拷贝:拷贝对象自己,会建立一个新的对象,指向不一样的内存地址。

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

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

3.2.二、非本身生成的对象,本身也能持有

alloc、new、copy、mutableCopy以外的方法得到的对象,由于并不是本身生产持有,因此本身不是该对象的持有者。

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

//经过retain持有对象
[obj retain];
复制代码

上述代码中NSMutableArray经过类方法array生成了一个对象赋给变量obj,但变量obj本身并不持有该对象。使用retain方法能够持有对象。

3.2.三、再也不须要本身持有的对象时释放

本身持有的对象,一旦该对象再也不须要时,持有者有义务调用release方法释放该对象。固然在ARC环境下并不须要开发者主动调用方法,系统会自动调用该方法,可是在MRC环境下须要开发者手动在合适的地方作对象的retain 方法和release方法的调用。

3.2.四、非本身持有的对象没法释放

对于用alloc、new、copy、mutableCopy方法生成并持有的对象,或是用retain方法持有的对象,因为持有者是本身,因此在不须要该对象时须要将其释放。而由此之外所获得的对象绝对不能释放。假若在程序中释放了非本身所持有的对象就会形成崩溃。

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

//释放对象
[obj release];

//再次释放已经非本身持有的对象,应用程序崩溃
[obj release];
复制代码

释放了非本身持有的对象,确定会致使应用崩溃。所以绝对不要去释放非本身持有的对象。

3.三、alloc、retain、release、dealloc、autorelease实现

3.3.一、alloc实现

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

3.3.二、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.三、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.四、autorelease实现

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

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

来看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.五、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.四、ARC下的规则

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

四、总结

  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

参考资料

相关文章
相关标签/搜索