相关文献:
iOS 内存管理底层分析(一)- 内存相关
iOS 内存管理底层分析(二)- AutoreleasePool底层
本文掌握知识点:
1.内存的五大分区
2.内存管理方案:MRC、ARC、TaggedPointer、nonpointer_isa、SideTables、自动释放池
3.weak_table_t 弱引用表底层原理、__weak的底层原理、弱引用对象的引用计数问题
4.retain、release、dealloc 的源码分析
一、内存的五大分区
堆区
:
堆是向高地址扩展的数据结构;是不连续的内存区域,类似于链表结构(便于增删,不便于查询)
,遵循先进先出(FIFO)
原则;通常以alloc/new/malloc
方式创建的对象。内存地址以0x6
开头。
优点:灵活方便,数据适应面广泛。
缺点:需手动管理,速度慢、容易产生内存碎片。栈区
:
栈是系统数据结构,其对应的进程或线程是唯一的;栈是向低地址扩展数据结构,是一块连续的存储区域,遵循先进后出(FILO)
的原则。堆区的分配一般是在运行时分配;存储局部变量、方法参数、对象的指针等。内存地址以0x7
开头。
优点:栈是由编译器自动分配并释放的,不会产生内存碎片,所以快速高效,便于查询,不便于增删
。
缺点:内存大小有限制,数据不灵活主线程栈大小是1MB,子线程栈大小是512KB。全局区(静态区)
:
全局区是编译时分配的内存空间,程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放,主要存放:未初始化的全局变量和静态变量,即bss区(.bss);已初始化的全局变量和静态变量,即数据区(.data)。内存地址以0x1
开头。
其中,全局变量是指变量值可以在运行时被动态修改,而静态变量是static修饰的变量,包含静态局部变量和静态全局变量。常量区
:
常量区是编译时分配的内存空间,在程序结束后由系统释放,主要存放 已经使用了的,且没有指向的字符串常量代码区
:
代码区是编译时分配主要用于存放程序运行时的代码,代码会被编译成二进制存进内存的
内存五大区的验证:
内存布局
介绍了内存的五大区,但其实除了内存区
,还有内核区
和保留区
。
以4GB手机为例,如下所示,系统将其中的3GB给了五大区+保留区,剩余的1GB给内核区使用:
- 内核区:系统用来进行内核处理操作的区域
- 五大区:上面已说明
- 保留区:预留给系统处理nil等
为什么五大区的最后内存地址是从0x00400000
开始的?
主要原因是0x00000000
表示nil
,不能直接用nil表示一个段,所以单独给了一段内存用于处理nil等情况。
内存布局面试题
面试题:全局变量和局部变量在内存中是否有区别?如果有,是什么区别? 答案是有区别。
-
全局变量
保存在内存的全局存储区(即bss+data段)
,占用静态的存储单元; -
局部变量
保存在栈
中,只有在所在函数被调用时才动态的为变量分配存储单元。
二、内存管理方案
MRC
- 手动引用计数ARC
- 自动引用计数nonpointer_isa
- 新版OC对象的isa指针优化TaggedPointer
- 小对象优化SideTables
- 散列表 (引用计数表和弱引用表)autoreleasePool
- 自动释放池
三、MRC 与 ARC (引用计数)
引用计数
是管理对象声明周期的一种方式。当新建一个对象它的引用计数为1,当这个对象引用计数为0的时候,这个对象就会被销毁并释放其所占用的内存空间。
MRC
在MRC时代,系统是通过对象的引用计数来判断一个是否销毁,有以下规则:
- 对象被创建时引用计数都为1;
- 当对象被其他指针引用时,需要手动调用
[objc retain]
,使对象的引用计数+1; - 当指针变量不再使用对象时,需要手动调用
[objc release]
来释放对象,使对象的引用计数-1; - 当一个对象的引用计数为0时,系统就会销毁这个对象。
ARC
ARC
模式是在WWDC2011
和iOS5
引入的自动管理机制,即自动引用计数
。是编译器的一种特性。其规则与MRC一致,区别在于无需程序员手动插入内存管理相关代码。
结论:
1.在MRC模式
下,必须遵守:谁创建,谁释放,谁引用,谁管理。
2.ARC模式
下不需要手动retain、release、autorelease
,编译器会在适当的位置插入release
和autorelease
。
四、nonpointer_isa - isa指针优化
nonpointer_isa
:非指针类型的isa,主要是在创建对象时,用来优化isa指针的64位地址,具体内容在Objective-C 对象的底层探索。
我们知道在创建OC对象的时候,会初始化一个8字节的isa指针指向该OC对象的类对象。在旧版本的OC对象的isa指针主要记录着对象的引用计数,很显然仅仅是记录这就使用8字节(64位)是非常奢侈的。于是在新版本的OC对象对isa的64位进行了优化。
五、TaggedPointer - 小对象
TaggedPointer
:是一个被打上标记的指针,在栈上分配8字节指针(不再需要堆去分配),该指针指向的不再是地址,而是真实值
。TaggedPointer专门用来处理小对象,例如NSNumber、NSDate、小NSString等。(它是在64位iOS系统下提出来的 iPhone5s以后)
以NSString
为例,运行下面代码打印:
NSString *firstString = @"helloworld"; // __NSCFConstantString 常量区
NSString *secondString = [NSString stringWithFormat:@"helloworld"]; // __NSCFString 堆区
NSString *thirdString = @"hello"; // __NSCFConstantString 常量区
NSString *fourthSting = [NSString stringWithFormat:@"hello"]; // NSTaggedPointerString 栈指针
NSLog(@"%p %@",firstString,[firstString class]); // 0x1058d60c0 __NSCFConstantString
NSLog(@"%p %@",secondString,[secondString class]); // 0x600000b7b960 __NSCFString
NSLog(@"%p %@",thirdString,[thirdString class]); // 0x1058d60e0 __NSCFConstantString
NSLog(@"%p %@",fourthSting,[fourthSting class]); // 0xd9d08f5a3bd7e123 NSTaggedPointerString
注意:此时打印NSTaggedPointerString
类型指针的内容0xd9d08f5a3bd7e123
其实是混淆过的。在下文探讨 小对象地址分析 时会介绍。
NSString
的内存管理主要分为3种:
__NSCFConstantString
:字符串常量,是一种编译时常量,retainCount值很大,对其操作不会引起引用计数变化,存储在字符串常量区
。__NSCFString
:是在运行时创建的NSString子类,创建后引用计数会加1,存储在堆上
。NSTaggedPointerString
:标签指针,是苹果在64位环境下对NSString、NSNumber等对象做的优化。
对于NSString对象来说,当字符串是由数字、英文字母组合且长度 <= 9时
,会自动成为NSTaggedPointerString类型
,存储在常量区
;当有中文 或者 其他特殊符号 或 长度 > 9
时,会直接成为__NSCFString类型
,存储在堆区
。
1.小对象地址分析
以NSString为例:
- 对于一般的
NSString对象指针
,都是string值 + 指针地址
,两者是分开的; - 对于
TaggedPointer指针
,是指针 + 值
,都能在小对象中体现。所以TaggedPointer 既包含指针,也包含值。
上文中提到过NSLog(@"%p %@",fourthSting,[fourthSting class]);
打印出来的取地址的内容是混淆过的。
这是因为objc4-838.1源码在类的加载过程中的_objc_init -> map_images -> _read_images -> initializeTaggedPointerObfuscator
我们可以在源码中通过objc_debug_taggedpointer_obfuscator
查找taggedPointer
的编码和解码
:
#define OBJC_TAG_INDEX_MASK 0x7UL
#define OBJC_TAG_INDEX_SHIFT 0
extern uintptr_t objc_debug_taggedpointer_obfuscator;
extern uint8_t objc_debug_tag60_permutations[8];
uintptr_t objc_obfuscatedTagToBasicTag(uintptr_t tag) {
for (unsigned i = 0; i < 7; i++)
if (objc_debug_tag60_permutations[i] == tag)
return i;
return 7;
}
uintptr_t
objc_decodeTaggedPointer(id ptr)
{
uintptr_t value = (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
uintptr_t basicTag = (value >> OBJC_TAG_INDEX_SHIFT) & OBJC_TAG_INDEX_MASK;
value &= ~(OBJC_TAG_INDEX_MASK << OBJC_TAG_INDEX_SHIFT);
value |= objc_obfuscatedTagToBasicTag(basicTag) << OBJC_TAG_INDEX_SHIFT;
return value;
}
static inline uintptr_t objc_basicTagToObfuscatedTag(uintptr_t tag) {
return objc_debug_tag60_permutations[tag];
}
void *
objc_encodeTaggedPointer(uintptr_t ptr)
{
uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
uintptr_t basicTag = (value >> OBJC_TAG_INDEX_SHIFT) & OBJC_TAG_INDEX_MASK;
uintptr_t permutedTag = objc_basicTagToObfuscatedTag(basicTag);
value &= ~(OBJC_TAG_INDEX_MASK << OBJC_TAG_INDEX_SHIFT);
value |= permutedTag << OBJC_TAG_INDEX_SHIFT;
return (void *)value;
}
于是我们就可以通过调用objc_decodeTaggedPointer
来还原真实指针内容:
其中最高位是标记是否是TaggedPointer,最低三位是看是什么类型,这里010的十进制是2 表示NSString,如下图:
可以通过objc4源码中的_objc_makeTaggedPointer
方法的参数tag类型objc_tag_index_t
进入其枚举,其中 2表示NSString
,3表示NSNumber
:
举例NSNumber
:
2.taggedpointer面试题
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
self.queue = dispatch_queue_create("com.wj.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"WJ"]; // 栈(长度<=9)
NSLog(@"%@",self.nameStr);
});
}
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"安安安安"]; // 堆(中文字符)
NSLog(@"%@",self.nameStr);
});
}
}
结果:引发崩溃,崩溃在第二个for循环里。
原因:在ARC环境下,这里使用多线程有可能引发对同一块堆内存多次调用了release
。
六、SideTables - 散列表
SideTables
:散列表,在散列表中主要有两个表,分别是引用计数表
和弱引用表
。(每次在访问SideTables时的加锁解锁操作,会降低效率的)
但是散列表不是一张表,而是多张表。
打开objc4-838.1源码,找到objc_retain
-> retain
-> rootRetain
这里在本章节的 七、retain源码分析 部分会着重介绍,有兴趣可以滑下去看。主要内容就是引用计数的存储方案。找到 sidetable_addExtraRC_nolock
:
它是从多张表SideTables
里获取该对象的SideTable
。
而SideTables
其实是StripedMap
类型:
多张表并不是无限数量的,苹果设计出: SideTables
在真机下8张表/模拟器下64张表 的方案,以达到运行效率和节省内存平衡目的。
提问一:那我可以设计只有一张全局的表来存储所有对象的引用计数和弱引用信息吗?
答案:可以。但是每次读写对象的时候都得操作这张全局表,而这张全局每次读写都需要频繁地加速/解锁操作,这样会导致系统非常地慢。
提问二:那我可以设计每一个对象都有它单独的表来存储自己的引用计数和弱引用信息吗?
答案:可以。但是每创建一个对象会就会产生一张表,则会产生好多的表,引发大量消耗内存的问题。
1.RefcountMap - 引用计数表
RefcountMap
是用来存储引用计数的。(RefcountMap在一般情况下是用不到的)。
这里的内容在本文章节 七、retain源码分析 里有分析,这里只给总结:
- a.当
isa指针
不是nonpointer_isa
类型的时候,该对象的引用计数就存储在SideTable
里的RefcountMap
; - b.当
isa指针
是nonpointer_isa
类型,并且nonpointer_isa
里的extra_rc
存满了,会把另一半的引用计数存储到SideTable
里的RefcountMap
里,而extra_rc
只有原来的一半。
2.weak_table_t
- 弱引用表(weak底层原理)
UIViewController *oldVC = [UIViewController new];
__weak typeof(oldVC) weakVC = oldVC; // 旧的
UIViewController *newVC = [UIViewController new];
weakVC = newVC; // 新的
接下来看看weak底层逻辑吧。
使用__weak
修饰的指针指向一个对象时,会走源码中的objc_initWeak
。
打开objc4-838.1源码,搜索objc_initWeak
函数
storeWeak
函数就是处理__weak修饰的弱对象
指向一个对象的处理:
// Update a weak variable.
// If HaveOld is true, the variable has an existing value
// that needs to be cleaned up. This value might be nil.
// If HaveNew is true, there is a new value that needs to be
// assigned into the variable. This value might be nil.
// If CrashIfDeallocating is true, the process is halted if newObj is
// deallocating or newObj's class does not support weak references.
// If CrashIfDeallocating is false, nil is stored instead.
enum CrashIfDeallocating {
DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
// HaveOld: weak指针是否之前就指向了对象 即weakVC是否指向过oldVC
// HaveNew: weak指针是否将指向的新的对象 即weakVC是否将要指向newVC
// CrashIfDeallocating: 被弱引用的对象是否正在析构,如果析构则Crash
template
// location: 弱引用指针的地址 即weakVC的地址
// newObj: 将被弱引用指针指向的对象 即newVC对象
static id
storeWeak(id *location, objc_object *newObj)
{
ASSERT(haveOld || haveNew);
if (!haveNew) ASSERT(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj; // 获取 weakVC之前指向的对象oldVC,若没有指向过,则为nil
SideTable *oldTable; // 获取 weakVC之前指向的对象oldVC的散列表,若没有指向过,则为nil
SideTable *newTable; // 获取 weakVC将被弱引用的对象newVC的散列表
// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
// Retry if the old value changes underneath us.
retry:
if (haveOld) { // 如果weakVC曾经指向过oldVC,获取 oldVC对象 和 oldVC散列表
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) { // 如果weakVC将要指向newVC,获取 newVC散列表
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
SideTable::lockTwo(oldTable, newTable); // 加锁
// 排除异常情况(不用管):如果weakVC曾经指向过oldVC,并且weakVC指向的还不是oldVC
if (haveOld && *location != oldObj) {
SideTable::unlockTwo(oldTable, newTable);
goto retry;
}
// Prevent a deadlock between the weak reference machinery
// and the +initialize machinery by ensuring that no
// weakly-referenced object has an un-+initialized isa.
// 排除异常情况(不用管):newVC的类对象还没有被初始化,就去初始化
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo(oldTable, newTable);
class_initialize(cls, (id)newObj);
// If this class is finished with +initialize then we're good.
// If this class is still running +initialize on this thread
// (i.e. +initialize called storeWeak on an instance of itself)
// then we may proceed but it will appear initializing and
// not yet initialized to the check above.
// Instead set previouslyInitializedClass to recognize it on retry.
previouslyInitializedClass = cls;
goto retry;
}
}
// Clean up old value, if any.
if (haveOld) { // 如果weakVC曾经指向过oldVC,则把之前的弱引用注销掉
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// Assign new value, if any.
// weakVC去注册新的弱引用,并指向newVC
if (haveNew) {
newObj = (objc_object *)
// 怎么注册?将weakVC的地址存储到SideTable里weak_table_t里去
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
// 如果不是TaggedPointer或nil,将isa里的是否被弱引用weakly_referenced 置为true
if (!_objc_isTaggedPointerOrNil(newObj)) {
newObj->setWeaklyReferenced_nolock(); // 将isa的是否被弱引用置为true
}
// Do not set *location anywhere else. That would introduce a race.
// 将weakVC的地址保存newVC
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}
SideTable::unlockTwo(oldTable, newTable); // 解锁
// This must be called without the locks held, as it can invoke
// arbitrary code. In particular, even if _setWeaklyReferenced
// is not implemented, resolveInstanceMethod: may be, and may
// call back into the weak reference machinery.
callSetWeaklyReferenced((id)newObj);
return (id)newObj;
}
weak
底层调用:objc_initWeak
-> storeWeak
storeWeak
的底层逻辑:
- 1.如果
weak指针
已经指向了一个A对象,则会把weak指针地址
从SideTable
散列表里的弱引用表weak_table_t
中注销weak_unregister_no_lock
; - 2.如果
weak指针
要 改变指向/指向新的 B对象,则会把weak指针地址
注册进SideTable
散列表里的弱引用表weak_table_t
中weak_register_no_lock
,并且将 B对象的类对象的isa
里的weakly_referenced
置为true,还会将weak指针
指向B对象。
下面就来研究如何注册和注销的。
3.了解weak指针
在弱引用表weak_table_t
是如何注销和注册的(探索weak_unregister_no_lock
、weak_register_no_lock
)。
3.1了解weak_table_t
的数据结构:
struct weak_table_t {
// weak_entry_t是一个hash表,key:当前对象的地址,value: 存储弱引用指针的地址的数组
weak_entry_t *weak_entries; // (因为一个对象可以被多个弱引用去指向)
size_t num_entries; // 个数
uintptr_t mask; // 数组的长度-1(扩容相关)
uintptr_t max_hash_displacement; // 解决hash冲突
};
// hash数组
struct weak_entry_t {
// referent是一个动态的hash数组,存储弱引用指针的地址的数组
DisguisedPtr referent;
union {
// 1.当前对象被弱引用个数>4时,就会用这个struct来存储弱引用指针的地址
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
// 2.当前对象被弱引用个数<=4时,就会用weak_referrer_t来存储弱引用指针的地址
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
// out_of_line用来判断,用哪种1/2方式来存储弱引用指针的地址
bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}
weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}
// weak_entry_t的构造函数
weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent)
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};
注意:SideTable在真机下有8张表,所以weak_table_t也有8张表。
weak_table_t
是一个hash表,key:当前对象的地址;value:存储弱引用指针的地址的数组。为什么是数组?因为一个对象可以同时被多个弱引用去指向。
weak_table_t 数据结构类似于:
// [key: value]
{
A对象的地址: [weak1地址, weak2地址],
B对象的地址: [weak3地址, weak4地址],
...
}
3.2 weak_register_no_lock
将弱引用注册到对象的弱引用表weak_table_t
中
weak_register_no_lock
的底层逻辑:
- a.对TaggedPointer和nil不处理;
- b.通过被引用对象的地址去取出
weak_entry_t哈希数组
,如果有哈希数组,则直接将弱引用指针地址
存入这个哈希数组; - c.如果没有哈希数组,则创建一个,再将
弱引用指针地址
插入到哈希数组。
3.3 weak_unregister_no_lock
将弱引用从对象的弱引用表weak_table_t
中注册
weak_unregister_no_lock
的底层逻辑:
- a.通过
被引用对象的地址
去取出weak_entry_t哈希数组
,如果有哈希数组,则将弱引用指针地址
从哈希数组中移除。倘若哈希数组空了,则需要去清空这个对象的弱引用表weak_table_t
。
weak面试题:(弱引用对象的引用计数问题)
为什么weakVC打印引用计数是2呢?
UIViewController *oldVC = [UIViewController new];
__weak typeof(oldVC) weakVC = oldVC; // 旧的
UIViewController *newVC = [UIViewController new];
weakVC = newVC; // 新的
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)(newVC)));//1 断点这
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)(weakVC)));//2 断点这
打开汇编调试Debug
->Debug Workflow
->Always Show Disassembly
当打印强引用newVC的引用计数时,可以看到汇编会调用CFGetRetainCount
,而打印弱引用weakVC的引用计数时候,则会调用objc_loadWeakRetained
打开objc4-838.1源码,找到objc_loadWeakRetained
:
注意:在打印弱引用weakVC的引用计数时候,会对obj
进行引用计数+1的操作,但是由于obj
是一个局部变量,出了函数域则会引用计数-1。
每次打印引用weakVC的引用计数都是2,其实是一个假象而已。
七、retain源码分析
打开objc4-838.1源码,搜索objc_retain
函数
rootRetain
函数就是处理对象的引用计数的逻辑:
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
if (slowpath(isTaggedPointer())) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
// 先去获取对象的isa指针,因为引用计数信息存储在isa里
isa_t oldisa;
isa_t newisa;
oldisa = LoadExclusive(&isa.bits);
if (variant == RRVariant::FastOrMsgSend) {
// These checks are only meaningful for objc_retain()
// They are here so that we avoid a re-load of the isa.
if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
ClearExclusive(&isa.bits);
if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
return swiftRetain.load(memory_order_relaxed)((id)this);
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}
}
if (slowpath(!oldisa.nonpointer)) {
// a Class is a Class forever, so we can perform this check once
// outside of the CAS loop
if (oldisa.getDecodedClass(false)->isMetaClass()) {
ClearExclusive(&isa.bits);
return (id)this;
}
}
// 核心逻辑在这个do...while!!!
do {
transcribeToSideTable = false;
newisa = oldisa;
// 1.不是nonpointer_isa的情况,sidetable存储引用计数
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain(sideTableLocked);
}
// 下面代码逻辑 是nonpointer_isa的情况
// don't check newisa.fast_rr; we already called any RR overrides
if (slowpath(newisa.isDeallocating())) {
ClearExclusive(&isa.bits);
if (sideTableLocked) {
ASSERT(variant == RRVariant::Full);
sidetable_unlock();
}
if (slowpath(tryRetain)) {
return nil;
} else {
return (id)this;
}
}
// 2.是nonpointer_isa的情况,引用技术位extra_rc存得下,直接+1
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
// 3.是nonpointer_isa的情况,引用技术位extra_rc存不下,extra_rc保留一半的引用计数,并准备将另一半copy到sidetable
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
// 判断extra_rc是否超出
if (variant != RRVariant::Full) {
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存不下,保留一半的引用计数,并准备将另一半copy到sidetable
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
if (variant == RRVariant::Full) {
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();
} else {
ASSERT(!transcribeToSideTable);
ASSERT(!sideTableLocked);
}
return (id)this;
}
总结:
retain
源码调用流程:objc_retain
--> objc_object::retain()
--> objc_object::rootRetain
rootRetain
操作逻辑:
- 1.判断对象是否为
taggedPointer类型
,如果是 则 return; - 2.获取对象的
isa指针
里取出对象的引用计数信息; - 3.判断
isa
是否是nonpointer_isa
:(通常是nonpointer_isa)
3.1 如果不是nonpointer_isa
,将sidetable散列表
里的引用计数+1并return。
3.2 如果是nonpointer_isa
,并且对象正在被释放,直接return。
3.3 如果是nonpointer_isa
,对象不是正在被释放,进入下一步4; - 4.先让
isa
里的extra_rc
引用计数+1,判断是否能够存得下:
4.1 如果extra_rc位
能存得下,就存着。
4.2 如果extra_rc位
能存不下(少数情况才会出现),将has_sidetable_rc
标志位为1,extra_rc
保留一半的引用计数,将另一半的引用计数存储到sidetable
提问:为什么要设计使用extra_rc
来存储引用计数,extra_rc
存满了才将另一半的引用计数存储到sidetable
呢?
因为sidetable
每次读写都需要加锁解锁的操作,系统就没有那么快。这样设计的目的是提高性能
。
八、release源码分析
打开objc4-838.1源码,搜索objc_release
函数
rootRelease
函数就是处理对象的引用计数的逻辑:
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
if (slowpath(isTaggedPointer())) return false;
bool sideTableLocked = false;
// 获取对象的isa,因为引用计数信息存储在isa里
isa_t newisa, oldisa;
oldisa = LoadExclusive(&isa.bits);
if (variant == RRVariant::FastOrMsgSend) {
// These checks are only meaningful for objc_release()
// They are here so that we avoid a re-load of the isa.
if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
ClearExclusive(&isa.bits);
if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
swiftRelease.load(memory_order_relaxed)((id)this);
return true;
}
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
return true;
}
}
if (slowpath(!oldisa.nonpointer)) {
// a Class is a Class forever, so we can perform this check once
// outside of the CAS loop
if (oldisa.getDecodedClass(false)->isMetaClass()) {
ClearExclusive(&isa.bits);
return false;
}
}
retry:
// 引用计数真正操作逻辑在do...while里
do {
newisa = oldisa;
// 1.如果不是nonpointer_isa,操作sidetable里的引用计数-1
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
return sidetable_release(sideTableLocked, performDealloc);
}
// 判断对象是否正在释放,如果是,则直接return
if (slowpath(newisa.isDeallocating())) {
ClearExclusive(&isa.bits);
if (sideTableLocked) {
ASSERT(variant == RRVariant::Full);
sidetable_unlock();
}
return false;
}
// don't check newisa.fast_rr; we already called any RR overrides
// 将nonpointer_isa里的引用计数位extra_rc进行-1操作
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
if (slowpath(carry)) {
// don't ClearExclusive()
goto underflow; // 如果extra_rc的值为0的情况,走underflow代码块
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
if (slowpath(newisa.isDeallocating()))
goto deallocate; // 释放对象内存
if (variant == RRVariant::Full) {
if (slowpath(sideTableLocked)) sidetable_unlock();
} else {
ASSERT(!sideTableLocked);
}
return false;
underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate
// abandon newisa to undo the decrement
newisa = oldisa;
// 如果nonpointer_isa的sidetable标志位 has_sidetable_rc == 1
if (slowpath(newisa.has_sidetable_rc)) {
if (variant != RRVariant::Full) {
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}
// Transfer retain count from side table to inline storage.
// 将引用计数从sidetable转移回extra_rc
if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
// Need to start over to avoid a race against
// the nonpointer -> raw pointer transition.
oldisa = LoadExclusive(&isa.bits);
goto retry;
}
// Try to remove some retain counts from the side table.
// 把sidetable里的引用计数移除
auto borrow = sidetable_subExtraRC_nolock(RC_HALF);
// 如果sidetable上没有多余的东西,我们就把sidetable清理干净
bool emptySideTable = borrow.remaining == 0; // we'll clear the side table if no refcounts remain there
if (borrow.borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
// sidetable保留引用计数-1。尝试将它们添加到内联计数中。
bool didTransitionToDeallocating = false;
newisa.extra_rc = borrow.borrowed - 1; // redo the original decrement too
newisa.has_sidetable_rc = !emptySideTable;
bool stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
if (!stored && oldisa.nonpointer) {
// 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.
uintptr_t overflow;
newisa.bits =
addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow);
newisa.has_sidetable_rc = !emptySideTable;
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
if (stored) {
didTransitionToDeallocating = newisa.isDeallocating();
}
}
}
if (!stored) {
// Inline update failed.
// Put the retains back in the side table.
ClearExclusive(&isa.bits);
sidetable_addExtraRC_nolock(borrow.borrowed);
oldisa = LoadExclusive(&isa.bits);
goto retry;
}
// Decrement successful after borrowing from side table.
// 减量成功后,从sidetable拿回引用计数给extra_rc。
if (emptySideTable)
sidetable_clearExtraRC_nolock();
if (!didTransitionToDeallocating) {
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
}
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}
deallocate:
// Really deallocate.
ASSERT(newisa.isDeallocating());
ASSERT(isa.isDeallocating());
if (slowpath(sideTableLocked)) sidetable_unlock();
__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
总结:
release
源码调用流程:objc_release
--> objc_object::release()
--> objc_object::rootRelease
rootRelease
操作逻辑:
- 1.判断对象是否为
taggedPointer
类型,如果是则return; - 2.获取对象的
isa指针
里取出对象的引用计数信息; - 3.判断
isa
是否是nonpointer_isa
:(通常是nonpointer_isa)
3.1 如果不是nonpointer_isa
,就去操作sidetable
里的引用计数-1,并return。
3.2 如果是nonpointer_isa
,并且对象正在被释放,直接return。
3.3 如果是nonpointer_isa
,对象不是正在被释放,进入下一步4; - 4.先让
isa
里的extra_rc
引用计数-1,判断extra_rc等于0
:
4.1 如果extra_rc!=0
时,直接return。
4.2 如果extra_rc==0
时,判断has_sidetable_rc等于1
:
4.2.1 如果has_sidetable_rc==0
,说明该对象引用计数全部清零,需要被回收内存dealloc
。
4.2.2 如果has_sidetable_rc==1
(少数情况才会出现),说明该对象借助sidetable
存储引用计数,将sidetable
的引用计数赋值给extra_rc
,将sidetable
的引用计数清空,has_sidetable_rc
赋值为0。
九、dealloc源码分析
打开objc4-838.1源码,搜索- (void)dealloc
方法
rootDealloc
去判断:
1.如果isa
是nonpointer_isa
、没有弱引用、没有关联对象、没有析构函数、没有向sidetable
借位,则去直接释放free
;
2.否则调用object_dispose
object_dispose
的逻辑:
1.如果有c++析构函数
,去调用析构函数释放该对象的实例成员变量;
2.如果有关联对象
,去移除关联对象;
3.清除弱引用表
和散列表里引用计数信息
4.释放对象 free
dealloc
源码调用流程:dealloc
--> _objc_rootDealloc
--> objc_object::rootDealloc()
--> 两个分支
1. -> free
2. -> object_dispose
-> free
dealloc
总结:
- 1.判断对象是否为
taggedPointer
类型,如果是则 return; - 2.判断如果
isa是nonpointer_isa
、没有弱引用
、没有关联对象
、没有析构函数
、没有向sidetable借位
,则直接释放对象free
。否则进入3; - 3.如果有
c++析构函数
,去调用析构函数释放该对象的实例成员变量; - 4.如果有
关联对象
,去移除关联对象; - 5.清除
弱引用表
和散列表里引用计数信息
; - 6.释放对象
free
。
说到这里本章节就结束啦,喜欢的朋友点亮⭐️!