随着各个平台的发展,如今被普遍采用的内存管理机制主要有 GC 和 RC 两种。html
Objective-C
支持三种内存管理机制:ARC
、MRC
和GC
,但Objective-C
的GC
机制有平台局限性,仅限于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。安全
做为一名 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
就是一个普通的指针,直接指向objc_class
,存储着Class
、Meta-Class
对象的内存地址。instance
对象的isa
指向class
对象,class
对象的isa
指向meta-class
对象;isa
进行了优化,用nonpointer
表示,变成了一个共用体(union
)结构,还使用位域来存储更多的信息。将 64 位的内存数据分开来存储着不少的东西,其中的 33 位才是拿来存储class
、meta-class
对象的内存地址信息。要经过位运算将isa
的值& ISA_MASK
掩码,才能获得class
、meta-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__ 架构下 }; 复制代码
若是isa
非nonpointer
,即 arm64 架构以前的isa
指针。因为它只是一个普通的指针,存储着Class
、Meta-Class
对象的内存地址,因此它自己不能存储引用计数,因此之前对象的引用计数都存储在一个叫SideTable
结构体的RefCountMap
(引用计数表)散列表中。
若是isa
是nonpointer
,则它自己能够存储一些引用计数。从以上union isa_t
的定义中咱们能够得知,isa_t
中存储了两个引用计数相关的东西:extra_rc
和has_sidetable_rc
。
has_sidetable_rc
的值就会变为 1;isa
中,那么超出的引用计数会存储SideTable
的RefCountMap
中。因此,若是isa
是nonpointer
,则对象的引用计数存储在它的isa_t
的extra_rc
中以及SideTable
的RefCountMap
中。
备注:
- 以上
isa_t
结构来自老版本的objc4
源码,从objc4-750
版本开始,isa_t
中的struct
的内容定义成了宏并写在isa.h
文件里,不过其数据结构不变,这里不影响。- 更多关于
isa
的知识,以及以上提到的一些细节,能够查看《深刻浅出 Runtime(二):数据结构》。
以上提到了一个数据结构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 - 老生常谈内存管理(四):源码分析内存管理方法》
。