iOS Objective-C 内存管理
[TOC]
在iOS开发中我们常说内存有五大区,那么都是哪五大区呢?在iOS中,内存主要分为:栈区、堆区、全局区(静态区)、常量区以及代码区这五大区。其实还分为内核区和保留区只不过这两个区域跟我们程序的运行关系不大。详细的内存五大区的介绍请看我的这篇文章iOS底层原理之内存五大区
以4GB内存举例,其内存结构如下
下面我们将对iOS中的Tagged Pointer
、Nonpointer_isa
、SideTables
、MRC
、ARC
做相关介绍。
1. TaggedPointer
对象在内存中是对齐的,它们的地址总是指针大小的整数倍,通常为16的倍数。对象指针是一个64位的整数,而为了对齐,一些位将永远是零。
Tagged Pointer
利用了这一现状,它使对象指针中非零位有了特殊的含义。在苹果的64位Objective-C
实现中,若对象指针的最低有效位为1(即奇数),则该指针为Tagged Pointer
。这种指针不通过解引用isa
来获取其所属类,而是通过接下来三位的一个类表的索引。该索引是用来查找所属类是采用Tagged Pointer
的哪个类。剩下的60位则留给类来使用。
针对Objective-C
中的小对象通常使用TaggedPointer
来减少内存的占用。Objective-C
中的小对象有NSNumber
、NSDate
、长度很小的NSString
等。一般这类对象不会存储特别多的信息,比如就是一个数字1,这样如果在栈区给其开辟一个指针指向堆区的数据就有些耗费内存了。下面我们就一起看看TaggedPointer
这项技术是如何实现的。
1.1 引入 TaggedPointer
对于NSString
在底层有三种分别是NSCFConstantString
、NSTargetPointerString
、NSCFString
。
对于底层到底是使用哪种类型的区分如下:
- 如果使用
stringWithString
方法或者@""
无论字符串长短都是NSCFConstantString
类型 - 如果使用
stringWithFormat
方法- 一般不超过8,9个英文字母或阿拉伯数字的时候是
NSTargetPointerString
,有时候11个也会使用NSTargetPointerString
,这就跟编码有关系了包含ASCII、六位编码或五位编码 - 如果超过这个长度或者字符串总含有中文或特殊符号一般就是
NSCFString
- 一般不超过8,9个英文字母或阿拉伯数字的时候是
以上的原理就是Apple有一个编码表,如果字符串适合TargetPointer
就会生成一个NSTargetPointerString
。具体的信息请看如下文章
Tagged Pointer Strings或者它的中午版本【译】采用Tagged Pointer的字符串
1.2 小对象的引用计数
首先我们来看一段代码:
- (void)test1 {
for (int i = 0; i<10000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.nameStr = [NSString stringWithFormat:@"test"];
NSLog(@"%@",self.nameStr);
});
}
}
- (void)test2 {
for (int i = 0; i<10000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.nameStr = [NSString stringWithFormat:@"这是一个taggedPointer测试代码"];
NSLog(@"%@",self.nameStr);
});
}
}
以上两个方法,看似没什么差别,但是test2
会引起崩溃,崩溃原因如下:
我们可以看到崩溃的原因出现在objc_release
函数中,那为什么只有test2
中的代码会引起崩溃呢?这两段代码的区别就是字符串长短不一样,test1
中的字符串是NSTargetPointerString
,test2
中的是NSCFString
,为了一探究竟,我们打开objc4-779.1
的代码,找到objc_release
函数来看看:
__attribute__((aligned(16), flatten, noinline))
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();
}
__attribute__((aligned(16), flatten, noinline))
id
objc_autorelease(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->autorelease();
}
通过以上代码我们可以看到,无论是objc_release
还是objc_retain
甚至是objc_autorelease
对TaggedPointer
的处理就是直接返回。
所以说对于小对象来说基本就是不会进行retain
和release
,所以上面的示例程序中使用TaggedPointer
的字符串就不会崩溃。
我们知道小对象并不会指向堆区的内存,所以在这里没有引用计数也就很好理解了。那么小对象是如何存储的呢?我们进一步分析。
1.3 Tagged Pointer存储分析
通过上面章节的介绍我们大概知道了Tagged Pointer
技术是将值存储到地址上,所以对于小对象的指针,是指针加值的组合,那么这项技术是如何实现的呢?我们来到objc4-779.1
来寻找一下:
1.3.1 从源码看实现
我可能通过搜索Tagged pointer
找到如下注释:
/***********************************************************************
* Tagged pointer objects.
*
* Tagged pointer objects store the class and the object value in the
* object pointer; the "pointer" does not actually point to anything.
*
* Tagged pointer objects currently use this representation:
* (LSB)
* 1 bit set if tagged, clear if ordinary object pointer
* 3 bits tag index
* 60 bits payload
* (MSB)
* The tag index defines the object's class.
* The payload format is defined by the object's class.
*
* If the tag index is 0b111, the tagged pointer object uses an
* "extended" representation, allowing more classes but with smaller payloads:
* (LSB)
* 1 bit set if tagged, clear if ordinary object pointer
* 3 bits 0b111
* 8 bits extended tag index
* 52 bits payload
* (MSB)
*
* Some architectures reverse the MSB and LSB in these representations.
*
* This representation is subject to change. Representation-agnostic SPI is:
* objc-internal.h for class implementers.
* objc-gdb.h for debuggers.
**********************************************************************/
从注释中我们可以得到如下信息:
- Tagged pointer对象存储了类信息和对象实际的值,此时的指针不指向任何东西;
- Tagged pointer目前使用以下表示
- 使用最低位作为标记位,如果是标签指针对象就标记为1,如果是普通对象类型就标记为0;
- 接下来的三位是标签索引位
- 余下的60位作为有效负载位,标签索引位定义了标签对象代表的对象的真实类型,负载的格式由实际的类定义;
- 如果标签位是
0b111
,表示该对象使用了是被扩展的标签对象,这种扩展的方式可以允许更多的类使用标签对象来表示,同时负载的有效位数变小。此时:- 1位还是标记位
- 紧接着三位是
0b111
- 接下来8位是扩展标记位
- 余下的52位作为有效的负载位
- 并不是所有架构都如以上的位置分配一样,有些事颠倒了MSB和LSB的,比如iOS,就是通过使用高位作为标记位的。
#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __x86_64__
// 64-bit Mac - tag bit is LSB
# define OBJC_MSB_TAGGED_POINTERS 0
#else
// Everything else - tag bit is MSB
# define OBJC_MSB_TAGGED_POINTERS 1
#endif
1.3.2 TaggedPointer标签
在上面我们提到了三位标签以及八位扩展位,那么这些都表示什么呢?经过一番的探索我们找到如下代码:
// Tagged pointer layout and usage is subject to change on different OS versions.
// Tag indexes 0..<7 have a 60-bit payload.
// Tag index 7 is reserved.
// Tag indexes 8..<264 have a 52-bit payload.
// Tag index 264 is reserved.
#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
};
从上面的定义中我们可以看到60-bit
和52-bit
时的一些标签定义:
- 当处于
60-bit
时使用三位最多表示8中,其中有我们常见的2(NSString)、3(NSNumber)、4(NSIndexPath)、6(NSDate) - 当处于
52-bit
时也会有16(NSColor)、17(UIColor)、18(CGColor)、19(NSIndexSet)这几个我们熟悉的 - 后面我们还可以看到
First60BitPayload = 0
、Last60BitPayload = 6
说明60-bit
时第一个是0,最后一个是6,First52BitPayload = 8
、Last52BitPayload = 263
说明52-bit
时第一个是8,最后一个是263 - 最后我们还可以看到保留264。最大值264是由3位表示8个,8位表示256个,由于
52-bit
是从8作为第一个值的,所以最后可以取值到264。
1.3.3 initializeTaggedPointerObfuscator
当初我们在类加载的时候就提到了Tagged Pointer
,下面我们来到objc4-779.1
中的_read_images
函数中,这里面会调用initializeTaggedPointerObfuscator
函数,初始化TaggedPointer
,下面我们就来到这个函数中一探究竟:
/***********************************************************************
* initializeTaggedPointerObfuscator
* Initialize objc_debug_taggedpointer_obfuscator with randomness.
*
* The tagged pointer obfuscator is intended to make it more difficult
* for an attacker to construct a particular object as a tagged pointer,
* in the presence of a buffer overflow or other write control over some
* memory. The obfuscator is XORed with the tagged pointers when setting
* or retrieving payload values. They are filled with randomness on first
* use.
**********************************************************************/
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;
}
}
- 该函数的左右是用随机性初始化
objc_debug_taggedpointer_obfuscator
- 我们可以看到在
MacOS10.14
和iOS12
之前或者禁用TargetPointer
的时候会将objc_debug_taggedpointer_obfuscator
置为0 - 其余的情况会生成一个随机数赋值给
objc_debug_taggedpointer_obfuscator
并与上_OBJC_TAG_MASK
取反的值,其实这里就是一次混淆操作,目的是让攻击者在缓冲区溢出或对某些内存进行其他写控制的情况下,更难以构造一个特定对象作为标记指针
_OBJC_TAG_MASK
#if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL
可以说objc_debug_taggedpointer_obfuscator
的值每次启动应用都是不一样的,就像该函数注释中说的一样:标记指针混淆器的目的是让攻击者在缓冲区溢出或对某些内存进行其他写控制的情况下,更难以构造一个特定对象作为标记指针。当设置或检索有效负载值时,混淆器会使用带标记的指针。它们在第一次使用时就充满了随机性。异或时也是根据不同架构从高位还是地位开始用作标记进行异或不同的_OBJC_TAG_MASK
,那么初始化的这个objc_debug_taggedpointer_obfuscator
是用来做什么的呢?
1.3.4 TaggedPointer 编码与解码
我们通过搜索objc_debug_taggedpointer_obfuscator
可以找到它的应用点,其实它的应用点就是对TaggedPointer
类型的指针做编码与解码操作,这里的编码与解码就是个异或操作,我们知道两次异或得到的就是其本身。实现代码如下:
extern uintptr_t 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;
}
1.3.5 _objc_makeTaggedPointer
那么我们是在何时进行编码的呢?我们通过搜索_objc_encodeTaggedPointer
来到如下代码处:
static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
// PAYLOAD_LSHIFT and PAYLOAD_RSHIFT are the payload extraction shifts.
// They are reversed here for payload insertion.
// ASSERT(_objc_taggedPointersEnabled());
if (tag <= OBJC_TAG_Last60BitPayload) {
// ASSERT(((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) == value);
uintptr_t result =
(_OBJC_TAG_MASK |
((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) |
((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
} else {
// ASSERT(tag >= OBJC_TAG_First52BitPayload);
// ASSERT(tag <= OBJC_TAG_Last52BitPayload);
// ASSERT(((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT) == value);
uintptr_t result =
(_OBJC_TAG_EXT_MASK |
((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
}
}
函数分析:
- 首先判断
tag
是否小于等于60bit时的最后一个值也就是6- 如果小于就是60bit中的值,进行一系列
|
和位移操作,详见代码吧,最后进行编码返回 - 如果不是就是52bit中的值同样也是一系列
|
和位移操作后进行编码返回
- 如果小于就是60bit中的值,进行一系列
以上位移和|
的值在不同情况是不一样的,感兴趣的可以自己跟一下源码。同样还需要判断是否开启了TaggedPointer
优化,因为有些情况是没有TaggedPointer
优化的。
1.4 验证TaggedPointer的实现
说了这么多关于TaggedPointer
的底层实现,下面我们来验证一番
1.4.1 准备条件
为了得到小对象的真实指针,我们需要对其地址进行解码操作,为了方便使用添加如下代码:
#import <objc/runtime.h>
extern uintptr_t objc_debug_taggedpointer_obfuscator;
uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
1.4.2 验证NSString
测试代码:
NSString *str1 = [NSString stringWithFormat:@"a"];
NSString *str2 = [NSString stringWithFormat:@"b"];
NSString *str3 = [NSString stringWithFormat:@"abcdefghi"];
NSString *str4 = [NSString stringWithFormat:@"cdefghijklm"];
// NSLog(@"%p-%@",str1,str1);
// NSLog(@"%p-%@",str2,str2);
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str1));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str2));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str3));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str4));
打印结果:
通过打印结果我们可以看到:
-
61
是a
的ASCII
码,62
是b
的ASCII
码 - 如果多个字符会用
ASCII
码依次进行存储的 - 对于多个字符不能使用
TaggedPointer
优化的则开辟地址进行存储 - 以上举例前四个都是支持
TaggedPointer
优化的,对于字符串的个数在上面的介绍中有所提及,Apple
为了做到极致的优化采用压缩算法,根据字符编码和使用频率进行优化,目前最多也就11个字符,最终有一个字符集为:
eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX
- 其中最后一位的1是表示字符的个数。
-
0xa
是标签,这里是16进制,转换成2进制为010
也就是2,在上面我们介绍标签的时候2代表NSString
1.4.2 验证NSNumber
验证代码:
NSNumber *number1 = @1;
NSNumber *number2 = @1;
NSNumber *number3 = @2.0;
NSNumber *number4 = @3.2;
NSNumber *number5 = @-1;
NSLog(@"%@-%p-%@ - 0x%lx",object_getClass(number1),number1,number1,_objc_decodeTaggedPointer_(number1));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number2));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number3));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number4));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number5));
打印结果:
通过打印结果我们可以看到:
- 对于1和2都在第位上显示出来
- 目前最后一位的作用还不得而知
- 对于小数即浮点数不能通过
TaggedPointer
优化 - 对于负数直接从最大数开始表示,也就是补码
-
0xb
是标签,这里是16进制,转换成2进制为011
也就是3,在上面我们介绍标签的时候3代表NSNumber
PS:对于最后一位,虽然在上面的打印中我们并不知道它代表着什么,如果多打印几个类型就知道了,其中0表示char类型,1表示short类型,2表示整形,3表示长整型,4表示单精度类型,5表示双精度类型.
1.4.3 验证NSDate
验证代码:
NSDate *date1 = [NSDate dateWithTimeIntervalSince1970:-1];
NSDate *date2 = [NSDate dateWithTimeIntervalSince1970:0];
NSDate *date3 = [NSDate dateWithTimeIntervalSince1970:1];
NSDate *date4 = [NSDate dateWithTimeIntervalSince1970:0xd27e440000000 / 0x800000 + 0x20000000000000 / 0x1000000];
NSDate *date5 = [NSDate dateWithTimeInterval:2 sinceDate:date4];
NSDate *date6 = [NSDate date];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"YYYY-MM-dd HH:mm:ss"];
NSLog(@"%@---%@ - 0x%lx",[formatter stringFromDate:date1],object_getClass(date1),_objc_decodeTaggedPointer_(date1));
NSLog(@"%@---%@ - 0x%lx",[formatter stringFromDate:date2],object_getClass(date2),_objc_decodeTaggedPointer_(date2));
NSLog(@"%@---%@ - 0x%lx",[formatter stringFromDate:date3],object_getClass(date3),_objc_decodeTaggedPointer_(date3));
NSLog(@"%@---%@ - 0x%lx",[formatter stringFromDate:date4],object_getClass(date4),_objc_decodeTaggedPointer_(date4));
NSLog(@"%@---%@ - 0x%lx",[formatter stringFromDate:date5],object_getClass(date5),_objc_decodeTaggedPointer_(date5));
NSLog(@"%@---%@ - 0x%lx",[formatter stringFromDate:date6],object_getClass(date6),_objc_decodeTaggedPointer_(date6));
打印结果:
通过打印结果我们可以看到:
-
0xe
是标签,这里是16进制,转换成2进制为110
也就是6,在上面我们介绍标签的时候3代表NSDate
- 在2001-01-01 00:00:00 这个特殊的时间点应该是一个坐标系原点,所有的时间都在这个时间点的基础上做相对偏移。具体如何体现比较复杂这里就不进一步分析了
1.5 小结
-
TaggedPointer
是一种内存优化技术,对于小对象类型,譬如NSNumber
、NSDate
、短NSString
、NSIndexPath
- 对于这些小对象的指针不在是简单的指针地址,而是
标签+值
的存储方式,这样就不用开辟堆内存以节约内存,也就不远malloc
和free
- 可以直接读取数据,对于读取速度也是一种提升,在内存读取上有着3倍的效率,创建时比以前快106倍。
-
TaggedPointer
不会retain
和release
,减少了内存管理 - 在iOS中
TaggedPointer
的64位地址的前四位代码类型,后四位对于不同类型有着不同的意义
2. NONPOINTER_ISA
既然谈到了NONPOINTER_ISA
那还是先回顾一下iOS Objective-C isa吧。
回顾完之后我们可以知道NONPOINTER_ISA
就是表示是否对 isa 指针开启指针优化,在isa
上占一位,0代表纯isa
指针,1不仅是类对象的的地址,isa
中还包含了类信息,对象的引用计数等信息。
PS:这里本来打算在objc4-779.1
中通过x/4gx
打印一下对象的4段内存地址,看看isa
的信息,结果发现在外部打印和内部真正操作的是不一样的地址,每当对象retain
的时候会修改isa
,前提是NONPOINTER_ISA。这里为什么不一样可能是为了保证对象初始化后展示给外界的地址信息一致吧,如果随意更改就不好了。
后面写了个iOS工程,使用真机运行,不断传递一个属性到下一个控制器中我发现:
- 在真机中
arm64
也是占用8位存储,不知道是不新的isa
修改了extra_rc
的占位数量 - 8位就是256,到了256就会将
has_sidetable_rc
标记为1,然后减少一半的存储,其他的存储到散列表中 - 当在次增加引用计数还是优先增加在
extra_rc
中 - 然后我又换了模拟器,模拟器竟然看不到引用计数。。。。无论跳转多少次,都是000000000
- 然后我又换了iOS12.1.1的iPadPro,发现
extra_rc
确实是占用19位,可能是iOS14以后修改了吧,暂时还看不到新的开源的objc
源码,19位524288个引用计数,就没试,内存估计不够。。。
这里说一下,对于NONPOINTER_ISA
:
- 会将一部分引用计数信息存储到
extra_rc
中,arm64占用19位,x86_64占用8位(iphone XS Max iOS 14.2 Xcode 12.2 也占有8位(不知道是不是修改了,下载了781.2看了看没修改)我又换了iOS12.1.1的iPadPro 发现确实是19位) - 表示该对象的引用计数值,实际上是引用计数值减 1,因为初始化对象时这里是0,获取引用计数会自动加1,当认为引用计数为0会销毁对象
- 如果
extra_rc
不足以存储引用计数则会标记has_sidetable_rc
为1,将引用计数存储到散列表中。
后面在objc4 818.2
找到了这段代码,此时19位怎么来的就清楚了。
3.散列表
在NONPOINTER_ISA
中我们提到,当extra_rc
存储不下引用计数的时候就会存储到散列中,下面我们就来看看散列表存储。散列表并不只是一张,从目前的源码中可以看到在iOS真机中的散列表是8张。
相对于数组和链表散列表的优缺点:
- 数组:优点在于读取,可以直接通过下标读取,但是增加和删除都要找位置后才能增删
- 链表:优点在于增删方便,直接修改指针即可,但是查找就比较麻烦了,需要从指定节点(一般都是头结点)开始遍历
- 散列表:散列也就是哈希,本质还是哈希表,这里实际用的是哈希链表,利用哈希值做下标方便查找,又有链表的特性方便增/删(retain/release),哈希表是苹果特别爱用的一种存储结构,在
@synchronized
中也是用的这种结构的表。(解决哈希冲突的一种方式->拉链法)
所以散列表就如下图所示的样子:
在后续的章节介绍中我们会通过代码中的使用继续说说散列表。
3.MRC & ARC
3.1 MRC & ARC简介
MRC 即手动内存管理,ARC 即自动内存管理
MRC:
- 在
MRC
时代对象被创建时引用计数为1(默认加的1) - 每当被其他指针引用时需要调用
retain
方法使其引用计数+1 - 反之当指针变量不在使用该对象时需要调用
release
方法来释放对象,没调用一次release
会使引用计数-1 - 当一个对象的引用计数为0时,系统就会销毁这个对象。
ARC:
- ARC模式是
WWDC2011
,即iOS5
开始引入的 - ARC模式是
LLVM
和Runtime
的结果 - ARC模式中禁止手动调用
retain
、release
、retainCount
、dealloc
- ARC模式中新加了
weak
、strong
属性关键字 - ARC模式下不需要手动
retain
、release
、autorelease
,编译器会在适当的地方插入以上方法的调用
3.2 源码分析
针对上面提到的一些方法,无论是MRC
的手动编写调用还是ARC
的自动插入调用,最终都会调用的,下面我们就来看看这些方法的具体作用,以及它们都是怎么实现的。
3.2.1 retainCount
我们首先来看看引用计数的统计,我们在以上的介绍中多次提到,对象初始化引用计数即为1,那么为什么为1呢?是在对象初始化的时候自动调用了retain
吗?
我们可以来到alloc
的源码中看看,alloc
的流程图(objc4-756.2
的流程,跟其他objc
源码中的也是大同小异)如下:
其实在alloc
的流程中重要的方法也就是_class_createInstanceFromZone
和initIsa
两个方法,我们顺着流程并着重在这两个方法中探索,并没有发现对象在初始化的时候主动调用retain
相关方法,那么所说的对象创建的引用计数就为1是怎么来的呢?所以我们就只能去retainCount
源码中去看看了。
打印引用计数源码:
#import <objc/runtime.h>
NSObject *objc = [NSObject alloc];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));
打印结果:
retainCount
的调用流程如下:
[NSObject retainCount]
->_objc_rootRetainCount
->objc_object::rootRetainCount
主要实现在objc_object::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);
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();
}
打印isa结构:
我们可以看到在源码中是默认给引用计数加 1
的,下面我们断点打印一下isa
,我们可以看到extra_rc
的值是 0
,所以对象初始的时候引用计数为1是系统默认加的,因为当引用计数为0就会销毁对象,不能一创建就被销毁释放,所以默认+1操作就是为了这个作用。
3.2.2 retain
下面我们来看看retain
是如何增加引用计数的,首先我们还是来到objc4-779.1
源码中查找retain
。经过分析我们可以看到retain
的调用流程如下:objc_retain
->retain
->rootRetain
objc_retain 源码:
id
objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
源码分析:
-
objc_retain
函数就是一层简单的调用封装 - 首先判断对象非空,如果空就直接返回
- 然后判断是否是
TaggedPointer
,如果是也就直接返回,这个在TaggedPointer
章节有详细介绍 - 接下来就是调用
retain
函数进一步处理
retain 源码:
// Equivalent to calling [this retain], with shortcuts if there is no override
inline id
objc_object::retain()
{
ASSERT(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
return rootRetain();
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}
源码分析:
- 这里再次判断了一次
TaggedPointer
- 然后通过
hasCustomRR
方法检查类(包括其父类)中是否含有默认的方法 - 如果有则通过消息发送调用自定义方法(一般都没有)
- 如果没有则调用
rootRetain
进一步处理
rootRetain 源码:
ALWAYS_INLINE id
objc_object::rootRetain()
{
return rootRetain(false, false);
}
ALWAYS_INLINE bool
objc_object::rootTryRetain()
{
return rootRetain(true, false) ? true : false;
}
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
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;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
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;
newisa.extra_rc = RC_HALF;
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;
}
源码分析:
这里还包含一层快捷调用,两个参数全部传false
,以及封装了一个rootTryRetain
函数,尝试进行retain
-
rootRetain
中首先还是判断了一次TaggedPointer
,并初始化了几个变量 - 这里是个
do while
循环,循环的判断是新旧isa的替换是否成功 - 如果不是
nonpointerisa
- 如果是元类直接返回
- 如果不是尝试
tryRetain
并且散列表处于锁定状态,则调用sidetable_unlock
函数解锁散列表 - 如果是尝试
retain
则调用sidetable_tryRetain
函数尝试retain
,retain
成功返回当前对象,不成功返回nil
- 不是尝试
reatin
则调用sidetable_retain
函数进行retain
操作
- 如果是尝试
retain
,并且当前对象正在析构deallocating
- 清理isa的内存
- 判断
!tryRetain && sideTableLocked
则调用sidetable_unlock
解锁散列表,这里应该是调用不到的,进入该判断说明tryRetain
,现在又判断!tryRetain
,则不成立 - 最后返回nil
- 然后调用
addc
函数增加引用计数RC_ONE
就是1左移45位和56位的区别,因为在arm64
和x86_64
同架构下,引用计数extra_rc
分别占用19位和8位,如果引用计数+1,正好分别左移45位和56位。addc
是一层封装调用,内部实现在开源代码中并未找到,其实无非就是对isa联合体进行操作加上一个引用计数extra_rc++
。 - 通过
addc
函数会取到一个carry
的值,如果carry
有值说明newisa.extra_rc++ overflowed
溢出,存满了- 判断是否处理溢出,如果不处理则调用
ClearExclusive
函数清理isa,调用rootRetain_overflow
返回,其实就包装了一层这次调用函数是传值处理溢出 - 判断不是尝试
retain
,散列表没加锁,则调用sidetable_lock
给散列表加锁 - 加上锁后则标记散列表加锁,标记向散列表写入数据
- 将
extra_rc
中的数据设置为满值的一半RC_HALF
就是1左移7位和18位,也是对应arm64
和x86_64
架构中extra_rc
分别占用8
位和19
位。
- 判断是否处理溢出,如果不处理则调用
- 跳出循环后判断
transcribeToSideTable
,向散列表写入数据,则调用sidetable_addExtraRC_nolock
函数向散列表中写入extra_rc
满值的一半的引用计数 - 判断
!tryRetain && sideTableLocked)
不是尝试,散列表加锁,后调用sidetable_unlock
将桑列表解锁,以供其他增加引用计数的地方使用散列表 - 返回当前对象
以上提到的函数源码:
sidetable_loc k& sidetable_unlock 源码:
void
objc_object::sidetable_lock()
{
SideTable& table = SideTables()[this];
table.lock();
}
void
objc_object::sidetable_unlock()
{
SideTable& table = SideTables()[this];
table.unlock();
}
void unlock() { slock.unlock(); }
spinlock_t slock;
以上就是散列表加锁和解锁的函数及其相关的代码,这里使用了spinlock_t
,对于spinlock_t
可以看我以前的iOS 中的锁(3)
sidetable_tryRetain 源码:
bool
objc_object::sidetable_tryRetain()
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
// NO SPINLOCK HERE
// _objc_rootTryRetain() is called exclusively by _objc_loadWeak(),
// which already acquired the lock on our behalf.
// fixme can't do this efficiently with os_lock_handoff_s
// if (table.slock == 0) {
// _objc_fatal("Do not call -_tryRetain.");
// }
bool result = true;
auto it = table.refcnts.try_emplace(this, SIDE_TABLE_RC_ONE);
auto &refcnt = it.first->second;
if (it.second) {
// there was no entry
} else if (refcnt & SIDE_TABLE_DEALLOCATING) {
result = false;
} else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
refcnt += SIDE_TABLE_RC_ONE;
}
return result;
}
// Inserts key,value pair into the map if the key isn't already in the map.
// The value is constructed in-place if the key is not in the map, otherwise
// it is not moved.
template <typename... Ts>
std::pair<iterator, bool> try_emplace(KeyT &&Key, Ts &&... Args) {
BucketT *TheBucket;
if (LookupBucketFor(Key, TheBucket))
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
false); // Already in map.
// Otherwise, insert the new element.
TheBucket =
InsertIntoBucket(TheBucket, std::move(Key), std::forward<Ts>(Args)...);
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
true);
}
- 简单来说就是调用
try_emplace
函数判断表中有没有存这个对象的引用计数,存了就继续增加引用计数,没有就在哈希表中新增一个进行存储。 - 判断链表中有没有第二个值,有的话
result
依旧为true - 如果正在析构
result
为false - 如果不是固定的则引用计数+1
sidetable_retain 源码:
id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
table.lock();
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
没啥说的,就是引用计数表中对引用计数+1,其中+1时会加锁和解锁,处理完返回当前对象。
addc 源码:
static ALWAYS_INLINE uintptr_t
addc(uintptr_t lhs, uintptr_t rhs, uintptr_t carryin, uintptr_t *carryout)
{
return __builtin_addcl(lhs, rhs, carryin, carryout);
}
这就是一层封装,后续的__builtin_addcl
暂时找不到,也没继续探索。
rootRetain_overflow 源码:
NEVER_INLINE id
objc_object::rootRetain_overflow(bool tryRetain)
{
return rootRetain(tryRetain, true);
}
正如上文所说的,只是封装了一次。
sidetable_addExtraRC_nolock 源码:
bool
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
ASSERT(isa.nonpointer);
SideTable& table = SideTables()[this];
size_t& refcntStorage = table.refcnts[this];
size_t oldRefcnt = refcntStorage;
// isa-side bits should not be set here
ASSERT((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
ASSERT((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);
if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;
uintptr_t carry;
size_t newRefcnt =
addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
if (carry) {
refcntStorage =
SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
return true;
}
else {
refcntStorage = newRefcnt;
return false;
}
}
- 一些判断
- 存储传入大小的引用计数到散列表中
关于散列表的扩展:
在上面探索时,我们可以找到散列表的定义如下:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
}
如何获得散列表呢?是通过SideTablesMap.get()
函数
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;
static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}
关于引用计数的散列表倒地有多少张,可以在StripedMap
的源码定义中看到:
template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
struct PaddedT {
T value alignas(CacheLineSize);
};
PaddedT array[StripeCount];
`
`
`
}
可以看到引用计数表在iOS真机和模拟器中有8张,else 64 张。
关于散列表的下标:
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
在StripedMap
类中我们找打了上面的函数,内部是通过哈希计算得到散列表中的下标的。
关于散列表的可以使用[]:
public:
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
const T& operator[] (const void *p) const {
return const_cast<StripedMap<T>>(this)[p];
}
通过以上函数我们可以考的这里是重载了操作符。
到此我们就基本对retain
操作进行近乎完整的分析,下面我们以一幅retain
流程图来总结一下。
3.2.2 release
下面我们来看看release
在底层的实现,release
的主要流程是objc_release
->release
->rootRelease
,这里的设计与retain
的设计思想是一致的,源码如下:
void objc_release(id obj) { [obj release]; }
inline void
objc_object::release()
{
ASSERT(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
rootRelease();
return;
}
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
}
ALWAYS_INLINE bool
objc_object::rootRelease()
{
return rootRelease(true, false);
}
ALWAYS_INLINE bool
objc_object::rootReleaseShouldDealloc()
{
return rootRelease(false, false);
}
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;
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;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
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.
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) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
对于一些调用封装与retain
一致,就不过多分析了,下面我们对rootRelease
做详细的分析:
- 首先还是判断
TaggedPointer
,如果是则返回false,并初始化一些局部变量 - 进入
retry
部分,同样还是一个do while
循环,循环的条件与retain
相同,同样是新旧isa
是否替换成功 -
do
里面还是判断是否是nonpointerisa
- 元类返回false
- 散列表加锁就解锁
- 调用
sidetable_release
函数直接处理散列表返回
- 不是
nonpointerisa
调用subc
函数对引用计数进行-1操作 - 如果-1时出现了获取到的
carry
没有值则解锁散列表以供其他release
进行操作 - 如果获取到的
carry
有值则goto underflow
- 进入
underflow
部分,首先判断has_sidetable_rc
,如果为true
说明存储用到了散列表- 同样是判断溢出操作,如果没有则调用包装了一层
rootRelease_underflow
进行处理 - 如果散列表没加锁则进行加锁,跳转到
retry
,最终还会跳转到underflow
部分 - 接下来则尝试调用
sidetable_subExtraRC_nolock
函数从散列表中借用RC_HALF
最大值一半的引用计数,相当于不够减了则向高位借位操作 - 如果散列表借不来,说明都没有了,则进入
dealloc
操作,如果散列表中能借来,即大于0,则把借来的-1存储到newisa.extra_rc
中 - 然后将新旧isa进行内联更新操作,如果操作成功,则解锁散列表,返回false,如果操作失败则做一些判断进行再次内联更新操作
- 如果还是内联更新失败则调用
sidetable_addExtraRC_nolock
函数将借来的加回去,再goto retry
- 同样是判断溢出操作,如果没有则调用包装了一层
- 以下就是散列表也没有了,则判断是否正在析构释放,
- 如果是则解锁散列表,调用
overrelease_error
函数crash
这就是多次release
导致的崩溃 - 如不没有正在析构释放,则标记为正在析构释放,将空isa与isa进行内联更新,也就是将isa置空,如果内联失败则
goto retry
- 如果内联更新成功则解锁散列表
- 操作原子线程栅栏(我不知道这是干啥的)
- 发送消息调用
dealloc
函数(按照此流程performDealloc
传入时即为true
)
- 如果是则解锁散列表,调用
- 最后返回
true
(析构释放销毁了返回true
,正常减引用计数返回false
)
几处上面提到的源码:
rootRelease_underflow 源码:
NEVER_INLINE uintptr_t
objc_object::rootRelease_underflow(bool performDealloc)
{
return rootRelease(performDealloc, true);
}
一层封装,跟retain
也是一致的,没啥可说的
sidetable_release 源码:
// return uintptr_t instead of bool so that the various raw-isa
// -release paths all return zero in eax
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
bool do_dealloc = false;
table.lock();
auto it = table.refcnts.try_emplace(this, SIDE_TABLE_DEALLOCATING);
auto &refcnt = it.first->second;
if (it.second) {
do_dealloc = true;
} else if (refcnt < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
do_dealloc = true;
refcnt |= SIDE_TABLE_DEALLOCATING;
} else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
refcnt -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return do_dealloc;
}
跟retain
差不多,这里就不过多介绍了
sidetable_subExtraRC_nolock 源码:
size_t
objc_object::sidetable_subExtraRC_nolock(size_t delta_rc)
{
ASSERT(isa.nonpointer);
SideTable& table = SideTables()[this];
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end() || it->second == 0) {
// Side table retain count is zero. Can't borrow.
return 0;
}
size_t oldRefcnt = it->second;
// isa-side bits should not be set here
ASSERT((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
ASSERT((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);
size_t newRefcnt = oldRefcnt - (delta_rc << SIDE_TABLE_RC_SHIFT);
ASSERT(oldRefcnt > newRefcnt); // shouldn't underflow
it->second = newRefcnt;
return delta_rc;
}
- 首先还是判断了一次
nonpointerisa
,并获取到散列表 - 这里判断链表头尾相等或者第二个元素=0(存储个数的?),说明散列表中是空的了返回0
- 如果不是则初始化
oldRefcnt
并且判断是否正在析构,是不是弱引用 - 取出一半的引用计数,判断旧值是不是大于新取出来到,并将新值赋值给
it->second
- 最后返回
delta_rc
至此我们对release
也做了近乎完整的分析,同样总结一张流程图:
3.2.2 dealloc
在release
流程中如果引用计数为0了就会走dealloc
流程,下面我们就来看看dealloc
在底层的实现,dealloc
主要流程是:dealloc
->_objc_rootDealloc
->rootDealloc
,源码如下:
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
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);
}
}
相对于retain
和release
来说dealloc
代码就简单多了:
- 首先还是判断了
TaggedPointer
- 然后判断是不是
nonpointerisa
并且- 没有弱引用
- 没有关联对象
- 没有
c++
析构函数 - 引用计数表没有存储引用计数
- 满足以上条件后还断言了一次是否存在副引用计数表
- 然后
free
该对象
- 如果不满以上任一条件则调用
object_dispose
进行处理
这里也就验证了一些我们以前提到过的,没有弱引用,没有关联对象,没有C++
析构函数就会更快的释放对象的内存。
下面我们来看看object_dispose
这个函数是怎么释放这些那些不满足条件的对象的。
object_dispose 源码:
/***********************************************************************
* object_dispose
* fixme
* Locking: none
**********************************************************************/
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
通过源码我们可以看到:
- 首先判断对象是否为空
- 然后调用
objc_destructInstance
函数进行处理 - 接着
free
对象并返回nil
那么我们接着探索objc_destructInstance
objc_destructInstance 源码:
/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory.
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is 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();
}
return obj;
}
通过源码我们可以看到:
- 首先还是对对象进行了非空判断
- 这里面获取了是否有
c++
的析构和关联对象 - 如果有则调用
object_cxxDestruct
函数处理C++
析构 - 调用
_object_remove_assocations
函数处理关联对象 - 最后调用
clearDeallocating
函数进一步处理
下面我们就来看看c++
析构函数是怎么处理的:
object_cxxDestruct 源码:
/***********************************************************************
* object_cxxDestruct.
* Call C++ destructors on obj, if any.
* Uses methodListLock and cacheUpdateLock. The caller must hold neither.
**********************************************************************/
void object_cxxDestruct(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
object_cxxDestructFromClass(obj, obj->ISA());
}
/***********************************************************************
* object_cxxDestructFromClass.
* Call C++ destructors on obj, starting with cls's
* dtor method (if any) followed by superclasses' dtors (if any),
* stopping at cls's dtor (if any).
* Uses methodListLock and cacheUpdateLock. The caller must hold neither.
**********************************************************************/
static void object_cxxDestructFromClass(id obj, Class cls)
{
void (*dtor)(id);
// Call cls's dtor first, then superclasses's dtors.
for ( ; cls; cls = cls->superclass) {
if (!cls->hasCxxDtor()) return;
dtor = (void(*)(id))
lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
if (dtor != (void(*)(id))_objc_msgForward_impcache) {
if (PrintCxxCtors) {
_objc_inform("CXX: calling C++ destructors for class %s",
cls->nameForLogging());
}
(*dtor)(obj);
}
}
}
在object_cxxDestruct
函数中最终是调用object_cxxDestructFromClass
进行处理的,在这个函数中:
- 通过不断向父类去遍历,直到没有
C++
析构 - 然后通过
lookupMethodInClassAndLoadCache
找到这个析构函数 - 然后判断这个函数不是
_objc_msgForward_impcache
,就调用一下找到的函数
接下来我们看看关联对象是怎么处理的:
_object_remove_assocations 源码:
// Unlike setting/getting an associated reference,
// this function is performance sensitive because of
// raw isa objects (such as OS Objects) that can't track
// whether they have associated objects.
void
_object_remove_assocations(id object)
{
ObjectAssociationMap refs{};
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
if (i != associations.end()) {
refs.swap(i->second);
associations.erase(i);
}
}
// release everything (outside of the lock).
for (auto &i: refs) {
i.second.releaseHeldValue();
}
}
通过源码我们可以看到:
- 关联对象还是通过
AssociationsManager
去处理 - 这里就是找到该对象的关联对象所在的关联表
- 然后判断此关联对象是否是最后一个
- 如果不是则进行一些处理
- 最后遍历这个关联表调用
releaseHeldValue
函数释放关联对象
下面我们看看clearDeallocating
内部都处理了什么?
clearDeallocating 源码:
inline void
objc_object::clearDeallocating()
{
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());
}
通过源码我们可以知道:
- 如果不是
nonpointerisa
则调用sidetable_clearDeallocating
进行处理 - 如果是并且有弱引用或者引用计数表存储了引用计数,则调用
clearDeallocating_slow
函数进行处理
sidetable_clearDeallocating 源码:
void
objc_object::sidetable_clearDeallocating()
{
SideTable& table = SideTables()[this];
// clear any weak table items
// clear extra retain count and deallocating bit
// (fixme warn or abort if extra retain count == 0 ?)
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
table.refcnts.erase(it);
}
table.unlock();
}
这里的源码是针对非nonpointerisa
进行处理的:
- 首先是拿到引用计数表,并加锁
- 然后判断是否包含说引用,包含就处理弱引用
- 最后清理该对象的引用计数
- 最后解锁
clearDeallocating_slow 源码:
// Slow path of clearDeallocating()
// for objects with nonpointer isa
// that were ever weakly referenced
// or whose retain count ever overflowed to the side table.
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();
}
这里是针对nonpointerisa
的处理:
- 首先还是做了一些判断
- 然后获取到引用计数表
- 如果有弱引用就清理该对象的弱引用
- 如果使用了引用计数表就清理引用计数表
- 最后解锁
关于弱引用的处理代码如下:
weak_clear_no_lock 源码:
/**
* Called by dealloc; nils out all weak pointers that point to the
* provided object so that they can no longer be used.
*
* @param weak_table
* @param referent The object being deallocated.
*/
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
// zero out references
weak_referrer_t *referrers;
size_t count;
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
weak_entry_remove(weak_table, entry);
}
通过以上代码我们可以知道:
- 首先是根据弱引用id找到弱引用表
- 然后获取到弱引用数量和弱引用链表
- 遍历这个弱引用链表,将其引用置空
- 最后将其移除出弱引用表
至此我们对dealloc
做了比较详细的分析,联调其内部的一些函数也进行了相关的解读,下面我们通过一个流程图来总结一下: