高龄助孕网
问题解答 高龄助孕网 > 问题解答 >
内存管理系列—OC的内存管理方案
来源:http://www.bjhra.cn  日期:2023-01-30

引言

果设备备受欢迎的背后离不开iOS优秀的内存管理,不同场景,系统提供了不同的内存管理方案来节省内存和提高执行效率,大致有如下三种:

TaggedPointer(对于一些小对象,比如说NSNumber,NSString等)

NONPOINTER_ISA(不仅仅是指针)

散列表SideTables


TaggedPointer

为了节省内存和提高执行效率,苹果提出了TaggedPointer的概念。对于64位程序,引入TaggedPointer后,相关逻辑能减少一半的内存占用,苹果对于TaggedPointer特点的介绍:

TaggedPointer专门用来存储小的对象,例如NSNumber和NSDate

TaggedPointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。

在内存读取上有着3倍的效率,创建时比以前快106倍。

为什么会出现TaggedPointer

假设我们要存储一个NSNumber对象,其值是一个整数。正常情况下,如果这个整数只是一个NSInteger的普通变量,那么它所占用的内存是与CPU的位数有关,在32位CPU下占4个字节,在64位CPU下是占8个字节的。而指针类型的大小通常也是与CPU位数相关,一个指针所占用的内存在32位CPU下为4个字节,在64位CPU下也是8个字节。

所以一个普通的iOS程序,如果没有TaggedPointer对象,从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种NSNumber、NSDate一类的对象所占用的内存会翻倍。如下图所示:


为了存储和访问一个NSNumber对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失,所以需要一种解决方案(TaggedPointer)来节省内存和提高执行效率。

TaggedPointer的原理

为了改进上面提到的内存占用和效率问题,苹果提出了TaggedPointer对象。由于NSNumber、NSDate一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿(注:2^31=2147483648,另外1位作为符号位),对于绝大多数情况都是可以处理的。

所以我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了TaggedPointer对象之后,64位CPU下NSNumber的内存图变成了以下这样:


方案对比:当NSNumber、NSDate、NSString存值很小的情况下

在没有使用TaggedPointer之前:NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值(需要创建OC对象)

使用TaggedPointer之后:NSNumber指针里面存储的数据变成了:Tag+Data,也就是将数据直接存储在了指针中(不需要创建OC对象)

当存值很大,指针不够存储数据时(超过64位),才会使用动态分配内存的方式来存储数据(创建OC对象)

消息调用时,objc_msgS能识别TaggedPointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销(而且这不是真的OC对象,根本就没有isa去找方法)

demo
intmain(intargc,constchar*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-2316:10:30.888204+080004-内存管理-TaggedPointer[6079:225288]0x2027be5cc632c9570x2027be5cc632ce570x2027be5cc632cf570x100512050

说明:猜测是iOS13之后底层多加了一层掩码,以前输出num1,num2,num3地址是0x3270x4270x527,直接可以从地址里面看到NSNumber的值

如何判定是否是TaggedPointer

判定规则:将某个对象和1进行位运算

iOS平台的判定位为最高有效位(第64位)

Mac平台的判定位为最低有效位(第1位)

判定为是【1】就是TaggedPointer,否则这就是分配到堆中的OC对象的内存地址(OC对象在内存中以16对齐,因此有效位肯定是0,16=0x10=0b00010000)。

BOOLisTaggedPointer(idpointer){return(long)(__bridgevoid*)pointer(long)1;//Mac平台是最低有效位(第1位)}intmain(intargc,constchar*argv[]){@autoreleasepool{NSNumber*num3=@5;NSNumber*num4=@(0xFFFFFFFFFFFFFFFF);NSLog(@"%d%d",isTaggedPointer(num3),isTaggedPointer(num4));}}//打印日志2020-03-2316:10:30.888286+080004-内存管理-TaggedPointer[6079:225288]10
优点

TaggedPointer技术的好处:

存值:直接把值存到指针中,不需要再新建一个OC对象来保存(额外多分配至少16个字节)---省内存

取值:直接从指针中把目标值抽取出来,不需要像OC对象那样,先从类对象的方法列表中查找再调用来获取那么麻烦---性能好、效率高

NONPOINTER_ISA

在arm64位下iOS操作系统,Objective-C对象的isa区域不再只是一个指针,在64位架构下的isa指针是64bit位,实际上33位就能够表示类对象(或元类对象)的地址,为了提供内存的利用率,在剩余的bit位当中添加了内存管理的数据内容

isa结构

arm64架构之前,isa是一个普通的指针,存储着Class、MetaClass对象的地址

从arm64架构之后,苹果对isa进行了优化,变成了一个公用体

#只看arm64情况下unionisa_t{Classcls;uintptr_tbits;struct{uintptr_tnonpointer:1;\uintptr_thas_assoc:1;\uintptr_thas_cxx_dtor:1;\uintptr_tshiftcls:33;/*MACH_VM_MAX_ADDRESS0x1000000000*/\uintptr_tmagic:6;\uintptr_tweakly_referenced:1;\uintptr_tdeallocating:1;\uintptr_thas_sidetable_rc:1;\uintptr_textra_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表


思考:为什么不是一个大表,而是多个表

回答:如果只有一张表,所有对象的引用计数都放到一张表中,则如果在修改某个对象的引用计数的时候,由于对象可能在不同线程中被操作,则需要对表进行加锁,这样一来,效率就会极地。

什么是哈希表

是根据关键码值(Keyvalue)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度,赋值和获取都避免了遍历,提高了效率


SideTable结构

底层源码结构如下:

structSideTable{spinlock_tslock;//自旋锁RefcountMaprefcnts;//引用计数表weak_table_tweak_table;//弱引用表}


可以看到SideTable是由三部分组成

Spinlock_t自旋锁

自旋锁来用来防止操作表结构时可能的竞态条件,适用于轻量访问。比如引用计数的修改

Spinlock_t是“忙等”的锁,对SideTable加锁,避免数据错误

引用计数表RefcountMap

引用计数表也是一个hash表,通过hash函数找到指针对应的引用计数的位置。


弱引用表weak_table_t

弱引用表也是一个hash表,通过hash函数找到对象对应的弱引用数组

底层结构:
structweak_table_t{weak_entry_t*weak_entries;size_tnum_entries;};


标签: