本文主要是分析内存管理中的内存管理方案,以及retain
、retainCount
、release
、dealloc
的底层源码分析
ARC & MRC
iOS中的内存管理方案,大致可以分为两类:MRC
(手动内存管理)和ARC(自动内存管理)
MRC
-
在
MRC
时代,系统是通过对象的引用计数来判断一个是否销毁,有以下规则对象被
创建时
引用计数都为1
当对象
被其他指针引用
时,需要手动调用[objc retain]
,使对象的引用计数+1
当指针变量不再使用对象时,需要手动调用
[objc release]
来释放
对象,使对象的引用计数-1
当一个对象的
引用计数为0
时,系统就会销毁
这个对象
所以,在MRC模式下,必须遵守:
谁创建,谁释放,谁引用,谁管理
ARCARC
模式是在WWDC2011和iOS5引入的自动管理机制,即自动引用计数
。是编译器的一种特性。其规则与MRC一致,区别在于,ARC模式下不需要手动retain、release、autorelease。编译器会在适当的位置插入release和autorelease
。
内存布局
我们在iOS-底层原理 24:内存五大区文章中,介绍了内存的五大区。其实除了内存区,还有内核区
和保留区
,以4GB
手机为例,如下所示,系统将其中的3GB
给了五大区+保留区
,剩余的1GB
给内核区使用
内核区
:系统用来进行内核处理操作的区域五大区:这里不再作说明,具体请参考上面的链接
保留区
:预留给系统处理nil等
这里有个疑问,为什么五大区的最后内存地址是从0x00400000
开始的。其主要原因是0x00000000
表示nil
,不能直接用nil表示一个段,所以单独给了一段内存用于处理nil
等情况
内存布局相关面试题
面试题1:全局变量和局部变量在内存中是否有区别?如果有,是什么区别?
有区别
全局变量
保存在内存的全局存储区(即bss+data段)
,占用静态的存储单元局部变量
保存在栈
中,只有在所在函数被调用时才动态的为变量分配存储单元
面试题2:Block中可以修改全局变量,全局静态变量,局部静态变量,局部变量吗?
可以修改
全局变量,全局静态变量
,因为全局变量 和 静态全局变量是全局
的,作用域很广
-
可以修改局部静态变量,不可以修改局部斌量
局部静态变量(static修饰的) 和 局部变量
,被block从外面捕获,成为__main_block_impl_0
这个结构体的成员变量局部变量
是以值方式
传递到block的构造函数中的,只会捕获block中会用到的变量,由于只捕获了变量的值,并非内存地址,所以在block内部不能改变
局部变量的值局部静态变量
是以指针形式
,被block捕获的,由于捕获的是指针,所以可以修改
局部静态变量的值
ARC环境下,一旦使用
__block
修饰并在block中修改,就会触发copy
,block就会从栈区copy到堆区
,此时的block是堆区block
ARC模式下,Block中引用
id类型
的数据,无论有没有__block修饰,都会retain
,对于基础数据类型
,没有__block就无法修改变量值
;如果有__block修饰
,也是在底层修改__Block_byref_a_0
结构体,将其内部的forwarding
指针指向copy后的地址
,来达到值的修改
内存管理方案
内存管理方案除了前文提及的MRC
和ARC
,还有以下三种
Tagged Pointer
:专门用来处理小对象,例如NSNumber、NSDate、小NSString等Nonpointer_isa
:非指针类型的isa,主要是用来优化64位地址,这个在iOS-底层原理 07:isa与类关联的原理一文中,已经介绍了SideTables
:散列表
,在散列表中主要有两个表,分别是引用计数表
、弱引用表
这里主要着重介绍Tagged Pointer
和SideTables
,我们通过一个面试题来引入Tagged Pointer
面试题
以下代码会有什么问题?
//*********代码1*********
- (void)taggedPointerDemo {
self.queue = dispatch_queue_create("com.cjl.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"CJL"]; // alloc 堆 iOS优化 - taggedpointer
NSLog(@"%@",self.nameStr);
});
}
}
//*********代码2*********
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"来了");
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"CJL_越努力,越幸运!!!"];
NSLog(@"%@",self.nameStr);
});
}
}
运行以上代码,发现taggedPointerDemo
单独运行没有问题,当触发touchesBegan
方法后。程序会崩溃,崩溃的原因是多条线程同时对一个对象进行释放
,导致了 过渡释放
所以崩溃。其根本原因是因为nameStr
在底层的类型不一致导致的,我们可以通过调试看出
taggedPointerDemo
方法中的nameStr
类型是NSTaggedPointerString
,存储在常量区
。因为nameStr
在alloc
分配时在堆区
,由于较小,所以经过xcode中iOS的优化,成了NSTaggedPointerString
类型,存储在常量区touchesBegan
方法中的nameStr
类型是NSCFString
类型,存储在堆上
NSString的内存管理
我们可以通过NSString初始化的两种方式,来测试NSString的内存管理
通过
WithString + @""
方式初始化通过
WithFormat
方式初始化
#define KLog(_c) NSLog(@"%@ -- %p -- %@",_c,_c,[_c class]);
- (void)testNSString{
//初始化方式一:通过 WithString + @""方式
NSString *s1 = @"1";
NSString *s2 = [[NSString alloc] initWithString:@"222"];
NSString *s3 = [NSString stringWithString:@"33"];
KLog(s1);
KLog(s2);
KLog(s3);
//初始化方式二:通过 WithFormat
//字符串长度在9以内
NSString *s4 = [NSString stringWithFormat:@"123456789"];
NSString *s5 = [[NSString alloc] initWithFormat:@"123456789"];
//字符串长度大于9
NSString *s6 = [NSString stringWithFormat:@"1234567890"];
NSString *s7 = [[NSString alloc] initWithFormat:@"1234567890"];
KLog(s4);
KLog(s5);
KLog(s6);
KLog(s7);
}
以下是运行的结果
所以,从上面可以总结出,NSString的内存管理
主要分为3种
__NSCFConstantString
:字符串常量,是一种编译时常量
,retainCount值很大,对其操作,不会引起引用计数变化,存储在字符串常量区
__NSCFString
:是在运行时
创建的NSString子类
,创建后引用计数会加1,存储在堆上
-
NSTaggedPointerString
:标签指针,是苹果在64位环境下对NSString、NSNumber
等对象做的优化
。对于NSString对象来说当
字符串是由数字、英文字母组合且长度小于等于9
时,会自动成为NSTaggedPointerString
类型,存储在常量区
当有
中文或者其他特殊符号
时,会直接成为__NSCFString
类型,存储在堆区
Tagged Pointer 小对象
由一个NSString的面试题,引出了Tagged Pointer
,为了探索小对象的引用计数处理,所以我们需要进入objc
源码中查看retain、release
源码 中对 Tagged Pointer
小对象的处理
小对象的引用计数处理分析
-
查看
setProperty -> reallySetProperty
源码,其中是对新值retain,旧值release
进入
objc_retain
、objc_release
源码,在这里都判断是否是小对象,如果是小对象,则不会进行retain或者release,会直接返回。因此可以得出一个结论:如果对象是小对象,不会进行retain 和 release
//****************objc_retain****************
__attribute__((aligned(16), flatten, noinline))
id
objc_retain(id obj)
{
if (!obj) return obj;
//判断是否是小对象,如果是,则直接返回对象
if (obj->isTaggedPointer()) return obj;
//如果不是小对象,则retain
return obj->retain();
}
//****************objc_release****************
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (!obj) return;
//如果是小对象,则直接返回
if (obj->isTaggedPointer()) return;
//如果不是小对象,则release
return obj->release();
}
小对象的地址分析
继续以NSString为例,对于NSString来说
一般的
NSString
对象指针,都是string值 + 指针地址
,两者是分开的对于
Tagged Pointer
指针,其指针+值
,都能在小对象中体现。所以Tagged Pointer
既包含指针,也包含值
在之前的文章讲类的加载时,其中的_read_images
源码有一个方法对小对象进行了处理,即initializeTaggedPointerObfuscator
方法
- 进入
_read_images -> initializeTaggedPointerObfuscator
源码实现
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;
}
//在iOS14之后,对小对象进行了混淆,通过与操作+_OBJC_TAG_MASK混淆
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;
}
}
在实现中,我们可以看出,在iOS14之后,Tagged Pointer
采用了混淆处理,如下所示
- 我们可以在源码中通过
objc_debug_taggedpointer_obfuscator
查找taggedPointer的编码
和解码
,来查看底层是如何混淆处理的
//编码
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;
通过实现,我们可以得知,在编码和解码部分,经过了两层异或
,其目的是得到小对象自己
,例如以 1010 0001
为例,假设mask
为 0101 1000
1010 0001
^0101 1000 mask(编码)
1111 1001
^0101 1000 mask(解码)
1010 0001
-
所以在外界,为了
获取小对象的真实地址
,我们可以将解码的源码拷贝到外面,将NSString混淆部分进行解码
,如下所示观察解码后的
小对象地址
,其中的62
表示b
的ASCII
码,再以NSNumber为例,同样可以看出,1
就是我们实际的值
到这里,我们验证了小对象指针地址中确实存储了值
,那么小对象地址高位其中的0xa、0xb
又是什么含义呢?
//NSString
0xa000000000000621
//NSNumber
0xb000000000000012
0xb000000000000025
- 需要去源码中查看
_objc_isTaggedPointer
源码,主要是通过保留最高位的值
(即64位的值),判断是否等于_OBJC_TAG_MASK
(即2^63),来判断是否是小对象
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
//等价于 ptr & 1左移63,即2^63,相当于除了64位,其他位都为0,即只是保留了最高位的值
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
所以0xa、0xb
主要是用于判断是否是小对象taggedpointer,即判断条件
,判断第64位上是否为1(taggedpointer
指针地址即表示指针地址,也表示值)
0xa
转换成二进制为1 010
(64为为1,63~61后三位表示 tagType类型 - 2),表示NSString
类型0xb
转换为二进制为1 011
(64为为1,63~61后三位表示 tagType类型 - 3),表示NSNumber
类型,这里需要注意一点,如果NSNumber
的值是-1
,其地址中的值是用补码
表示的
这里可以通过_objc_makeTaggedPointer
方法的参数tag类型objc_tag_index_t
进入其枚举,其中 2
表示NSString
,3
表示NSNumber
-
同理,我们可以定义一个
NSDate对象
,来验证其tagType
是否为6
。通过打印结果,其地址高位是0xe
,转换为二进制为1 110
,排除64位的1,剩余的3位正好转换为十进制是6,符合上面的枚举值
Tagged Pointer 总结
Tagged Pointer
小对象类型(用于存储NSNumber、NSDate、小NSString
),小对象指针不再是简单的地址,而是地址 + 值
,即真正的值
,所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量
而以。所以可以直接进行读取。优点是占用空间小 节省内存
Tagged Pointer
小对象不会进入retain 和 release
,而是直接返回了
,意味着不需要ARC进行管理
,所以可以直接被系统自主的释放和回收
Tagged Pointer
的内存并不存储在堆
中,而是在常量区
中,也不需要malloc和free
,所以可以直接读取,相比存储在堆区的数据读取,效率上快了3倍左右
。创建
的效率相比堆区快了近100倍左右
所以,综合来说,
taggedPointer
的内存管理方案,比常规的内存管理,要快很多Tagged Pointer
的64位地址中,前4
位代表类型
,后4位主要适用于系统做一些处理,中间56位用于存储值
优化内存建议:对于
NSString
来说,当字符串较小
时,建议直接通过@""
初始化,因为存储在常量区
,可以直接进行读取。会比WithFormat初始化方式
更加快速
SideTables 散列表
当引用计数
存储到一定值是,并不会再存储到Nonpointer_isa
的位域的extra_rc
中,而是会存储到SideTables
散列表中
下面我们就来继续探索引用计数retain的底层实现
retain 源码分析
- 进入
objc_retain -> retain -> rootRetain
源码实现,主要有以下几部分逻辑:【第一步】判断是否为
Nonpointer_isa
-
【第二步】操作引用计数
1、如果不是
Nonpointer_isa
,则直接操作SideTables
散列表,此时的散列表并不是只有一张,而是有很多张(后续会分析,为什么需要多张)2、判断
是否正在释放
,如果正在释放,则执行dealloc流程3、执行
extra_rc+1
,即引用计数+1操作,并给一个引用计数的状态标识carry
,用于表示extra_rc
是否满了4、如果
carray
的状态表示extra_rc的引用计数满
了,此时需要操作散列表
,即 将满状态的一半拿出来存到extra_rc
,另一半存在 散列表的rc_half
。这么做的原因是因为如果都存储在散列表,每次对散列表操作都需要开解锁,操作耗时,消耗性能大,这么对半分
操作的目的在于提高性能
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
//为什么有isa?因为需要对引用计数+1,即retain+1,而引用计数存储在isa的bits中,需要进行新旧isa的替换
isa_t oldisa;
isa_t newisa;
//重点
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//判断是否为nonpointer isa
if (slowpath(!newisa.nonpointer)) {
//如果不是 nonpointer isa,直接操作散列表sidetable
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return (id)this;
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
// don't check newisa.fast_rr; we already called any RR overrides
//dealloc源码
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
uintptr_t carry;
//执行引用计数+1操作,即对bits中的 1ULL<<45(arm64) 即extra_rc,用于该对象存储引用计数值
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
//判断extra_rc是否满了,carry是标识符
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;
//如果extra_rc满了,则直接将满状态的一半拿出来存到extra_rc
newisa.extra_rc = RC_HALF;
//给一个标识符为YES,表示需要存储到散列表
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.
//将另一半存在散列表的rc_half中,即满状态下是8位,一半就是1左移7位,即除以2
//这么操作的目的在于提高性能,因为如果都存在散列表中,当需要release-1时,需要去访问散列表,每次都需要开解锁,比较消耗性能。extra_rc存储一半的话,可以直接操作extra_rc即可,不需要操作散列表。性能会提高很多
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}
问题1:散列表为什么在内存有多张?最多能够多少张?
如果散列表只有一张表
,意味着全局所有的对象都会存储在一张表中,都会进行开锁解锁(锁是锁整个表的读写)。当开锁时,由于所有数据都在一张表,则意味着数据不安全
如果
每个对象都开一个表
,会耗费性能
,所以也不能有无数个表散列表的类型是
SideTable
,有如下定义
struct SideTable {
spinlock_t slock;//开/解锁
RefcountMap refcnts;//引用计数表
weak_table_t weak_table;//弱引用表
....
}
- 通过查看
sidetable_unlock
方法定位SideTables
,其内部是通过SideTablesMap
的get方法获取。而SideTablesMap
是通过StripedMap<SideTable>
定义的
void
objc_object::sidetable_unlock()
{
//SideTables散列表并不只是一张,而是很多张,与关联对象表类似
SideTable& table = SideTables()[this];
table.unlock();
}
👇
static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}
👇
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;
从而进入StripedMap
的定义,从这里可以看出,同一时间,真机中散列表最多只能有8张
问题2:为什么在用散列表,而不用数组、链表?
数组
:特点在于查询方便(即通过下标访问),增删比较麻烦
(类似于之前讲过的methodList
,通过memcopy、memmove
增删,非常麻烦),所以数据的特性是读取快,存储不方便
链表
:特点在于增删方便,查询慢(需要从头节点开始遍历查询)
,所以链表的特性是存储快,读取慢
-
散列表
的本质
就是一张哈希表
,哈希表集合了数组和链表的长处
,增删改查都比较方便
,例如拉链哈希表
(在之前锁的文章中,讲过的tls
的存储结构就是拉链形式
的),是最常用的,如下所示可以从
SideTables -> StripedMap -> indexForPointer
中验证是通过哈希函数计算哈希下标
以及sideTables
为什么可以使用[]
的原因
所以,综上所述,retain
的底层流程如下所示
总结:retain 完整回答
retain
在底层首先会判断是否是 Nonpointer isa
,如果不是,则直接操作散列表 进行+1操作
如果
是Nonpointer isa
,还需要判断是否正在释放
,如果正在释放,则执行dealloc流程
,释放弱引用表和引用技术表,最后free释放对象内存如果
不是正在释放,则对Nonpointer isa进行常规的引用计数+1.
这里需要注意一点的是,extra_rc
在真机上只有8位用于存储引用计数的值
,当存储满了
时,需要借助散列表
用于存储。需要将满了的extra_rc
对半分,一半(即2^7)存储在散列表
中。另一半还是存储在extra_rc
中,用于常规的引用计数的+1或者-1操作,然后再返回
release 源码分析
分析了retain
的底层实现,下面来分析release
的底层实现
- 通过
setProperty -> reallySetProperty -> objc_release -> release -> rootRelease -> rootRelease
顺序,进入rootRelease
源码,其操作与retain 相反判断是否是
Nonpointer isa
,如果不是,则直接对散列表进行-1操作
如果是
Nonpointer isa
,则对extra_rc
中的引用计数值进行-1
操作,并存储此时的extra_rc状态到carry
中如果此时的状态
carray
为0,则走到underflow
流程-
underflow
流程有以下几步:判断
散列表
中是否存储了一半的引用计数
如果是,则从
散列表
中取出
存储的一半引用计数,进行-1操作
,然后存储到extra_rc
中如果此时
extra_rc
没有值,散列表中也是空的,则直接进行析构,即dealloc
操作,属于自动触发
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
if (isTaggedPointer()) return false;
bool sideTableLocked = false;
isa_t oldisa;
isa_t newisa;
retry:
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//判断是否是Nonpointer isa
if (slowpath(!newisa.nonpointer)) {
//如果不是,则直接操作散列表-1
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return false;
if (sideTableLocked) sidetable_unlock();
return sidetable_release(performDealloc);
}
// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
//进行引用计数-1操作,即extra_rc-1
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
//如果此时extra_rc的值为0了,则走到underflow
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;
//判断散列表中是否存储了一半的引用计数
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.
//进行-1操作,然后存储到extra_rc中
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.
}
}
//此时extra_rc中值为0,散列表中也是空的,则直接进行析构,即自动触发dealloc流程
// Really deallocate.
//触发dealloc的时机
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();
__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
if (performDealloc) {
//发送一个dealloc消息
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
所以,综上所述,release
的底层流程如下图所示
dealloc 源码分析
在retain
和release
的底层实现中,都提及了dealloc
析构函数,下面来分析dealloc
的底层的实现
- 进入
dealloc -> _objc_rootDealloc -> rootDealloc
源码实现,主要有两件事:- 根据条件
判断是否有isa、cxx、关联对象、弱引用表、引用计数表
,如果没有,则直接free释放内存
- 如果有,则进入
object_dispose
方法
- 根据条件
inline void
objc_object::rootDealloc()
{
//对象要释放,需要做哪些事情?
//1、isa - cxx - 关联对象 - 弱引用表 - 引用计数表
//2、free
if (isTaggedPointer()) return; // fixme necessary?
//如果没有这些,则直接free
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);
}
}
- 进入
object_dispose
源码,其目的有以下几个-
销毁实例,主要有以下操作
调用c++析构函数
删除关联引用
释放散列表
清空弱引用表
free释放内存
-
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.
//调用C ++析构函数
if (cxx) object_cxxDestruct(obj);
//删除关联引用
if (assoc) _object_remove_assocations(obj);
//释放
obj->clearDeallocating();
}
return obj;
}
👇
inline void
objc_object::clearDeallocating()
{
//判断是否为nonpointer isa
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());
}
👇
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
ASSERT(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
SideTable& table = SideTables()[this];
table.lock();
if (isa.weakly_referenced) {
//清空弱引用表
weak_clear_no_lock(&table.weak_table, (id)this);
}
if (isa.has_sidetable_rc) {
//清空引用计数
table.refcnts.erase(this);
}
table.unlock();
}
所以,综上所述,dealloc
底层的流程图如图所示
所以,到目前为止,从最开始的alloc
底层分析(见iOS-底层原理 02:alloc & init & new 源码分析)-> retain
-> release
-> dealloc
就全部串联起来了
retainCount 源码分析
引用计数的分析通过一个面试题来说明
面试题:alloc创建的对象的引用计数为多少?
- 定义如下代码,打印其引用计数
NSObject *objc = [NSObject alloc];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));
打印结果如下
- 进入
retainCount -> _objc_rootRetainCount -> rootRetainCount
源码,其实现如下
- (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();
}
在这里我们可以通过源码断点调试,来查看此时的extra_rc
的值,结果如下
答案:综上所述,alloc
创建的对象实际的引用计数为0
,其引用计数打印结果为1
,是因为在底层rootRetainCount
方法中,引用计数默认+1
了,但是这里只有
对引用计数的读取
操作,是没有写入操作的,简单来说就是:为了防止alloc创建的对象被释放(引用计数为0会被释放),所以在编译阶段,程序底层默认进行了+1操作。实际上在extra_rc中的引用计数仍然为0
总结
alloc
创建的对象没有retain和release
alloc
创建对象的引用计数为0
,会在编译时期
,程序默认加1
,所以读取引用计数时为1
强应用(强持有)
假设此时有两个界面A、B,从A push
到B界面,在B界面中有如下定时器代码。当从B pop
回到A界面[图片上传中...(E70D3F5D-8815-4138-BFDD-017B1BFCE0E7.png-6861f8-1609331145410-0)]
时,发现定时器没有停止,其方法仍然在执行,为什么?
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
其主要原因是B界面没有释放
,即没有执行dealloc
方法,导致timer也无法停止和释放
解决方式一
- 重写
didMoveToParentViewController
方法
- (void)didMoveToParentViewController:(UIViewController *)parent{
// 无论push 进来 还是 pop 出去 正常跑
// 就算继续push 到下一层 pop 回去还是继续
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}
解决方式二
- 定义timer时,采用
闭包
的形式,因此不需要指定target
- (void)blockTimer{
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer fire - %@",timer);
}];
}
现在,我们从底层来深入研究,为什么B
界面有了timer
之后,导致B界面释放不掉,即不会走到dealloc
方法。我们可以通过官方文档查看timerWithTimeInterval:target:selector:userInfo:repeats:
方法中对target的描述
从文档中可以看出,timer对传入的target具有强持有,即timer
持有self
。由于timer是定义在B界面中,所以self也持有timer
,因此 self -> timer -> self
构成了循环引用
在iOS-底层原理 30:Block底层原理文章中,针对循环应用提供了几种解决方式。我们我们尝试通过__weak
即弱引用
来解决,代码修改如下
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
我们再次运行程序,进行push-pop跳转。发现问题还是存在,即定时器方法仍然在执行,并没有执行B的dealloc方法,为什么呢?
-
我们使用
__weak
虽然打破了self -> timer -> self
之前的循环引用,即引用链变成了self -> timer -> weakSelf -> self
。但是在这里我们的分析并不全面,此时还有一个Runloop对timer的强持有
,因为Runloop
的生命周期
比B
界面更长
,所以导致了timer无法释放
,同时也导致了B界面的self也无法释放
。所以,最初引用链
应该是这样的加上
weakSelf
之后,变成了这样
weakSelf 与 self
对于weakSelf
和 self
,主要有以下两个疑问
1、
weakSelf
会对引用计数进行+1
操作吗?2、
weakSelf
和self
的指针地址相同吗,是指向同一片内存吗?带着疑问,我们在
weakSelf
前后打印self
的引用计数
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakSelf = self;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
运行结果如下,发现前后self
的引用计数都是8
因此可以得出一个结论:weakSelf没有对内存进行+1操作
- 继续打印
weakSelf
和self
对象,以及指针地址
po weakSelf
po self
po &weakSelf
po &self
结果如下
从打印结果可以看出,当前self
取地址 和 weakSelf
取地址的值是不一样的。意味着有两个指针地址,指向的是同一片内存空间
,即weakSelf 和 self 的内存地址是不一样,都指向同一片内存空间
的
从上面打印可以看出,此时
timer
捕获的是<LGTimerViewController: 0x7f890741f5b0>
,是一个对象
,所以无法通过weakSelf来解决强持有
。即引用链关系为:NSRunLoop -> timer -> weakSelf(<LGTimerViewController: 0x7f890741f5b0>)
。所以RunLoop对整个 对象的空间有强持有
,runloop没停,timer 和 weakSelf是无法释放的而我们在
Block
原理中提及的block的循环引用
,与timer
的是有区别的。通过block底层原理的方法__Block_object_assign
可知,block
捕获的是对象的指针地址
,即weakself 是 临时变量的指针地址
,跟self
没有关系,因为weakSelf是新的地址空间
。所以此时的weakSelf相当于中间值
。其引用关系链为self -> block -> weakSelf(临时变量的指针地址)
,可以通过地址
拿到指针
所以在这里,我们需要区别下block
和timer
循环引用的模型
timer模型:
self -> timer -> weakSelf -> self
,当前的timer
捕获的是B界面的内存,即vc对象的内存
,即weakSelf
表示的是vc对象
Block模型:
self -> block -> weakSelf -> self
,当前的block捕获的是指针地址
,即weakSelf
表示的是指向self的临时变量的指针地址
解决 强引用(强持有)
以下几种方法的思路均是:依赖中介者模式
,打破强持有
,其中推荐思路四
思路一:pop时在其他方法中销毁timer
根据前面的解释,我们知道由于Runloop对timer的强持有
,导致了Runloop间接的强持有了self
(因为timer中捕获的是vc对象
)。所以导致dealloc
方法无法执行。需要查看在pop
时,是否还有其他方法可以销毁timer
。这个方法就是didMoveToParentViewController
didMoveToParentViewController
方法,是用于当一个视图控制器中添加或者移除viewController后,必须调用的方法。目的是为了告诉iOS,已经完成添加/删除子控制器的操作。在B界面中重写
didMoveToParentViewController
方法
- (void)didMoveToParentViewController:(UIViewController *)parent{
// 无论push 进来 还是 pop 出去 正常跑
// 就算继续push 到下一层 pop 回去还是继续
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}
思路二:中介者模式,即不使用self,依赖于其他对象
在timer模式中,我们重点关注的是fireHome
能执行,并不关心timer捕获的target
是谁,由于这里不方便使用self
(因为会有强持有问题),所以可以将target换成其他对象
,例如将target换成NSObject对象
,将fireHome
交给target
执行
- 将timer的target 由self改成objc
//**********1、定义其他对象**********
@property (nonatomic, strong) id target;
//**********1、修改target**********
self.target = [[NSObject alloc] init];
class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:");
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireHome) userInfo:nil repeats:YES];
//**********3、imp**********
void fireHomeObjc(id obj){
NSLog(@"%s -- %@",__func__,obj);
}
运行结果如下
运行发现执行dealloc
之后,timer还是会继续执行
。原因是解决了中介者的释放
,但是没有解决中介者的回收
,即self.target
的回收。所以这种方式有缺陷
可以通过在dealloc
方法中,取消定时器来解决,代码如下
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
运行结果如下,发现pop之后,timer释放,从而中介者也会进行回收释放
思路三:自定义封装timer
这种方式是根据思路二的原理,自定义封装timer,其步骤如下
- 自定义timerWapper
-
在初始化方法中,定义一个timer,其target是自己。即
timerWapper
中的timer
,一直监听自己,判断selector
,此时的selector已交给了传入的target(即vc对象),此时有一个方法fireHomeWapper
,在方法中,判断target是否存在如果
target存在
,则需要让vc知道,即向传入的target发送selector消息,并将此时的timer参数也一并传入,所以vc就可以得知fireHome
方法,就这事这种方式定时器方法能够执行的原因如果
target不存在
,已经释放了,则释放当前的timerWrapper,即打破了RunLoop对timeWrapper的强持有 (timeWrapper <-×- RunLoop
)
自定义
cjl_invalidate
方法中释放timer。这个方法在vc的dealloc方法中调用,即vc释放,从而导致timerWapper释放
,打破了vc
对timeWrapper
的的强持有(vc -×-> timeWrapper
)
-
//*********** .h文件 ***********
@interface CJLTimerWapper : NSObject
- (instancetype)cjl_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (void)cjl_invalidate;
@end
//*********** .m文件 ***********
#import "CJLTimerWapper.h"
#import <objc/message.h>
@interface CJLTimerWapper ()
@property(nonatomic, weak) id target;
@property(nonatomic, assign) SEL aSelector;
@property(nonatomic, strong) NSTimer *timer;
@end
@implementation CJLTimerWapper
- (instancetype)cjl_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
if (self == [super init]) {
//传入vc
self.target = aTarget;
//传入的定时器方法
self.aSelector = aSelector;
if ([self.target respondsToSelector:self.aSelector]) {
Method method = class_getInstanceMethod([self.target class], aSelector);
const char *type = method_getTypeEncoding(method);
//给timerWapper添加方法
class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);
//启动一个timer,target是self,即监听自己
self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
}
}
return self;
}
//一直跑runloop
void fireHomeWapper(CJLTimerWapper *wapper){
//判断target是否存在
if (wapper.target) {
//如果存在则需要让vc知道,即向传入的target发送selector消息,并将此时的timer参数也一并传入,所以vc就可以得知`fireHome`方法,就这事这种方式定时器方法能够执行的原因
//objc_msgSend发送消息,执行定时器方法
void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
lg_msgSend((__bridge void *)(wapper.target), wapper.aSelector,wapper.timer);
}else{
//如果target不存在,已经释放了,则释放当前的timerWrapper
[wapper.timer invalidate];
wapper.timer = nil;
}
}
//在vc的dealloc方法中调用,通过vc释放,从而让timer释放
- (void)cjl_invalidate{
[self.timer invalidate];
self.timer = nil;
}
- (void)dealloc
{
NSLog(@"%s",__func__);
}
@end
- timerWapper的使用
//定义
self.timerWapper = [[CJLTimerWapper alloc] cjl_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
//释放
- (void)dealloc{
[self.timerWapper cjl_invalidate];
}
运行结果如下
这种方式看起来比较繁琐,步骤很多,而且针对timerWapper
,需要不断的添加method,需要进行一系列的处理。
思路四:利用NSProxy虚基类的子类
下面来介绍一种timer
强引用最常用
的处理方式:NSProxy子类
可以通过NSProxy
虚基类,可以交给其子类实现,NSProxy的介绍在iOS-底层原理 30:Block底层原理已经介绍过了,这里不再重复
- 首先定义一个继承自
NSProxy
的子类
//************NSProxy子类************
@interface CJLProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end
@interface CJLProxy()
@property (nonatomic, weak) id object;
@end
@implementation CJLProxy
+ (instancetype)proxyWithTransformObject:(id)object{
CJLProxy *proxy = [CJLProxy alloc];
proxy.object = object;
return proxy;
}
-(id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
- 将
timer
中的target
传入NSProxy子类对象
,即timer持有NSProxy子类对象
//************解决timer强持有问题************
self.proxy = [CJLProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
//在dealloc中将timer正常释放
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
}
这样做的主要目的是将强引用的注意力转移成了消息转发
。虚基类只负责消息转发,即使用NSProxy
作为中间代理、中间者
这里有个疑问,定义的proxy
对象,在dealloc释放时,还存在吗?
-
proxy
对象会正常释放,因为vc
正常释放了,所以可以释放其持有者,即timer和proxy
,timer
的释放也打破了runLoop对proxy的强持有
。完美的达到了两层释放
,即vc -×-> proxy <-×- runloop
,解释如下:vc释放,导致了
proxy
的释放dealloc方法中,timer进行了释放,所以runloop强引用也释放了