引言
果设备备受欢迎的背后离不开iOS优秀的内存管理,不同场景,系统提供了不同的内存管理方案来节省内存和提高执行效率,大致有如下三种:
- TaggedPointer (对于一些小对象,比如说NSNumber,NSString等)
- NONPOINTER_ISA (不仅仅是指针)
- 散列表SideTables
TaggedPointer
为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念。对于 64 位程序,引入 Tagged Pointer 后,相关逻辑能减少一半的内存占用,苹果对于Tagged Pointer特点的介绍:
- Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
- Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free。
- 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。
为什么会出现TaggedPointer
假设我们要存储一个 NSNumber 对象,其值是一个整数。正常情况下,如果这个整数只是一个 NSInteger 的普通变量,那么它所占用的内存是与 CPU 的位数有关,在 32 位 CPU 下占 4 个字节,在 64 位 CPU 下是占 8 个字节的。而指针类型的大小通常也是与 CPU 位数相关,一个指针所占用的内存在 32 位 CPU 下为 4 个字节,在 64 位 CPU 下也是 8 个字节。
所以一个普通的 iOS 程序,如果没有Tagged Pointer对象,从 32 位机器迁移到 64 位机器中后,虽然逻辑没有任何变化,但这种 NSNumber、NSDate 一类的对象所占用的内存会翻倍。如下图所示:
为了存储和访问一个 NSNumber 对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失,所以需要一种解决方案(TaggedPointer)来节省内存和提高执行效率。
TaggedPointer的原理
为了改进上面提到的内存占用和效率问题,苹果提出了Tagged Pointer对象。由于 NSNumber、NSDate 一类的变量本身的值需要占用的内存大小常常不需要 8 个字节,拿整数来说,4 个字节所能表示的有符号整数就可以达到 20 多亿(注:2^31=2147483648,另外 1 位作为符号位),对于绝大多数情况都是可以处理的。
所以我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了Tagged Pointer对象之后,64 位 CPU 下 NSNumber 的内存图变成了以下这样:
方案对比:当NSNumber、NSDate、NSString存值很小的情况下
- 在没有使用TaggedPointer之前:NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值(需要创建OC对象)
- 使用TaggedPointer之后:NSNumber指针里面存储的数据变成了:Tag Data,也就是将数据直接存储在了指针中(不需要创建OC对象)
- 当存值很大,指针不够存储数据时(超过64位),才会使用动态分配内存的方式来存储数据(创建OC对象)
- 消息调用时,objc_msgSend 能识别TaggedPointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销(而且这不是真的OC对象,根本就没有isa去找方法)
demo
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSNumber *num1 = @3;
NSNumber *num2 = @4;
NSNumber *num3 = @5;
// 数值太大,64位不够放,得alloc生成个对象来保存
NSNumber *num4 = @(0xFFFFFFFFFFFFFFFF);
// 小数值的NSNumber对象,并不是alloc出来放在堆中的对象,只是一个单纯的指针,目标值是存放在指针的地址值中
NSLog(@"%p %p %p %p", num1, num2, num3, num4);
}
}
// 打印日志
2020-03-23 16:10:30.888204 0800 04-内存管理-Tagged Pointer[6079:225288] 0x2027be5cc632c957 0x2027be5cc632ce57 0x2027be5cc632cf57 0x100512050
说明: 猜测是iOS13之后底层多加了一层掩码,以前输出num1, num2, num3地址是0x327 0x427 0x527 ,直接可以从地址里面看到NSNumber的值
如何判定是否是TaggedPointer
判定规则:将某个对象和1进行位运算
- iOS平台的判定位为最高有效位(第64位)
- Mac平台的判定位为最低有效位(第1位)
判定为是【1】就是TaggedPointer,否则这就是分配到堆中的OC对象的内存地址(OC对象在内存中以16对齐,因此有效位肯定是0,16 = 0x10 = 0b00010000)。
BOOL isTaggedPointer(id pointer) {
return (long)(__bridge void *)pointer & (long)1; // Mac平台是最低有效位(第1位)
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSNumber *num3 = @5;
NSNumber *num4 = @(0xFFFFFFFFFFFFFFFF);
NSLog(@"%d %d ", isTaggedPointer(num3), isTaggedPointer(num4));
}
}
// 打印日志
2020-03-23 16:10:30.888286 0800 04-内存管理-Tagged Pointer[6079:225288] 1 0
优点
TaggedPointer技术的好处:
- 存值:直接把值存到指针中,不需要再新建一个OC对象来保存(额外多分配至少16个字节)--- 省内存
- 取值:直接从指针中把目标值抽取出来,不需要像OC对象那样,先从类对象的方法列表中查找再调用来获取那么麻烦 --- 性能好、效率高
NONPOINTER_ISA
在arm64位下iOS操作系统,Objective-C对象的isa区域不再只是一个指针,在64位架构下的isa指针是64bit位,实际上33位就能够表示类对象(或元类对象)的地址,为了提供内存的利用率,在剩余的bit位当中添加了内存管理的数据内容
isa结构
- arm64架构之前,isa是一个普通的指针,存储着Class、MetaClass对象的地址
- 从arm64架构之后,苹果对isa进行了优化,变成了一个公用体
# 只看arm64情况下
union isa_t {
Class cls;
uintptr_t bits;
struct {
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
};
};
字段含义解释
- nonpointer:0,代表普通的指针,存储着Class、Meta-Class对象的内存地址。 1,代表优化过,使用位域存储更多的信息
- has_assoc:是否有设置过关联对象,如果没有,释放时会更快
- has_cxx_dtor:是否有C 的析构函数(.cxx_destruct),如果没有,释放时会更快
- shiftcls:存储着Class、Meta-Class对象的内存地址信息
- magic: 用于在调试时分辨对象是否未完成初始化
- weakly_referenced:是否有被弱引用指向过,如果没有,释放时会更快
- deallocating:对象是否正在释放
- extra_rc:里面存储的值是引用计数器减1
- has_sidetable_rc:引用计数器是否过大无法存储在isa中,如果为1,那么引用计数会存储在一个叫SideTable的类的属性中。
散列表(SideTables)
SideTables()实际是一个哈希表,我们可以通过对象指针,找到所对应的引用计数表或弱引用表位于哪个SideTable表中。也就是有多个sideTable表
思考:为什么不是一个大表,而是多个表
?
回答:如果只有一张表,所有对象的引用计数都放到一张表中,则如果在修改某个对象的引用计数的时候,由于对象可能在不同线程中被操作,则需要对表进行加锁,这样一来,效率就会极地。
什么是哈希表
是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度,赋值和获取都避免了遍历,提高了效率
SideTable结构
底层源码结构如下:
struct SideTable {
spinlock_t slock;//自旋锁
RefcountMap refcnts;//引用计数表
weak_table_t weak_table;//弱引用表
}
可以看到SideTable是由三部分组成
Spinlock_t自旋锁
- 自旋锁来用来防止操作表结构时可能的竞态条件,适用于轻量访问。比如引用计数的修改
- Spinlock_t是“忙等”的锁,对SideTable加锁,避免数据错误
引用计数表RefcountMap
引用计数表也是一个hash表,通过hash函数找到指针对应的引用计数的位置。
弱引用表weak_table_t
弱引用表也是一个hash表,通过hash函数找到对象对应的弱引用数组
底层结构:
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
};
,