什么是内存管理
程序在运行过程中通常会有以下行为,来增加程序的内存占用
- 创建一个对象
- 定义一个变量
- 调用一个函数或者方法
设备的内存是毕竟有限的,每个软件所能占用的内存也是有限的;当程序所占用的内存较多时,系统就会发出内存警告,这个时候就得回收一些不需要再使用的内存空间(例如回收一些不再使用的对象、变量等);如果程序占用内存过大,系统可能会强制关闭程序,造成闪退、崩溃现象,影响用户体验。
在开发过程中,对内存进行合理的分配、适当的清除内存,回收不再使用的对象,来确保程序的稳定性
在 iOS 开发中,那些对象需要进行内存管理呢?
- 所有继承自
NSObject
的对象需要进行内存管理 - 非对象类型(例如
int
、char
、float
、double
、struct
、enum
等)不需要进行内存管理
继承自
NSobject
的对象的存储是在堆
上
堆是由程序员分配释放,如果程序员不释放,程序结束时可能由OS
回收
非OC
对象一般放在栈
上
栈是由系统自动分配释放,存放函数的参数值,局部变量的值等
内存管理分类
苹果的内存管理分为以下三种:
- 垃圾回收(iOS 运行环境不支持)
- 手动内存管理(Manual Reference Counting)
- 自动管理内存(Automatic Reference Counting)
手动内存管理(MRC)
- 引用计数
每个 OC 对象都有自己的引用计数,它是一个整数,系统是根据对象的引用计数来判断什么时候需要回收一个对象所占用的内存。遵循以下规则:
- 任何一个对象,刚创建的时候,初始的引用计数为 1(alloc、new、copy)
- 对象被引用时,需要给对象发送一条
retain
消息,对象的引用计数 +1 - 不再使用对象时,需要给对象发送一条
release
消息,对象的引用计数 -1 - 当对象的引用计数为 0 时,这个对象即将被销毁,其占用的内存被系统回收。给对象发送
dealloc
消息
谁创建谁
release
谁retain
谁release
有加必须有减,成对出现
- 自动释放池
当我们不再使用一个对象的时候应该将其空间释放,但是有时候我们不知道何时应该将其释放。为了解决这个问题,苹果提供了 autorelease
方法。
autorelease
是一种支持引用计数的内存管理方式,只要给对象发送一条autorelease
消息,会将对象放到一个自动释放池中,当自动释放池被销毁时,会对池子里面的所有对象做一次release
操作
好处:不用再关心对象释放的时间;不用再关心什么时候调用 release
实质:autorelease
实际上只是把对 release
的调用延迟了,对于每一个 autorelease
,系统只是把该对象放入了当前的 autorelease pool
中,当该 pool
被释放时,该 pool
中的所有对象会被调用 release
。
注意,这里只是发送
release
消息,如果当时的引用计数依然不为 0,则该对象依然不会被释放。
自动管理内存(ARC)
自动引用计数(Automatic Reference Counting),即 ARC
,是苹果在 WWDC2011 和 iOS 5 引入的自动管理机制,是新的LLVM 3.0编译器的一项特性。其规则与 MRC
一致,区别在于,ARC
模式下不需要手动 retain
、release
、autorelease
。编译器会在适当的位置插入 release
和 autorelease
。
内存管理优化方案
除了前面提到的 MRC
和 ARC
内存管理方案,苹果还有以下三种优化方案
- Tagged Pointer: 用来处理小对象(例如
NSNumber
、NSDate
等) - Nonpointer_isa: 是否对 isa 指针开启指针优化(1:不止是类对象地址,isa 中包含了类信息、对象引用计数等)
- SideTables: 散列表,主要有两个表,分别为
引用计数表
、弱引用表
下面通过一个题目引出今天的主题
//1
- (void)taggedPointerDemo {
self.queue = dispatch_queue_create("com.lc.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"lhc"]; // alloc 堆 iOS优化 - taggedpointer
NSLog(@"%@",self.nameStr);
});
}
}
//2
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"来了");
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"lhc_和谐学习"];
NSLog(@"%@",self.nameStr);
});
}
}
运行代码,此时会运行方法 1,正常打印;点击屏幕,此时会运行方法2,程序崩溃了。崩溃原因:self.nameStr 调用了对象的 setter
方法,而 setter
的本质是新值 retain
,旧值 release
。多条线程同时对一个对象执行 setter
方法,导致上一个 release
还没结束,下一个 release
执行,从而 过度释放
以至于程序崩溃。
为什么方法 1 正常,方法 2 调用就崩溃了呢?两段代码只是字符串长短不一样,猜想一下是不是它们的数据类型不一样呢,我们下个断点看下
方法 2
从上面我们可以看到,方法 1 中的 nameStr
类型是 NSTaggedPointerString
;方法 2 中的 nameStr
类型是 NSCFString
。它们的数据类型不一致,是因为这个吗,我们先来看下 NSString
的内存管理情况
NSString 的内存管理
通过以下初始化 NSString
的方式,来测试 NSString
的内存管理
//方式一
NSString *str1 = @"123";
NSString *str2 = [[NSString alloc] initWithString:@"234"];
NSString *str3 = [NSString stringWithString:@"456"];
NSLog(@"%p -- %@", str1, [str1 class]);
NSLog(@"%p -- %@", str2, [str2 class]);
NSLog(@"%p -- %@", str3, [str3 class]);
//方式二:
//字符串长度在9以内
NSString *str4 = [NSString stringWithFormat:@"123456789"];
NSString *str5 = [[NSString alloc] initWithFormat:@"123456789"];
NSLog(@"%p -- %@", str4, [str4 class]);
NSLog(@"%p -- %@", str5, [str5 class]);
//字符串长度大于9
NSString *str6 = [NSString stringWithFormat:@"1234567890"];
NSString *str7 = [[NSString alloc] initWithFormat:@"1234567890"];
NSLog(@"%p -- %@", str6, [str4 class]);
NSLog(@"%p -- %@", str7, [str5 class]);
运行结果如下
从上面我们可以发现几种字符串类型
- __NSCFConstantString: 字符串常量,是一种编译时常量,存储在字符串常量区
- NSTaggedPointerString: 小对象类型,是苹果在 64 位环境下对
NSString
、NSNumber
等对象做的优化- 当字符串是由数字、英文字母组合且长度小于等于 9 时,会自动成为
NSTaggedPointerString
类型,存储在常量区 - 当有
中文或者其他特殊符号
时,会直接成为__NSCFString
类型,存储在堆区
- 当字符串是由数字、英文字母组合且长度小于等于 9 时,会自动成为
- __NSCFString: 是在运行时创建的
NSString
子类,创建后引用计数会加 1,存储在堆上
看到这里,上面的题目中方法 1 正常运行也就可以理解了
Tagged Pointer(小对象)
虽然我们知道了小对象的意义,但是苹果的底层到底怎么处理的,我们需要进入源码探索下,怎么下手呢?
从上面的题中我们可以知道 self.nameStr
调用的是对象的 setter
方法,而 setter
的本质是新值 retain
,旧值 release
。 我们就可以按照属性的调用流程去查看苹果对 Tagged Pointer
小对象的处理,在 objc-781
-> setProperty
-> reallySetProperty
我们看到了如下源码
再次进入 objc_retain
、objc_release
源码
//retain
__attribute__((aligned(16), flatten, noinline))
id
objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
//release
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}
通过源码我们可以看到,都做了一层判断是否是小对象 isTaggedPointer()
,如果是小对象,则直接返回对象,不做 retain
和 release
操作
小对象地址分析
一般的 NSString
对象指针,它们的 string 值
和 指针地址
是分开的。那么 Tagged Pointer
会有什么特殊性呢?
Tagged Pointer
指针,既包含了指针,也包含值,在iOS 14
后苹果对Tagged Pointer
采用了混淆处理
NSString *str1 = [NSString stringWithFormat:@"1"];
NSString *str2 = [[NSString alloc] initWithFormat:@"1234567890"];
NSLog(@"%p -- %@ -- %@", str1, str1, [str1 class]);
NSLog(@"%p -- %@ -- %@", str2, str2, [str2 class]);
iOS 14 之后
在类的加载时,_read_images
源码中有个方法对 Tagged Pointer
做了混淆处理 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;
} else {
//iOS 14 之后,对 taggedpointer 进行了混淆
// 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;
}
}
那么底层是如何进行混淆处理的呢?这里我们需要查找 Tagged Pointer
的 编码
和 解码
操作
//编码
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;
}
通过上述代码,我们可以看到,编码和解码过程是加了两层异或操作,目的就是为了拿到 Tagged Pointer
本身。举个例子: 以 1000 1001
为例,mask
为 0101 1000
1000 1001
^ 0101 1000 mask(编码)
1101 0001
^ 0101 1000 mask(解码)
1000 1001
现在我们可以将上述解码的源码放到工程外面去解码,看看 Tagged Pointer
的真实情况
从上面 Tagged Pointer
的地址可以看到,62
就是 b
的 ASCII
码,我们在多打印几个案例看下
到这里,我们验证了小对象指针地址中确实存储了值。那么小对象地址高位其中的0xa、0xb 以及地位的 1、2、4又是什么含义呢?这里我们去看源码底层是如何判断 Tagged Pointer
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
// # define _OBJC_TAG_MASK (1UL<<63)
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
_OBJC_TAG_MASK
等价于 1 左移 63 位,即最高位(64位)为 1(2^63),与上 ptr
结果是否等于 _OBJC_TAG_MASK
。简单点说,就是判断 64 位上值是否为 1。
上面打印的结果中
0xa
转换成二进制数据为1010
,0xb
转换成二进制数据为1011
。64 位为 1,63~61 三位表示tagType
类型:010 表示NSString
类型;011 表示NSNumber
类型。
我们可以通过 _objc_makeTaggedPointer
方法第一个参数传入的 tag
查看其定义
#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,
// 60-bit reserved
OBJC_TAG_RESERVED_7 = 7,
// 52-bit payloads
OBJC_TAG_Photos_1 = 8,
OBJC_TAG_Photos_2 = 9,
OBJC_TAG_Photos_3 = 10,
OBJC_TAG_Photos_4 = 11,
OBJC_TAG_XPC_1 = 12,
OBJC_TAG_XPC_2 = 13,
OBJC_TAG_XPC_3 = 14,
OBJC_TAG_XPC_4 = 15,
OBJC_TAG_NSColor = 16,
OBJC_TAG_UIColor = 17,
OBJC_TAG_CGColor = 18,
OBJC_TAG_NSIndexSet = 19,
OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
#if __has_feature(objc_fixed_enum) && !defined(__cplusplus)
typedef enum objc_tag_index_t objc_tag_index_t;
#endif
Tagged Pointer
用于存储小对象(NSNumber、NSDate、NSString(有条件)等),它不再是简单的地址,而是地址+值
,所以,它实际上已经不再是一个对象了,只是一个披着对象的普通变量
Tagged Pointer
存储在常量区
,不需要malloc和free
,可以直接读取更改值,相比存储组在堆区的数据读取上效率快了3倍
,创建的效率快了106倍
左右
Tagged Pointer
的 64 位地址中,前 4 位代表数据类型,后 4 位主要适用于系统做一些处理,中间位用于存储值
Tagged Pointer
不会进入retain
和release
,直接返回,不需要引用计数管理,可以直接被系统自主的释放和回收
retain 源码
如果是普通的对象,就会走 retain
、release
操作,首先我们去查看 retain
的源码。通过 objc_retain -> retain -> rootRetain
底层源码如下
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
// 为什么有isa?因为需要对引用计数+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
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.
//如果extra_rc满了,则直接将满状态的一半拿出来存到extra_rc,另一半存储到散列表中
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF; // (1ULL<<7) 就是8位的一半
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.
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}
-
- 判断是否为 nonpointer isa,如果不是则直接操作散列表
-
- 判断是否正在释放,如果正在释放,则执行
dealloc
流程
- 判断是否正在释放,如果正在释放,则执行
-
- extra_rc++ 引用计数加 1,
carry
标示extra_rc
是否满了
- extra_rc++ 引用计数加 1,
-
- 如果
extra_rc
满了,此时将满状态的extra_rc
的一半存在extra_rc
,另一半存在散列表中
- 如果
如果都存在散列表中,当需要
release
时,需要去访问散列表,每次都需要开解锁,比较消耗性能。extra_rc
存储一半的话,可以直接操作extra_rc
,不需要操作散列表。性能会提高很多。
SideTables 散列表
散列表的结构如何?它在内存中是不是有多张?通过 sidetable_unlock()
和 sidetable_lock()
源码
void
objc_object::sidetable_lock()
{
SideTable& table = SideTables()[this];
table.lock();
}
void
objc_object::sidetable_unlock()
{
SideTable& table = SideTables()[this];
table.unlock();
}
我们可以看到散列表不止一个,散列表的结构如下
struct SideTable {
spinlock_t slock; //锁
RefcountMap refcnts; //引用计数表
weak_table_t weak_table; //弱引用表
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }
// Address-ordered lock discipline for a pair of side tables.
template
static void lockTwo(SideTable *lock1, SideTable *lock2);
template
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
我们再来看 SideTables
其是通过 SideTablesMap
的 get
方法获取。而 SideTablesMap
是通过 StripedMap
定义的
static StripedMap& SideTables() {
return SideTablesMap.get();
}
static objc::ExplicitInit> SideTablesMap;
再次进入 StripedMap
的定义可以看到同一时间,真机中散列表最多只能有8张,非真机下散列表最多有 64 张
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
散列表的优点
散列表的本质是一张 哈希表
,它集合了数组和链表的长处,增删改查比较方便(数组读取快,存储慢;链表存储快,读取慢)
release 源码
上面分析了 retain
的源码,下面来分析 release
的底层实现。通过 objc_setProperty -> reallySetProperty -> objc_release -> release -> rootRelease -> rootRelease
源码如下:
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)) {
//如果不是,则直接操作散列表
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
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
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.
}
}
// Really deallocate.
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;
}
-
- 判断是否是
nonpointer isa
,如果不是直接操作散列表
- 判断是否是
-
- 如果是
nonpointer isa
,则对extra_rc
中的引用计数 -1,并存储标记carry
- 如果是
-
- 当
extra_rc
中的引用计数为 0,判断散列表中是否有引用计数,如果有执行引用计数 -1 操作
- 当
-
- 如果
extra_rc
中没有值,散列表中的也没有,则直接走析构
- 如果
dealloc 源码
通过 dealloc -> _objc_rootDealloc -> rootDealloc ->
,查看源码实现如下
inline void
objc_object::rootDealloc()
{
// 小对象,直接 return
if (isTaggedPointer()) return; // fixme necessary?
// 没有这些,直接free
if (fastpath(isa.nonpointer && // isa
!isa.weakly_referenced && //引用计数表
!isa.has_assoc && //关联函数
!isa.has_cxx_dtor && // C++
!isa.has_sidetable_rc)) // 散列表
{
assert(!sidetable_present());
free(this);
}
else {
//有
object_dispose((id)this);
}
}
-
- 如果是
Tagged Pointer
,直接return
- 如果是
-
- 判断是否有
isa、cxx、关联对象、弱引用表、引用计数表
,没有的话,直接走free
释放内存
- 判断是否有
-
- 如果有,进入下一步
object_dispose
,源码如下:
- 如果有,进入下一步
id
object_dispose(id obj)
{
if (!obj) return nil;
// 销毁实例
objc_destructInstance(obj);
free(obj);
return nil;
}
-
- 销毁实例
-
- 释放内存
objc_destructInstance
的源码如下
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); //C++ 析构函数
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();
}
- 调用c++析构函数
- 删除关联引用
- 释放散列表
- 清空弱引用表
retainCount 源码分析
通过 retainCount -> _objc_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);
//判断是否是 nonpointer isa
if (bits.nonpointer) {
//是,才会走下层的处理
uintptr_t rc = 1 + bits.extra_rc;
//alloc创建的对象引用计数为0,包括sideTable,所以对于alloc来说,是 0+1=1,这也是为什么通过retaincount获取的引用计数为1的原因
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
//不是,正常返回
sidetable_unlock();
return sidetable_retainCount();
}
这里我们看一道面试题,如下代码,打印结果是什么?
NSObject *objc = [NSObject alloc];
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)objc));
做过开发的人都知道,结果肯定是 1,但是为什么是 1呢?下面我们打个断点来验证下
alloc
创建的对象实际的引用计数为 0,但是它打印的结果为 1,因为在底层 rootRetainCount
方法中,引用计数默认加 1,这里只有对引用计数的读取操作,是没有写入操作的。简单来说就是:为了防止 alloc
创建的对象被释放(引用计数为 0 会被释放),所以在编译阶段,程序底层默认进行了 +1操作。实际上在 extra_rc
中的引用计数仍然为 0。