前言
-
由于ARC的出现,内存管理在我们开发中其实经常被熟练的开发者忽视,但是经过对底层的深入研究,我们会发现苹果对内存的管理的处理会让我们学习到很多东西,其次移动设备的内存资源是有限的,当 App 运行时占用的内存大小超过了限制后,就会被强杀掉,从而导致用户体验被降低。所以,为了提升 App 质量,开发者要非常重视应用的内存管理问题。
-
这篇开始由浅入深分析总结整理内存管理的内容
内容
一、内存布局
具体参考: iOS 底层探索:内存五大区
针对4GB虚拟内存二、内存管理方案
不同场景下的内存方案有以下三种:
TaggedPointer
:⼩对象-NSNumber,NSDateNONPOINTER_ISA
:⾮指针型isa散列表
:引⽤计数表,弱引⽤表
1.TaggedPointer
先分析一个面试题
//MARK: - taggedPointer 面试题
- (void)taggedPointerDemo {
self.queue = dispatch_queue_create("com.HJ.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"HJ"]; // alloc 堆 iOS优化 - taggedpointer
NSLog(@"%@ --- %d",self.nameStr , i);
});
}
}
//------完整的打印. 运行不报错
2020-11-24 14:22:04.326685+0800 002---taggedPointer[28847:474034] HJ --- 9995
2020-11-24 14:22:04.327784+0800 002---taggedPointer[28847:474074] HJ --- 9996
2020-11-24 14:22:04.332117+0800 002---taggedPointer[28847:474077] HJ --- 9997
2020-11-24 14:22:04.333909+0800 002---taggedPointer[28847:474068] HJ --- 9998
2020-11-24 14:22:04.334401+0800 002---taggedPointer[28847:474031] HJ --- 9999
//------打印. 运行报错
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"HJ_内存管理的探索"];
NSLog(@"%@",self.nameStr);
});
}
运行报错如下图
我们知道这段就崩溃了,在多线程的时候讨论过,这段崩溃是由于多线程同时setter /getter 导致的过度释放。
我们可以看到两段代码唯有唯有字符串长短不一样。为什么会这样呢?我们通过断点调试分析如下:
不崩溃 崩溃我们可以看到 一个是NSTaggedPointerString
,一个是__NSCFString
,这里就可以看出@"HJ" 是经过iOS优化过的,它变成了一个字符串常量NSTaggedPointerString
打印其地址如下:很明显他们存储在不同的区域。
NSString * str1 = [NSString stringWithFormat:@"HJ"];
NSString * str2 = [NSString stringWithFormat:@"HJ_内存管理的探索"];
NSLog(@"%p-%@",str1,str1);
NSLog(@"%p-%@",str2,str2);
//真机下运行的
// 0x8a9c70c41689beb6 - HJ
// 0x000000028234fcf0 - HJ_内存管理的探索
我们进入源码查看retain和release方法如下:
id
objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}
- 分析:
很明显我们可以看到:if (obj->isTaggedPointer()) return; 如果obj->isTaggedPointer()就直接return,再次说明了苹果利用了TaggedPointer做了相应内存的处理。所以在上面的面试题中我们就能很好的解释,为什么第一个不会崩溃,因为它在多线程时,没有不断的进行retain和release的本质操作,所以不存在过度释放的可能,则第二个相反。
那么TaggedPointer 到底是什么呢???
还记得之前分析【类的加载下】read_image的时候如下:
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
....省略...
initializeTaggedPointerObfuscator(); //初始化 TaggedPointer 模糊处理
....省略...
}
static void
initializeTaggedPointerObfuscator(void)
{
if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
// Set the obfuscator to zero for apps linked against older SDKs,
// in case they're relying on the tagged pointer representation.
DisableTaggedPointerObfuscation) {
objc_debug_taggedpointer_obfuscator = 0;
} else {
// Pull random data into the variable, then shift away all non-payload bits.
// 这里 这里
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}
可以看出在SDK10_14之后,经过了一次模糊处理:
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
其中#define _OBJC_TAG_MASK 1UL
为什么要做一次这个处理呢?
这里要说明一点小对象的引用,在iOS中一个对象8个字节,占8位就是64个字节,然后一个@"HJ" 或者@"1",这种需要占用这么多字节吗?很明显造成了浪费,所以就引用了
小对象
这个概念。在苹果推出了 采用64位架构的A7双核处理器 iphone 5s的时候,为了节省内存
和提高执行效率
,苹果提出了Tagged Pointer
的概念。
进入objc_debug_taggedpointer_obfuscator
我们可以看到进行了两次异或处理
,类似于编码与解码的过程。
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;
}
我们移植代码在模拟器上运行如下:
{
NSString *str2 = [NSString stringWithFormat:@"b"];
NSLog(@"%p-%@",str2,str2);
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str2));
}
uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
0xbdc43db247c3b1b0 - b
0xa000000000000621
很明显 0xa000000000000621 这是一个非常简单的指针 ,分析如下:
- 在苹果的64位OC实现中,若对象指针的二进制第一位是1,则该指针为
Tagged Pointer。
例如0xa000000000000621其中a的2进制为1010,第一位1
表示这是Tagged Pointer
,010
就是2表示这是一个NSString
类;#if __has_feature(objc_fixed_enum) || __cplusplus >= 201103L enum objc_tag_index_t : uint16_t > #else typedef uint16_t objc_tag_index_t; enum #endif { // 这里就列举了小对象 有空可以探索试试喔 // 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, .....
- 这个地址最后一位
1
表示字符串的数目
,这里是0001表示有1位字符串;其中真正用来存储的位数只有中间的14位16进制。b
在ASCII码
中查询对应的就是62
- 说明 :这个
地址本身
其实就存储了字符串的值
,可以说是存储在&strS内存中值
,只是伪装成了地址
,它不需要存储在数据区
,也不需要申请堆空间
关于更深的字符串研究可以参考:采用Tagged Pointer的字符串
TaggedPointer
总结:
-
TaggedPointer
专⻔⽤来存储⼩对象
,例如NSNumber
和NSDate
-
-
TaggedPointer指针
的值
不再是地址了,⽽是真正的值
。所以,实际上它不再是⼀个对象了,它只是⼀个披着对象⽪的普通变量
⽽已。所以,它的内存并不存储在堆
中,也不
需要malloc
和free
-
- 在内存读取上有着
3倍的效率
,创建时⽐以前快106倍
。
- 在内存读取上有着
2. NONPOINTER_ISA 与 散列表
分析了iOS 底层探索:isa与类关联的原理,我们得出结论:在isa
初始化obj->initInstanceIsa(cls, hasCxxDtor)
的时候,通过isa_t联合体
,在位域运算
中,将类信息cls
存进了存储类的指针的值shiftclx
, 最后isa = newisa
;isa
中既有类的指针,又有类的信息,这个过程也叫做指针优化
:NONPOINTER_ISA
,所以isa
其实不仅仅是一个指针,其中一些位仍然存储了每个对象的信息
,例如:引用计数
和是否被弱引用
。仔细一想,NONPOINTER_ISA
的设计思想是不是和TaggetPointer
类似呢!
再次查看isa的本质,再次看看哪里存了引用计数等信息呢?
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
找到ISA_BITFIELD
的结构体信息如下
重点
-
nonpointer
:表示是否对isa开启指针优化 。0代表是纯isa指针,1代表除了地址外,还包含了类的一些信息、对象的引用计数
等。 -
extra_rc
:表示该对象的引用计数值,实际上是引用计数减一。例如:如果引用计数为10,那么extra_rc为9。如果引用计数大于10,则需要使用has_sidetable_rc
-
has_sidetable_rc
:当对象引用计数大于10时,则需要进位
其中has_sidetable_rc
就引入了散列表
的概念:
在runtime内存空间中,
SideTables
是一个hash数组,里面存储了SideTable
。SideTables
的hash键值就是一个对象obj的address。 因此可以说,一个obj,对应了一个SideTable
。但是一个SideTable
,会对应多个obj。因为SideTable
的数量有限,所以会有很多obj共用同一个SideTable
。
我们先看看SideTable
的结构
struct SideTable {
spinlock_t slock; // 自旋锁,用于上锁/解锁 SideTable。
RefcountMap refcnts; //用来存储OC对象的引用计数的 hash表 (仅在未开启isa优化或在isa优化情况下isa_t的引用计数溢出时才会用到)。
weak_table_t weak_table; //弱引用表 存储对象弱引用指针的 hash表 。是OC中weak功能实现的核心数据结构。
......
}
目前有了 NONPOINTER_ISA
与 散列表
初步概念之后,我们就去看看在内存管理ARC&MRC中是如何使用的呢?
三、ARC&MRC
我们早已经进入ARC(ARC是LLVM和Runtime配合的结果)时代,那些MRC的retain,release等操作已经消失在我们视野中了,绝大多数的内存管理细节已经由编译器代劳了。但是研究内存管理,还是逃不过他们。关于ARC&MRC定义,我建议参考这篇内容:iOS内存管理(MRC、ARC)深入浅出这里我不做过多的说明,我们主要来探索以下几个常用方法的本质:
1.alloc
关于alloc我们以前分析过,具体可见 iOS 底层探索:alloc & init等
这里贴一个流程图以便复习:
2.reatinCount
reatinCount
:也就是引用计数
。操作引用计数的方法:retain
、release
在非 ARC 环境可以使用retainCount
方法获取某个对象的引用计数,其会调用 objc_object
的 rootRetainCount()
方法
- (NSUInteger)retainCount {
return _objc_rootRetainCount(self);
}
uintptr_t
_objc_rootRetainCount(id obj)
{
ASSERT(obj);
return obj->rootRetainCount();
}
在 ARC 时代除了使用Core Foundation
库的CFGetRetainCount()
方法,也可以使用Runtime
的 _objc_rootRetainCount(id obj)
方法来获取引用计数,此时需要引入头文件。这个函数也是调用 objc_object
的 rootRetainCount()
方法:
// rootRetainCount() 方法对引用计数存储逻辑进行了判断
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();
}
rootRetainCount()
方法对引用计数存储逻辑进行了判断,
- 如果是TaggedPointer,可以直接获取引用计数;
- 如果不是TaggedPointer, 调用的
sidetable_retainCount()
方法:
注 : 这里暂时不讲解
sidetable_retainCount()
等方法,在下面讲解retain
、release
可操作引用计数的的时候穿插理解。
3.retain
retain:MRC下 ,作为修饰属性的关键字,该属性在赋值的时候,先release之前的值,然后再赋新值给属性,引用再加1。 如果手动调用会对引用计数加1
///在MRC里,retain关键字表示会由编译器帮我们生成ivar和set/get方法,生成的set方法里自带内存管理
@property (nonatomic, retain) Dog * dog;
(void)setDog:(Dog *)dog;
//编译时自动生成的代码
- (void)setDog:(Dog *)dog {
//这里必须要判断_dog != dog
if (_dog != dog) {
//先release之前的值
[_dog release];
//然后再赋新值给属性,引用再加1
_dog = [dog retain];
}
}
- (Dog *)dog {
return _dog;
}
我们可以查看retain
的调用栈,也可以直接进入源码中查找如下:
+ (id)retain {
return (id)self;
}
// Replaced by ObjectAlloc
- (id)retain {
return _objc_rootRetain(self);
}
NEVER_INLINE id
_objc_rootRetain(id obj)
{
ASSERT(obj);
return obj->rootRetain();
}
ALWAYS_INLINE id
objc_object::rootRetain()
{
return rootRetain(false, false);
}
核心源码rootRetain
的解析:
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
// 如果如果是taggedPointer直接返回不需要引用计数
if (isTaggedPointer()) return (id)this;
// 默认不使用sideTable
bool sideTableLocked = false;
// 是否需要将引用计数转到sidetable
bool transcribeToSideTable = false;
// 记录新旧两个isa指针
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable = false;
通过 LoadExclusive 方法加载 isa 的值,加锁
oldisa = LoadExclusive(&isa.bits);
// 此时 newisa = oldisa
newisa = oldisa;
// slowpath表示if中的条件是小概率事件
// 如果newisa(此时和oldisa相等) 如果没有采用isa优化
if (slowpath(!newisa.nonpointer)) {
// 解锁
ClearExclusive(&isa.bits);
//rawISA() = (Class)isa.bits
// 如果当前对象的 isa 指向的类对象是元类(也就是说当前对象不是实例对象,而是类对象),直接返回
if (rawISA()->isMetaClass()) return (id)this;
// 如果不需要retain对象(引用计数+1) 且sideTable是锁上的
if (!tryRetain && sideTableLocked)
// sidetable解锁
sidetable_unlock();
if (tryRetain)
// sidetable_tryRetain 尝试对引用计数器进行+1的操作 返回+1操作是否成功
return sidetable_tryRetain() ? (id)this : nil;
else
// 将sidetable中保存的引用计数+1同时返回引用计数
return sidetable_retain();
}
// 如果需要尝试 +1 但是当前对象正在销毁中
if (slowpath(tryRetain && newisa.deallocating)) {
// 解锁
ClearExclusive(&isa.bits);
// 如果不需要去尝试 +1 并且 SideTables 表锁住了,就将其解锁
// 这里的条件 应该永远都不会被满足
if (!tryRetain && sideTableLocked)
sidetable_unlock();
// 如果对象正在被释放 执行retain是无效的
return nil;
}
// 引用计数是否溢出标志位
uintptr_t carry;
//为 isa 中的 extra_rc 位 +1 ,并保存引用计数
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
// 如果 isa中的extra_rc 溢出
if (slowpath(carry)) {
// newisa.extra_rc++ 溢出
// 是否需要处理溢出 这个变量是rootRetain函数外部传入的参数 是否需要处理溢出时的情况
if (!handleOverflow) {
//解锁
ClearExclusive(&isa.bits);
// rootRetain_overflow 方法实际上就是递归调用了当前方法只是将handleOverflow
// 置为yes
return rootRetain_overflow(tryRetain);
}
// 保留isa中extra_rc一半的值 将另一半转移到sidetable中
// 如果不需要尝试 +1 并且 sidetable 表未加锁,就将其加锁
if (!tryRetain && !sideTableLocked) sidetable_lock();
// sidetable加锁
sideTableLocked = true;
// 需要将引用计数转移到sidetable
transcribeToSideTable = true;
// 将newisa中的引用计数置为之前的一半 # define RC_HALF (1ULL<<18)
newisa.extra_rc = RC_HALF;
// isa中是否使用sidetable存储retiancount的标志位置为1
newisa.has_sidetable_rc = true;
}
//while循环开始 直到 isa.bits 中的值被成功更新成 newisa.bits
// StoreExclusive(uintptr_t *dst, uintptr_t oldvalue, uintptr_t value)
// 将更新后的newisa的值更新到isabit中
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
// 如果需要转移引用计数到sidetable中
if (slowpath(transcribeToSideTable)) {
// 将溢出的引用计数加到 sidetable 中
sidetable_addExtraRC_nolock(RC_HALF);
}
// 如果不需要去尝试 +1 并且 SideTables 表锁住了,就将其解锁
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
// 返回当前对象 引用计数已完成+1操作
return (id)this;
}
根据对isa
的理解和rootRetain
源码的解读,接下来我们会分以下四种情况
对 rootRetain
进行分析。
3.1 如果没有采用isa优化
在这里我们假设的条件:!newisa.nonpointer
objc_object::rootRetain(bool tryRetain, bool handleOverflow) {
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
// 解锁
ClearExclusive(&isa.bits);
//rawISA() = (Class)isa.bits
// 如果当前对象的 isa 指向的类对象是元类(也就是说当前对象不是实例对象,而是类对象),直接返回
if (rawISA()->isMetaClass()) return (id)this;
// 如果不需要retain对象(引用计数+1) 且sideTable是锁上的
if (!tryRetain && sideTableLocked)
// sidetable解锁
sidetable_unlock();
if (tryRetain)
// sidetable_tryRetain 尝试对引用计数器进行+1的操作 返回+1操作是否成功
return sidetable_tryRetain() ? (id)this : nil;
else
// 将sidetable中保存的引用计数+1同时返回引用计数
return sidetable_retain();
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
return (id)this;
}
- 3.1.1 这里没有优化isa的情况下,并且 tryRetain = false 我们全局搜索
rootRetain(
,我们会发现,只有当执行rootTryRetain()
,才会tryRetain = true
,源码如下:
objc_object::rootTryRetain()
{
return rootRetain(true, false) ? true : false;
}
那么谁引用了rootTryRetain()
? 再次搜索会发现在objc_loadWeakRetained
这个函数中调用了。看名字就知道这里使用了weak弱引用计数。这里后面再分析。
- 3.1.2. 这里没有优化isa的情况下,并且 tryRetain = false ,那么它会走
sidetable_retain()
这个函数,进入源码分析如下:
// 将 SideTable 表中的引用计数 +1
id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
// 根据对象获取 存储引用计数的sidetable
SideTable& table = SideTables()[this];
table.lock();
// 获取sidetable中存储的引用计数值
size_t& refcntStorage = table.refcnts[this];
// 如果引用计数值没有溢出
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
// 引用计数值+SIDE_TABLE_RC_ONE
// #define SIDE_TABLE_RC_ONE (1UL<<2)
// SIDE_TABLE_RC_ONE = 4 为什么这里会+4我们下面会介绍
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
通过上面这个方法,我们对引用计数器完成了+1(实际上是+4)的操作,那么这里为什么会+4
呢?
那是因为对于table.refcnts
,实际上并不完全是表示引用计数的值,refcnts
的最后两位有特殊的标示意义:
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING (1UL<<1)
倒数第一位标记当前对象是否被weak指针指向(1:有weak指针指向);
倒数第二位标记当前对象是否正在销毁状态(1:处在正在销毁状态);
因此,我们每次执行retain方法时,虽然每次都是+4,但是对于引用计数真实的值来说就是+1 , 64位环境下只有62位是保存溢出的引用计数的.
3.2 正常的 rootRetain(足以保存引用计数)
在这里我们假设的条件:isa
中的 extra_rc
的位数足以存储retainCount
//简化rootRetain
objc_object::rootRetain(bool tryRetain, bool handleOverflow) {
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
return (id)this;
}
方法执行的流程如下:
1.使用 LoadExclusive 加载 isa 的值
2.调用 addc(newisa.bits, RC_ONE, 0, &carry) 方法将 isa 的值加一
3.调用 StoreExclusive(&isa.bits, oldisa.bits, newisa.bits) 更新 isa 的值
4.返回当前对象
3.3 rootRetain(extra_rc不足以保存引用计数)
在这里我们假设的条件:调用 addc
方法为extra_rc
加一
时,8
位的 extra_rc
可能不足以保存引用计数。
//简化rootRetain
objc_object:::rootRetain(bool tryRetain, bool handleOverflow) {
.....省略......
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
.....省略......
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
// 是否需要处理溢出 这个变量是rootRetain函数外部传入的参数 是否需要处理溢出时的情况
if (carry && !handleOverflow)
// rootRetain_overflow 方法实际上就是递归调用了当前方法只是将handleOverflow
// 置为yes
return rootRetain_overflow(tryRetain);
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
}
extra_rc
不足以保存引用计数,并且handleOverflow
=false
。
当方法传入的 handleOverflow
=false
时(这也是通常情况),我们会调用 rootRetain_overflow
方法
NEVER_INLINE id
objc_object::rootRetain_overflow(bool tryRetain)
{
return rootRetain(tryRetain, true);
}
这个方法其实就是重新执行rootRetain
方法,并传入handleOverflow
=true
。
- 拓展一个面试题: 下面打印是多少?
NSObject *objc = [NSObject alloc];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));// 1
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc)); // 1
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc)); // 1
// alloc 引用计数为多少 : 0
// extrc
// + 1
这个题实际上是问objc的引用计数是多少,答案是都是1
。
分析流程如下:
- (NSUInteger)retainCount {
return _objc_rootRetainCount(self);
}
uintptr_t
_objc_rootRetainCount(id obj)
{
ASSERT(obj);
return obj->rootRetainCount();
}
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
//如果是nonpointer isa,才有引用计数的下层处理
if (bits.nonpointer) {
//alloc创建的对象引用计数为0,包括sideTable,所以对于alloc来说,是 0+1=1,这也是为什么通过retaincount获取的引用计数为1的原因
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();
}
因为alloc 不做引用计数加1
的操作,为0
,但是在读取器引用计数的时候会出现调用CFGetRetainCount
时实际调用了retainCount
,最后到rootRetainCount
,其中rc = 1 + bits.extra_rc
。只做了读取没有写入,所以后面读取的都是1
。为了防止alloc创建的对象被释放(引用计数为0会被释放),所以在编译阶段,程序底层默认进行了+1操作。实际上在extra_rc
中的引用计数仍然为0
3.4 rootRetain(处理引用计数的溢出)
当传入的 handleOverflow
=true
时,我们就会在 rootRetain
方法中处理引用计数的溢出。
//简化rootRetain
objc_object::rootRetain(bool tryRetain, bool handleOverflow) {
.....省略......
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
.....省略......
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
// 如果 isa中的extra_rc 溢出
if (slowpath(carry)) {
.....省略......
// 保留isa中extra_rc一半的值 将另一半转移到sidetable中
// 如果不需要尝试 +1 并且 sidetable 表未加锁,就将其加锁
if (!tryRetain && !sideTableLocked) sidetable_lock();
// sidetable加锁
sideTableLocked = true;
// 需要将引用计数转移到sidetable
transcribeToSideTable = true;
// 将newisa中的引用计数置为之前的一半 # define RC_HALF (1ULL<<18)
// 我们都知道NSTaggedPointer预留了19个bit位用来存放引用计数,RC_HALF的值刚好为 2^19 次方的一半。
newisa.extra_rc = RC_HALF;
// isa中是否使用sidetable存储retiancount的标志位置为1
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
// 如果需要转移引用计数到sidetable中
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
//拷贝 另外一半的 引用计数到 side table
//分一半的原因: 减少散列表的操作次数,如果release的时候可以直接在ExtraRC 上操作了。
sidetable_addExtraRC_nolock(RC_HALF);
}
.....省略......
// 返回当前对象 引用计数已完成+1操作
return (id)this;
}
当调用这个方法,并且 handleOverflow
= true
时,我们就可以确定carry
一定是存在的了,
因为extra_rc
已经溢出
了,所以要更新它的值为 RC_HALF(define RC_HALF (1ULL<<7))
,extra_rc
总共为 8
位,RC_HALF
= 0b10000000
。
此时设置 has_sidetable_rc
为真,存储新的isa
的值之后,调用 sidetable_addExtraRC_nolock
方法。
// Move some retain counts to the side table from the isa field.
// Returns true if the object is now pinned.
// 将isa中的引用计数移动到sidetable中 当引用计数达到最大值(溢出)是返回true
bool
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
// 根据对象地址获取到存放引用计数对应的sidetable
SideTable& table = SideTables()[this]; //散列表数组
// 从table.refcnts中获取当前对象的引用计数
size_t& refcntStorage = table.refcnts[this];
// 声明一个局部变量存储旧的引用计数
size_t oldRefcnt = refcntStorage;
// 这时候 引用计数已经超过了三十二位所能表达的最大值 直接返回true
if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;
// 溢出标志位
uintptr_t carry;
//------- 这里我们将溢出的一位 RC_HALF 添加到 oldRefcnt 中 ----
size_t newRefcnt =
addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
// 如果引用计数溢出 设置标识为已满
if (carry) {
// 如果是32位的情况 SIDE_TABLE_RC_PINNED = 1<< (32-1)
// int的最大值 SIDE_TABLE_RC_PINNED = 2147483648
// SIDE_TABLE_FLAG_MASK = 3
// refcntStorage = 2147483648 | (oldRefcnt & 3)
// 如果溢出,直接把refcntStorage 设置成最大值
refcntStorage =
SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
return true;
}
else {
// 如果没有溢出 那么直接将新的引用计数赋值给refcntStorage
refcntStorage = newRefcnt;
return false;
}
}
进入散列表的操作: SideTables()
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;
static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 }; // iPhone时这个值为8
#else
enum { StripeCount = 64 }; //否则为64
#endif
struct PaddedT {
T value alignas(CacheLineSize);
};
PaddedT array[StripeCount];
static unsigned int indexForPointer(const void *p) {
//这里是做类型转换
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
//这就是哈希算法了
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
public:
T& operator[] (const void *p) {
//返回sideTable
return array[indexForPointer(p)].value;
}
-
可以看到,通过
indexForPointe r
,在对StripeCount
取余后,通过哈希函数
来获取到了sideTable
的下标,然后再根据value
取到所需的sideTable
。 -
我们通过
table.refcnts[this]
和StripeCount = 8
可以看出是用SideTables
去管理多个SideTable
,那么可以提出疑问:为什么为什么不直接用一张SideTable
,而是用SideTables
去管理多个SideTable
?
流程图如下:回答 : SideTable里有一个自旋锁,如果把所有的类都放在同一个SideTable,有任何一个类有改动都会对整个table做操作,并且在操作一个类的同时,操作别的类会被锁住等待,这样会导致操作效率和查询效率都很低。而有多个SideTable的话,操作的都是单个Table,并不会影响其他的table,这就是分离锁。
综上分析:在 iOS 的内存管理中,我们使用了isa
结构体中的extra_rc
和 SideTable
来存储某个对象的自动引用计数。
在rootRetain
分析中我们看到如果tryRetain && newisa.deallocating
为真,就会ClearExclusive(&isa.bits)
并且return nil
,自此我们开始引入dealloc的分析
4. dealloc
dealloc执行时机
:当一个对象retain count为0 (不再有强引用指向)时会触发dealloc。
dealloc
方法的作用是什么 ?就是释放当前接收器占用的内存空间,暴露出来是提供给开发者重写的 ,用来释放编码过程中用到的通知、Observer 等需要手动管理释放的操作方法。
dealloc
的源码分析:
void
_objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
核心源码 : rootDealloc
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // isTaggedPointer 为真直接返回
if (fastpath(isa.nonpointer && //是否优化过isa指针
!isa.weakly_referenced && //是否存在弱引用指向
!isa.has_assoc && //是否设置过关联对象
!isa.has_cxx_dtor && //是否有cpp的析构函数(.cxx_destruct)
!isa.has_sidetable_rc)) //引用计数器是否过大无法存储在isa中
{
assert(!sidetable_present());
free(this); // 都为空直接释放
}
else {
object_dispose((id)this); // 如果不是进入object_dispose
}
}
直接free 这个比较简单,我们直接分析 object_dispose()
源码:
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj); // 销毁实例
free(obj); // 释放对象
return nil;
}
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(); //将当前对象的指针指为nil
}
return obj;
}
inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
//如果是优化过的isa 先取出sideTable拿到引用表和当前对象 ,然后执行weak_clear_no_lock()
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.
//如果引用计数表中还有数据,就会擦除引用计数表中的数据 执行 table.refcnts.erase()
clearDeallocating_slow();
}
}
整体流程图如下:
dealloc流程图
5.release
release作用 : 使引用计数器值-1
release源码如下:
inline void
objc_object::release()
{
ASSERT(!isTaggedPointer());
// 如果没有自定义的release方法 就直接系统默认调用rootRelease
if (fastpath(!ISA()->hasCustomRR())) {
sidetable_release();
return;
}
// 如果有自定义的release方法那么调用对象的release方法
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
}
objc_object::rootRelease()
{
return rootRelease(true, false);
}
核心源码rootRelease
// 两个参数分别是 是否需要调用dealloc函数,是否需要处理 向下溢出的问题
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
// 如果是TaggedPointer 不需要进行release操作
if (isTaggedPointer()) return false;
// 局部变量sideTable是否上锁 默认false
bool sideTableLocked = false;
// 两个局部变量用来记录这个对象的isa指针
isa_t oldisa;
isa_t newisa;
retry:
do {
// 加载这个isa指针
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
// 如果没有进行nonpointer优化
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
// 如果是类对象直接返回false 不需要释放
if (rawISA()->isMetaClass()) return false;
// 如果sideTableLocked 则解锁 这里默认是false
if (sideTableLocked) sidetable_unlock();
// 调用sidetable_release 进行引用计数-1操作
return sidetable_release(performDealloc);
}
// don't check newisa.fast_rr; we already called any RR overrides
// 溢出标记位
uintptr_t carry;
// newisa 对象的extra_rc 进行-1操作
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
// 如果-1操作后 向下溢出了 结果为负数
if (slowpath(carry)) {
// don't ClearExclusive()
// 调用underflow 进行向下溢出的处理
goto underflow;
}
// 循环,直到 isa.bits 中的值被成功更新成 newisa.bits
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits)));
//走到这说明引用计数的 -1 操作已完成
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
underflow:
{
......省略........
}
}
我们发现rootRelease里的内容基本和rootRetain反过来,主要是由两个内部方法retry,underflow组成,下面我们来一步步的整理下引用计数-1操作的具体步骤
同理rootDealloc ,分为以下几种情况
5.1 如果没有采用isa优化
假设条件:如果这个对象没有nonpointer
优化,且不是一个类对象,那么我们直接通过对sidetable
进行-1
操作 ,直接return sidetable_release(performDealloc)
;
// 将 SideTable 表中的引用计数 -1
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
// 根据对象地址获取SideTable
SideTable& table = SideTables()[this];
// 是否需要执行dealloc方法 默认是false
bool do_dealloc = false;
table.lock();
// 获取当前对象的销毁状态 方法的返回值有2个
// 引用计数和当前对象是否已存在与map中
auto it = table.refcnts.try_emplace(this, SIDE_TABLE_DEALLOCATING);
auto &refcnt = it.first->second;
// 如果当前对象之前不存在与map中
if (it.second) {
do_dealloc = true;
} else if (refcnt < SIDE_TABLE_DEALLOCATING) {
// 如果引用计数的值小于 SIDE_TABLE_DEALLOCATING = 2(0010)
// refcnt 低两位分别是SIDE_TABLE_WEAKLY_REFERENCED 0 SIDE_TABLE_DEALLOCATING 1
// 这个对象需要被销毁
do_dealloc = true;
refcnt |= SIDE_TABLE_DEALLOCATING;
} else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
// 如果引用计数有值且未溢出那么-1
refcnt -= SIDE_TABLE_RC_ONE;
}
table.unlock();
// 如果需要执行dealloc 那么就调用这个对象的dealloc
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return do_dealloc;
}
5.2 如果有采用isa优化
假设条件:如果该对象做了nonpointer
优化,那么我们直接对extra_rc
进行-1
操作,操作如下:
// newisa 对象的extra_rc 进行-1操作
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
// 如果-1操作后 向下溢出了 结果为负数
if (slowpath(carry)) {
// don't ClearExclusive()
// 调用underflow 进行向下溢出的处理
goto underflow;
}
其中subc()
将extra_rc
计数-1
- 【情况1】: 如果发现
-1
操作之后,extra_rc
的个数为0
,那么就出现了向下溢出
,执行goto underflow
;,我们需要将sideTable
中的部分引用计数拿到extra_rc
中记录。 - 【情况2】: 如果
没有向下溢出
,执行sidetable_unlock()
,那么我们就直接将修改后的newisa
同步到isa
中即完成了release
操作。
underflow的具体流程分析
underflow:
//newisa的extra_rc在执行-1操作后导致了向下溢出
// 放弃对newisa的修改 使用之前的oldisa
newisa = oldisa;
// 如果 isa 的 has_sidetable_rc 标志位标识引用计数已溢出
// has_sidetable_rc 用于标识是否当前的引用计数过大,无法在isa中存储,
// 而需要借用sidetable来存储。(这种情况大多不会发生)
if (slowpath(newisa.has_sidetable_rc)) {
// 是否需要处理下溢
if (!handleUnderflow) {
// 清除原 isa 中的数据的原子独占
ClearExclusive(&isa.bits);
// 如果不需要处理下溢 直接调用 rootRelease_underflow方法
return rootRelease_underflow(performDealloc);
}
// 如果sidetable是上锁状态
if (!sideTableLocked) {
// 解除清除原 isa 中的数据的原子独占
ClearExclusive(&isa.bits);
// sidetable 上锁
sidetable_lock();
sideTableLocked = true;
// 跳转到 retry 重新开始,避免 isa 从 nonpointer 类型转换成原始类型导致的问题
goto retry;
}
// sidetable_subExtraRC_nolock 放回要从sidetable移动到isa的extra_rc的值
// 默认是获取extra_rc可存储的长度一半的值
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
// 为了避免冲突 has_sidetable_rc 标志位必须保留1的状态 及时sidetable中的个数为0
if (borrowed > 0) {
// 将newisa中引用计数值extra_rc 设置为borrowed - 1
// -1 是因为 本身这次是release操作
newisa.extra_rc = borrowed - 1;
// 然后将修改同步到isa中
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
// 如果保存失败
if (!stored) {
// dropped the reservation.
// 从新装载isa
isa_t oldisa2 = LoadExclusive(&isa.bits);
isa_t newisa2 = oldisa2;
// 如果newisa2是nonpointer类型
if (newisa2.nonpointer) {
// 下溢出标志位
uintptr_t overflow;
// 将从 SideTables 表中获取的引用计数保存到 newisa2 的 extra_rc 标志位中
newisa2.bits =
addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
//
if (!overflow) {
// 如果没有溢出再次将 isa.bits 中的值更新为 newisa2.bits
stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits,
newisa2.bits);
}
}
}
// 如果重试之后依然失败
if (!stored) {
// 将从sidetable中取出的引用计数borrowed 重新加到sidetable中
sidetable_addExtraRC_nolock(borrowed);
// 重新尝试
goto retry;
}
// 完成对 SideTables 表中数据的操作后,为其解锁
sidetable_unlock();
return false;
}
else {
// 在从Side table拿出一部分引用计数之后 Side table为空
// Side table is empty after all. Fall-through to the dealloc path.
}
}
// 如果当前的对象正在被释放
if (slowpath(newisa.deallocating)) {
ClearExclusive(&isa.bits);
// 如果sideTableLocked被锁 那么解锁
if (sideTableLocked) sidetable_unlock();
// 兑现被过度释放
return overrelease_error();
// does not actually return
}
// 将对象被释放的标志位置为true
newisa.deallocating = true;
// 将newisa同步到isa中 如果失败 进行重试
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits))
goto retry;
// 如果sideTableLocked= true
if (slowpath(sideTableLocked))
// Side table解锁
sidetable_unlock();
__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
// 如果需要执行dealloc方法 那么调用该对象的dealloc方法
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
- 先判断
has_sidetable_rc
是否有sidetable
引用计数,如果有我们要确认是否需要处理向下溢出,如果不需要处理向下溢出,那么我们直接调用rootRelease_underflow
方法,
NEVER_INLINE uintptr_t
objc_object::rootRelease_underflow(bool performDealloc)
{
return rootRelease(performDealloc, true);
}
很明显这个方法实际上与retain
操作时处理溢出逻辑相同,将rootRelease
方法中的handleUnderflow
参数置为true
,要处理向下溢出。
- 需要处理向下溢出时,如果当前的
sidetable
处于未上锁的状态时,将sidetable
上锁然后进行重试,如果sidetable
已经上锁了,那么我们会执行下面这句代码
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
sidetable_subExtraRC_nolock
返回要从sidetable
移动到isa
的extra_rc
的值,默认是获取extra_rc
可存储的长度一半的值。
- 如果此时从
sidetable
中拿到的值 > 0,那么我们要将这部分值放到isa
的extra_rc
中进行存储,如果取到的borrowed
的值为0,那么说明sidetable
中的引用计数为0,那么我们直接释放该对象即可。
StoreReleaseExclusive
上面说到如果从sidetable中获取到的值borrowed大于0,那么我们直接将newisa.extra_rc
设置为borrowed - 1
即可。然后我们在调用StoreReleaseExclusive
方法将newisa
同步到isa
中。
如果这里
StoreReleaseExclusive
方法保存失败了,那么我们需要重新调用LoadExclusive
重新声明两个变量newisa2
,oldisa2
。通过addc
方法将extra_rc
置为borrowed-1
执行:newisa2.bits = addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
然后再次调用
StoreReleaseExclusive
方法将newisa2
的改动同步到isa
中。如果
StoreReleaseExclusive
方法依然保存失败,那么我们就把从sidetable中获取的borrowed
重新加到sideTable中。然后调用retry方法。经过
StoreReleaseExclusive
这一步,引用计数更新操作完成。
-
如果引用计数更新成功,那么我们需要先判断,当前对象是否正在被释放,如果正在被释放 那么调用过度释放方法
overrelease_error
-
当一个用户正在被释放时,再次调用
release
方法时会导致crash
。 (过渡释放crash的由来)
overrelease_error
这个方法主要是定义了crash
信息:
NEVER_INLINE uintptr_t
objc_object::overrelease_error()
{
_objc_inform_now_and_on_crash("%s object %p overreleased while already deallocating; break on objc_overrelease_during_dealloc_error to debug", object_getClassName((id)this), this);
objc_overrelease_during_dealloc_error();
return 0; // allow rootRelease() to tail-call this
}
-
如果当前对象没有被正在释放,那么我们将当前对象正在被释放标志位置为true
newisa.deallocating = true;
同时将状态的更新同步到isa
中。如果同步失败,那么会重复走一次retry。 -
最后,更新状态成功后,对
sidetable
的操作也结束了,我们就可以将sidetable
解锁,如果需要执行dealloc
方法,那么我们调用dealloc
进行对象释放
网友评论