原文链接OC内存管理--引用计数器
引用计数的存储策略
- 有些对象如果支持使用
TaggedPointer
,苹果会直接将其指针值作为引用计数返回; - 如果当前设备是
64
位环境并且使用Objective-C 2.0
,那么“一些”对象会使用其isa
指针的一部分空间来存储它的引用计数; - 否则
Runtime
会使用一张散列表来管理引用计数。
Tagged Pointer
Tagged Pointer
用来优化内存,其特点:
-
Tagged Pointer
专门用来存储小的对象,例如NSNumber
和NSDate
等; -
Tagged Pointer
指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc
和free
; - 在内存读取上有着3倍的效率,创建时比以前快106倍。
下面这个实现用来反映在64位系统下Tagged Pointer
的应用:
int main(int argc, char * argv[]) {
@autoreleasepool {
NSNumber *number1 = @1;
NSNumber *number2 = @2;
NSNumber *number3 = @3;
NSNumber *number4 = @4;
NSNumber *numberLager = @(MAXFLOAT);
NSLog(@"number1 pointer is %p", number1);
NSLog(@"number2 pointer is %p", number2);
NSLog(@"number3 pointer is %p", number3);
NSLog(@"number4 pointer is %p", number4);
NSLog(@"numberLager pointer is %p", numberLager);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
//打印结果:
2018-09-25 15:26:05.788382+0800 NSObjectProject[68029:24580896] number1 pointer is 0x9c344c19d780bc93
2018-09-25 15:26:05.789257+0800 NSObjectProject[68029:24580896] number2 pointer is 0x9c344c19d780bca3
2018-09-25 15:26:05.789383+0800 NSObjectProject[68029:24580896] number3 pointer is 0x9c344c19d780bcb3
2018-09-25 15:26:05.789489+0800 NSObjectProject[68029:24580896] number4 pointer is 0x9c344c19d780bcc3
2018-09-25 15:26:05.789579+0800 NSObjectProject[68029:24580896] numberLager pointer is 0x600001e60d80
我们知道,所有对象都有其对应的isa
指针,那么引入Tagged Pointer
会对isa
指针产生影响。
我们看下对象中的Tagged Pointer
的使用
inline bool
objc_object::isTaggedPointer() {
return _objc_isTaggedPointer(this);
}
那么如何判断是否是Tagged Pointer
的对象:
- 看对象。前面说到
Tagged Pointer
专门用来存储小的对象,这些对象有NSDate
、NSNumber
、NSString
; - 自己设置。在环境变量中设置
OBJC_DISABLE_TAGGED_POINTERS
为YES
表示强制不启用Tagged Pointer
。
isa指针
isa的本质——isa_t联合体
在objc_object
这个结构体中定义了isa
指针:
struct objc_object {
isa_t isa;
}
//isa_t的定义
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__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
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;
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
};
# else
# error unknown architecture for packed isa
# endif
// SUPPORT_PACKED_ISA
#endif
#if SUPPORT_INDEXED_ISA
# if __ARM_ARCH_7K__ >= 2
# define ISA_INDEX_IS_NPI 1
# define ISA_INDEX_MASK 0x0001FFFC
# define ISA_INDEX_SHIFT 2
# define ISA_INDEX_BITS 15
# define ISA_INDEX_COUNT (1 << ISA_INDEX_BITS)
# define ISA_INDEX_MAGIC_MASK 0x001E0001
# define ISA_INDEX_MAGIC_VALUE 0x001C0001
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t indexcls : 15;
uintptr_t magic : 4;
uintptr_t has_cxx_dtor : 1;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 7;
# define RC_ONE (1ULL<<25)
# define RC_HALF (1ULL<<6)
};
# else
# error unknown architecture for indexed isa
# endif
// SUPPORT_INDEXED_ISA
#endif
};
这里定义了很多环境,我们主要看64位CPU(if __arm64__
)的定义:
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
nonpointer
该变量占用1bit
内存空间,可以有两个值:0
和1
,分别代表不同的isa_t
的类型:
-
0
表示isa_t
没有开启指针优化,不使用isa_t
中定义的结构体。访问objc_object
的isa
会直接返回isa_t
结构中的cls
变量,cls
变量会指向对象所属的类的结构; -
1
表示isa_t
开启指针优化,不能直接访问objc_object
的isa
成员变量 (此时的isa而是一个Tagged Pointer
),isa
中包含了类信息、对象的引用计数等信息。
has_assoc
该变量与对象的关联引用有关。
has_cxx_dtor
表示该对象是否有析构函数,如果有析构函数,则需要做析构逻辑;如果没有,则可以更快的释放对象。
shiftcls
在开启指针优化的情况下,用33bits存储类指针的值。在initIsa()
中有newisa.shiftcls = (uintptr_t)cls >> 3;
这样的代码,就是将类指针存在isa中。
magic
用于调试器判断当前对象是真的对象还是没有初始化的空间
weakly_referenced
标志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。
deallocating
标志对象是否正在释放内存。
extra_rc
extra_rc
占了19位,可以存储的最大引用计数应该是(为什么要这么写是因为extra_rc
保存的是值-1,而在获取引用计数的时候会+1),当超过它就需要SideTables
。SideTables
内包含一个RefcountMap
,用来保存引用计数,根据对象地址取出其引用计数,类型是size_t
。
这里有个问题,为什么既要使用一个extra_rc
又要使用SideTables
?
可能是因为历史问题,以前cpu是32
位的,isa
中能存储的引用计数就只有。因此在arm64
下,引用计数通常是存储在isa
中的。
更具体的会在retain操作的时候讲到。
has_sidetable_rc
当引用计数器过大的时候,那么引用计数会存储在一个叫SideTable
的类的属性中。
ISA_MAGIC_MASK
通过掩码方式获取magic
值。
ISA_MASK
通过掩码方式获取isa
的类指针值。
RC_ONE 和 RC_HALF
用于引用计数的相关计算。
isa_t联合体里面的宏
SUPPORT_PACKED_ISA
表示平台是否支持在isa
指针中插入除Class
之外的信息。
- 如果支持就会将
Class
信息放入isa_t
定义的struct内,并附上一些其他信息,例如上面的nonpointer
等等; - 如果不支持,那么不会使用
isa_t
内定义的struct
,这时isa_t
只使用cls
(Class 指针)。
在iOS以及MacOSX设备上,SUPPORT_PACKED_ISA
定义为1。
SUPPORT_INDEXED_ISA
SUPPORT_INDEXED_ISA
表示isa_t
中存放的Class
信息是Class
的地址。在initIsa()
中有:
#if SUPPORT_INDEXED_ISA
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
iOS设备上SUPPRT_INDEXED_ISA是0。
isa类型有关的宏
SUPPORT_NONPOINTER_ISA
用于标记是否支持优化的isa
指针,其定义:
#if !SUPPORT_INDEXED_ISA && !SUPPORT_PACKED_ISA
# define SUPPORT_NONPOINTER_ISA 0
#else
# define SUPPORT_NONPOINTER_ISA 1
#endif
那如何判断是否支持优化的isa指针?
- 已知iOS系统的
SUPPORT_PACKED_ISA
为1,SUPPORT_INDEXED_ISA
为0,从上面的定义可以看出,iOS系统的SUPPORT_NONPOINTER_ISA
为1; - 在环境变量中设置
OBJC_DISABLE_NONPOINTER_ISA
。
这里需要注意的是,即使是64位环境下,优化的isa
指针并不是就一定会存储引用计数,毕竟用19bit iOS 系统)保存引用计数不一定够。另外这19位保存的是引用计数的值减一。
SideTable
在源码中我们经常会看到SideTable
这个结构体。它的定义:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
//省略其他代码
};
从上面可知,SideTable
中有三个成员变量:
-
slock
用于保证原子操作的自旋锁; -
refcnts
用于引用计数的hash
表; -
weak_table
用于weak引用的hash
表。
这里我们主要看引用计数的哈希表。RefcountMap
的定义:typedef objc::DenseMap
可以看出SideTable
用来保存引用计数具体是用DenseMap
这个类(在llvm-DenseMap.h
中)实现的。DenseMap
以DisguisedPtr
为key
,size_t
为value
,DisguisedPtr
类是对objc_object *
指针及其一些操作进行的封装,其内容可以理解为对象的内存地址,值的类型为__darwin_size_t
,在 darwin 内核一般等同于 unsigned long
。其实这里保存的值也是等于引用计数减1。
引用计数的获取
通过retainCount
可以获取到引用计数器,其定义:
- (NSUInteger)retainCount {
return ((id)self)->rootRetainCount();
}
inline uintptr_t
objc_object::rootRetainCount() {
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
//加锁,用汇编指令ldxr来保证原子性
isa_t bits = LoadExclusive(&isa.bits);
//释放锁,使用汇编指令clrex
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();
}
//sidetable_retainCount()函数实现
uintptr_t
objc_object::sidetable_retainCount() {
SideTable& table = SideTables()[this];
size_t refcnt_result = 1;
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
// this is valid for SIDE_TABLE_RC_PINNED too
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
table.unlock();
return refcnt_result;
}
从上面的代码可知,获取引用计数的时候分为三种情况:
-
Tagged Pointer
的话,直接返回isa本身; - 非
Tagged Pointer
,且开启了指针优化,此时引用计数先从extra_rc
中去取(这里将取出来的值进行了+1操作,所以在存的时候需要进行-1操作),接着判断是否有SideTable
,如果有再加上存在SideTable
中的计数; - 非
Tagged Pointer
,没有开启了指针优化,使用sidetable_retainCount()
函数返回。
手动操作对引用计数的影响
objc_retain()
#if __OBJC2__
__attribute__((aligned(16)))
id
objc_retain(id obj) {
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
#else
id objc_retain(id obj) { return [obj retain]; }
首先判断是否是Tagged Pointer
的对象,是就返回对象本身,否则通过对象的retain()
返回。
inline id
objc_object::retain() {
assert(!isTaggedPointer());
// hasCustomRR方法检查类(包括其父类)中是否含有默认的方法
if (fastpath(!ISA()->hasCustomRR())) {
return rootRetain();
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
首先判断是否是Tagged Pointer
,这个函数并不希望处理的对象是Tagged Pointer
;接着通过hasCustomRR
函数检查类(包括其父类)中是否含有默认的方法,有则调用自定义的方法;如果没有,调用rootRetain()
函数。
ALWAYS_INLINE id
objc_object::rootRetain() {
return rootRetain(false, false);
}
//将源码精简后的逻辑
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;
isa_t oldisa;
isa_t newisa;
// 加锁,用汇编指令ldxr来保证原子性
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (newisa.nonpointer = 0) {
// newisa.nonpointer = 0说明所有位数都是地址值
// 释放锁,使用汇编指令clrex
ClearExclusive(&isa.bits);
// 由于所有位数都是地址值,直接使用SideTable来存储引用计数
return sidetable_retain();
}
// 存储extra_rc++后的结果
uintptr_t carry;
// extra_rc++
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
if (carry == 0) {
// extra_rc++后溢出,进位到side table
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
sidetable_addExtraRC_nolock(RC_HALF);
}
// 将newisa写入isa
StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)
return (id)this;
}
从上面的可以看到:
-
Tagged Pointer
直接返回对象本身; -
newisa.nonpointer == 0
没有开启指针优化,直接使用SideTable
来存储引用计数; - 开启指针优化,使用isa的
extra_rc
保存引用计数,当超出的时候,使用SideTable
来存储额外的引用计数。
objc_release()
#if __OBJC2__
__attribute__((aligned(16)))
void
objc_release(id obj) {
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}
#else
void objc_release(id obj) { [obj release]; }
#endif
//release()源码
inline void
objc_object::release()
{
assert(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
rootRelease();
return;
}
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}
这边的逻辑和objc_retain()
的逻辑一致,所以直接看rootRelease()
函数,与上面一样,下面的代码也是经过精简的。
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow) {
if (isTaggedPointer()) return false;
isa_t oldisa;
isa_t newisa;
retry:
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (newisa.nonpointer == 0) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return sidetable_release(performDealloc);
}
uintptr_t carry;
// extra_rc--
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
if (carry == 0) {
// 需要从SideTable借位,或者引用计数为0
goto underflow;
}
// 存储引用计数到isa
StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits)
return false;
underflow:
// 从SideTable借位
// 或引用计数为0,调用delloc
// 此处省略N多代码
// 总结一下:修改Side Table与extra_rc,
// 引用计数减为0时,调用dealloc
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return true;
}
从上面可以看到:
- 判断是否是
Tagged Pointer
的对象,是就直接返回; - 没有开启指针优化,使用
SideTable
存储的引用计数-1; - 开启指针优化,使用isa的
extra_rc
保存的引用计数-1,当carry==0
表示需要从SideTable
保存的引用计数也用完了或者说引用计数为0,所以执行最后一步; - 最后调用
dealloc
,所以这也回答了之前的《OC内存管理--对象的生成与销毁》中dealloc
什么时候被调用这个问题,在rootRelease(bool performDealloc, bool handleUnderflow)
函数中如果判断出引用计数为0了,就要调用dealloc
函数了。
总结
-
引用计数存在什么地方?
-
Tagged Pointer
不需要引用计数,苹果会直接将对象的指针值作为引用计数返回; - 开启了指针优化(
nonpointer == 1
)的对象其引用计数优先存在isa
的extra_rc
中,大于524288
便存在SideTable
的RefcountMap
或者说是DenseMap
中; - 没有开启指针优化的对象直接存在
SideTable
的RefcountMap
或者说是DenseMap
中。
-
-
retain/release的实质
-
Tagged Pointer
不参与retain
/release
; - 找到引用计数存储区域,然后+1/-1,并根据是否开启指针优化,处理进位/借位的情况;
- 当引用计数减为0时,调用
dealloc
函数。
-
-
isa是什么
// ISA() assumes this is NOT a tagged pointer object Class ISA(); // getIsa() allows this to be a tagged pointer object Class getIsa();
- 首先要知道,isa指针已经不一定是类指针了,所以需要用
ISA()
获取类指针; -
Tagged Pointer
的对象没有isa
指针,有的是isa_t
的结构体; - 其他对象的isa指针还是类指针。
- 首先要知道,isa指针已经不一定是类指针了,所以需要用
-
对象的值是什么
- 如果是
Tagged Pointer
,对象的值就是指针; - 如果非
Tagged Pointer
, 对象的值是指针指向的内存区域中的值。
- 如果是
补充: 一道多线程安全的题目
以下代码运行结果
@property (nonatomic, strong) NSString *target;
//....
dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000 ; i++) {
dispatch_async(queue, ^{
self.target = [NSString stringWithFormat:@"ksddkjalkjd%d",i];
});
}
答案:Crash。
Crash的原因:过度释放。
关键知识点:
- 全局队列和自定义并行队列在异步执行的时候会根据任务系统决定开辟线程个数;
-
target
使用strong
进行了修饰,Block是会截获对象的修饰符的; - 即使使用
_target
效果也是一样,因为默认使用strong
修饰符隐式修饰; -
strong
的源代码如下:
objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
假设这个并发队列创建了两个线程A和B,由于是异步的,可以同时执行。因此会出现这么一个场景,在线程A中,代码执行到了objc_retain(obj)
,但是在线程B中可能执行到了objc_release(prev)
,此时prev
已经被释放了。那么当A在执行到objc_release(prev)
就会过度释放,从而导致程序crash。
解决方法:
- 加个互斥锁
- 使用串行队列,使用串行队列的话,其实内部是靠
DISPATCH_OBJ_BARRIER_BIT
设置阻塞标志位 - 使用weak
- 使用Tagged Pointer