iOS - 老生常谈内存管理(一):引用计数

简单聊聊 GC 与 RC

随着各个平台的发展,如今被普遍采用的内存管理机制主要有 GC 和 RC 两种。html

  • GC (Garbage Collection):垃圾回收机制,按期查找再也不使用的对象,释放对象占用的内存。
  • RC (Reference Counting):引用计数机制。采用引用计数来管理对象的内存,当须要持有一个对象时,使它的引用计数 +1;当不须要持有一个对象的时候,使它的引用计数 -1;当一个对象的引用计数为 0,该对象就会被销毁。

Objective-C支持三种内存管理机制:ARCMRCGC,但Objective-CGC机制有平台局限性,仅限于MacOS开发中,iOS开发用的是RC机制,从MRC到如今的ARC编程

备注: 苹果在引入ARC的时候称将在MacOS中弃用GC机制。

OS X Mountain Lion v10.8 中不推荐使用GC机制,而且将在 OS X 的将来版本中删除GC机制。ARC是推荐的替代技术。为了帮助现有应用程序迁移,Xcode 4.3 及更高版本中的ARC迁移工具支持将使用GC的 OS X 应用程序迁移到ARC

注意:对于面向 Mac App Store 的应用,Apple 强烈建议你尽快使用ARC替换GC,由于 Mac App Store Guidelines 禁止使用已弃用的技术,不然不会经过审核,详情请参阅 Mac App Store Review Guidelines安全

Reference Counting

做为一名 iOS 开发者,引用计数机制是咱们必须掌握的知识。那么,引用计数机制下是怎样工做的呢?它存在什么优点?数据结构

办公室里的照明问题

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

假设办公室里的照明设备只有一个。上班进入办公室的人须要照明,因此要把灯打开。而对于下班离开办公室的人来讲,已经不须要照明了,因此要把灯关掉。架构

如果不少人上下班,每一个人都开灯或者关灯,那么办公室的状况又将如何呢?最先下班的人若是关了灯,那就会像下图那样,办公室里还没走的全部人都将处于一片黑暗之中。并发

解决这一问题的办法就是使办公室在还有至少一人的状况下保持开灯状态,而在无人时保持关灯状态。app

(1)最先进入办公室的人开灯。
(2)以后进入办公室的人,须要照明。
(3)下班离开办公室的人,不须要照明。
(4)最后离开办公室的人关灯(此时已无人须要照明)。ide

为判断是否还有人在办公室里,这里导入计数功能来计算 “须要照明的人数”。下面让咱们来看看这一功能是如何运做的吧。函数

(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 的内存管理。以下图所示:

引用计数的存储

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

首先,不得不提一下isa

isa

  • isa指针用来维护 “对象” 和 “类” 之间的关系,并确保对象和类可以经过isa指针找到对应的方法、实例变量、属性、协议等;
  • 在 arm64 架构以前,isa就是一个普通的指针,直接指向objc_class,存储着ClassMeta-Class对象的内存地址。instance对象的isa指向class对象,class对象的isa指向meta-class对象;
  • 从 arm64 架构开始,对isa进行了优化,用nonpointer表示,变成了一个共用体(union)结构,还使用位域来存储更多的信息。将 64 位的内存数据分开来存储着不少的东西,其中的 33 位才是拿来存储classmeta-class对象的内存地址信息。要经过位运算将isa的值& ISA_MASK掩码,才能获得classmeta-class对象的内存地址。
// objc.h
struct objc_object {
    Class isa;  // 在 arm64 架构以前
};

// 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__ 架构下 }; 复制代码

若是isanonpointer,即 arm64 架构以前的isa指针。因为它只是一个普通的指针,存储着ClassMeta-Class对象的内存地址,因此它自己不能存储引用计数,因此之前对象的引用计数都存储在一个叫SideTable结构体的RefCountMap(引用计数表)散列表中。

若是isanonpointer,则它自己能够存储一些引用计数。从以上union isa_t的定义中咱们能够得知,isa_t中存储了两个引用计数相关的东西:extra_rchas_sidetable_rc

  • extra_rc:里面存储的值是对象自己以外的引用计数的数量,这 19 位若是不够存储,has_sidetable_rc的值就会变为 1;
  • has_sidetable_rc:若是为 1,表明引用计数过大没法存储在isa中,那么超出的引用计数会存储SideTableRefCountMap中。

因此,若是isanonpointer,则对象的引用计数存储在它的isa_textra_rc中以及SideTableRefCountMap中。

备注

  • 以上isa_t结构来自老版本的objc4源码,从objc4-750版本开始,isa_t中的struct的内容定义成了宏并写在isa.h文件里,不过其数据结构不变,这里不影响。
  • 更多关于isa的知识,以及以上提到的一些细节,能够查看《深刻浅出 Runtime(二):数据结构》

SideTable

以上提到了一个数据结构SideTable,咱们进入objc4源码查看它的定义。

// NSObject.mm
struct SideTable {
    spinlock_t slock;        // 自旋锁
    RefcountMap refcnts;     // 引用计数表(散列表)
    weak_table_t weak_table; // 弱引用表(散列表)
    ......
}
复制代码

SideTable存储在SideTables()中,SideTables()本质也是一个散列表,能够经过对象指针来获取它对应的(引用计数表或者弱引用表)在哪个SideTable中。在非嵌入式系统下,SideTables()中有 64 个SideTable。如下是SideTables()的定义:

// NSObject.mm
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;

static StripedMap<SideTable>& SideTables() {
    return SideTablesMap.get();
}
复制代码

因此,查找对象的引用计数表须要通过两次哈希查找:

  • ① 第一次根据当前对象的内存地址,通过哈希查找从SideTables()中取出它所在的SideTable
  • ② 第二次根据当前对象的内存地址,通过哈希查找从SideTable中的refcnts中取出它的引用计数表。

Q:为何不是一个SideTable,而是使用多个SideTable组成SideTables()结构?

若是只有一个SideTable,那咱们在内存中分配的全部对象的引用计数或者弱引用都放在这个SideTable中,那咱们对对象的引用计数进行操做时,为了多线程安全就要加锁,就存在效率问题。
系统为了解决这个问题,就引入 “分离锁” 技术方案,提升访问效率。把对象的引用计数表分拆多个部分,对每一个部分分别加锁,那么当所属不一样部分的对象进行引用操做的时候,在多线程下就能够并发操做。因此,使用多个SideTable组成SideTables()结构。

备注: 关于引用计数具体是怎么管理的,请参阅《iOS - 老生常谈内存管理(四):源码分析内存管理方法》

相关文章
相关标签/搜索