欢迎阅读iOS底层系列(建议按顺序)
本文主要说明iOS
的内存优化方案,从底层探索系统优化内存的方式等。
1. ROM和RAM
ROM
是只读存储器
,是内部存储器的一种。它用来存储手机系统文件、图片、软件等等,不会随着掉电而丢失数据,ROM
越大存储的数据就越多。
RAM
是随机存取存储器
,是内部存储器最重要的一种,我们常称为运行内存
(物理内存地址)。它的运行速度是比较快的,什么时候需要数据,就从ROM
读取数据加入内存,但同时RAM
断电会丢失数据,所以手机断电了会丢失原来正在运行的数据。RAM
内存越大,能同时执行的程序就越多,性能一般是越好的。
我们常说的内存管理,内存优化,指的是RAM
。
2.内存分区
-
内核区
:内核模块使用的区域。一般4GB的设备,系统会使用1GB留给内核区。 -
栈区
:从高地址向低地址延伸,所以汇编中开辟栈空间使用sub
指令,且地址空间是连续的。它用来存储局部变量
,函数跳转时的现场保护
,多余参数
等。它是由系统管理的,在app
启动时就确定了大小,压栈超过固定大小会报栈溢出错误。所以大量的局部变量,深递归可能耗尽栈内存而造成程序崩溃。 -
堆区
:从低地址向高地址延伸,系统使用链表来管理此空间,所以它的地址空间是不连续的。堆的空间比较大且是动态变化的, 一般由程序员管理。 -
全局区
:初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统自动释放。 -
常量区
:存放一些常量字符串,程序结束后由系统自动释放。 -
代码区
:存放编译后的代码数据。 -
保留区
:系统保留区域
其中,代码区
,常量区
,全局区
在APP启动时地址已固定,因此不会因为这些区的指针为空而产生崩溃性的错误。而堆的创建销毁,栈的压栈出栈,导致堆栈的空间时刻都在变化,所以当使用一个指针指向这两个区里面的内存时,一定要注意内存是否已经被释放,否则会产生程序崩溃。
那为什么栈区的速度比堆区快?
因为当访问一个常规对象时,堆栈都参与了工作,需要先找到栈区存储的指向对象地址的指针,根据指针在找到堆区的对象。
而栈区的数据是直接通过寄存器访问的,所以栈区的速度比堆区快。
3.系统内存管理方案
内存是所有程序都使用的重要系统资源,系统一定会采取诸多的内存管理方案来优化内存。比如VM
,NONPOINTER_ISA
,TaggedPointer
,ARC
,autoreleasePool
等。
3.1 虚拟内存(VM)
在早期,程序是被完整的加载到物理内存,后来渐渐意识到这种做法的弊端:
-
内存地址是连续的,黑客可以很轻松的从一个进程地址获取到其他进程的地址
-
每个时刻只会使用到程序的一小部分内存,可是却把全部内存预先加载,明显存在内存浪费
为了解决这些严重的问题,就引入了虚拟内存的概念:
虚拟内存允许操作系统摆脱物理
RAM
的限制。虚拟内存管理器创建一个虚拟地址空间,然后将其划分为大小统一的内存块,称为页数。处理器及其内存管理单元(MMU)维护一个页面表,将程序虚拟地址空间中的页面映射到计算机RAM
中的硬件地址。当程序的代码访问内存中的地址时,MMU
使用页表将指定的虚拟地址转换为实际的硬件内存地址。该转换自动发生,并且对于正在运行的应用程序是透明的。
虚拟内存 -> 映射表 -> 物理内存
映射需要的地址
简单来说,程序被预先加载到虚拟内存空间,当程序的虚拟内存地址被访问时,MMU
使用页表将这个虚拟地址映射到物理地址,保证程序的正常运行。
通过引入虚拟内存,被使用的数据才会被映射到物理内存,不仅解决了内存浪费的问题,也解决了物理地址连续易破解的问题。
但在虚拟内存中地址依然是连续的,可以查看编译后在macho
中的虚拟内存地址是确定且连续的。
连续的地址始终是不安全的,苹果为了解决这个问题,又引入了ASLR
的概念。
- ASLR(Address space layout randomization):地址空间布局随机化,虚拟地址在内存中会发生的偏移量。增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。
在app
启动后,苹果在原先虚拟内存的基础上,又加上了一个随机的地址值,且每次启动不一致,保证了内存地址无法被直接定位。
这里存在两个虚拟地址的概念:
1.编译后的虚拟地址:就是
macho
文件中固定的地址2.运行后的虚拟地址:
macho
文件中固定的地址加上ASLR
,就是运行后调试输出的地址
3.2 NONPOINTER_ISA
关于NONPOINTER_ISAd
的优化在包含万象的isa中已经说明,这里直接给出结论:
isa
是串联对象,类,元类和根元类的重要线索,采用联合体加位域
的数据结构使有限的空间充分利用,存储了丰富的信息
isa
的底层结构:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
因为联合体
内部的的元素在内存中是互相覆盖的,isa
采用联合体的特点优化了内存;
而且类中有大量的信息需要记录,如果都额外声明属性存储,需要不少内存,NONPOINTER_ISA
采用了位域
的形式,使用每个二进制位分别定义存储的内容。
isa
做为对象和类的固有属性,在内存中是大量存在的,任何一点优化带来的价值是很可观的。
关于isa
存储的内容:
nonpointer
:表示是否对 isa 指针开启指针优化(0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等)
has_assoc
:关联对象标志位(0没有,1存在)
has_cxx_dtor
:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象
shiftcls
:存储类指针的值。开启指针优化的情况下,在arm64架构下有33位用来存储类指针
magic
:用于调试器判断当前对象是真的对象还是没有初始化的空间
weakly_referenced
:对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放。
unused(之前是deallocating)
:标志对象是否正在释放内存
extra_rc
:当表示该对象的引⽤计数值,实际上是引⽤计数值减 1,例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到下⾯的 has_sidetable_rc。
has_sidetable_rc
:当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位
其中nonpointer
,weakly_referenced
,unused
,extra_rc
,has_sidetable_rc
都和内存息息相关。
3.3 TaggedPointer
关于TaggedPointer
在包含万象的isa中也做过介绍:
早期64位系统时,当我们存储基础数据类型 , 底层封装成
NSNumber
对象 , 也会占用8字节内存 , 32位机器占用4字节。为了存储和访问一个NSNumber
对象,需要在堆上分配内存,另外还要维护它的引用计数,管理它的生命期 。这些都给程序增加了额外的逻辑,造成运行效率上的损失 。因此如果没有额外处理 , 会造成很大空间浪费 .因此苹果引入了TaggedPointer
,当对象为指针为TaggedPointer
类型时,指针的值不是地址了,而是真正的值,直接优化了存储,提升了获取速度。
TaggedPointer
的特点:
-
专门用来存储小对象,例如
NSNumber
和部分NSString
-
指针不再存储地址,而是直接存储对象的值(异或后的值)。所以,它不是一个对象,而是一个伪装成对象的普通变量。内存不在堆,而是在栈,由系统管理,不需要
malloc
和free
-
不需要处理引用计数,少了
retain
和release
的流程 -
在内存读取上有着3倍的效率,创建时比以前快106倍。(少了
malloc
流程,获取时直接从地址提取值)
在
iOS10.14
以下的版本,TaggedPointer
的地址存储着真正的值,这对于攻击者来说和明文没有区别。因此iOS10.14
之后,苹果加入了TaggedPointerObfuscator
(混淆器)的概念,TaggedPointer
的指针存储的值就不是原始的值的了。
3.4 ARC
关于ARC
只要牢记这几个规则:
- 自己生成的对象,自己持有
- 非自己生成的对象,自己也能持有
- 不再需要自己持有的对象时释放
- 非自己持有的对象无法释放
ARC
的其他相关部分大家已经足够熟悉,不再赘述,后面只会从相关的源码进行解读。
3.5 autoreleasePool
自动释放池
提供了一种机制:可以放弃对象的所有权,但又避免其被提早释放的可能性。
默认情况下,自动释放池在runloop
的一个迭代周期结束时,会自动释放这个周期所对应的哨兵对象的指针之后的所有对象。
大部分情况下,默认的情况足以保证内存的合理分配,但是某些特殊时刻,比如一个周期产生大量的临时对象,会产生内存峰值,这时候就需要手动添加自动释放池
了。
手动添加的自动释放池,在自动释放池的作用域结束时,就会销毁其中产生的对象。这个时间点是快于runloop
的一个迭代周期,也就减少了峰值内存产生的可能。
4.相关源码探索
简单介绍了几种内存优化机制,下面从源码的角度看看它们相关实现。
4.1 TaggedPointer相关
之前在类的加载分析中,分析过libobjc
会从dyld
那边接手map_images
,load_images
,unmap_image
三件事,其中map_images
时会来到_read_images
这个重要函数,
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses){
...
initializeTaggedPointerObfuscator();
...
}
省略处的代码分析过,直接来看initializeTaggedPointerObfuscator()
iOS10.14
之前,objc_debug_taggedpointer_obfuscator
默认为0
,之后等于一个随机数。
然后系统对TaggedPointer
的赋值和获取采用了较简单且快速的方式:异或同一个数。
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;
}
赋值时异或生成的随机数,获取时在异或这个随机数。两次异或后就会得到原来的值。
4.2 ARC
引用计数是如果存储的?
为什么优先存
isa
?
retain
如何处理引用计数的?
release
如何处理引用计数的?
alloc
的对象引用计数是多少?什么时候调用
dealloc
,流程是怎样的?
以上常见的面试题可以从源码的角度一一找到答案。本文使用的源码版本是objc4-818.2
4.2.1 retain
跟一下retain
的调用,(跟踪流程比较简单就跳过了,可以直接搜下面关键字)
retain -> _objc_rootRetain -> rootRetain
,
最后会来到rootRetain
:
objc_object::rootRetain()
{
return rootRetain(false, RRVariant::Fast);
}
传入的参数会影响处理流程,需要注意下。
其中,rootRetain
传入的tryRetain
是false
,variant
传入的是Fast
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
//TaggedPointer拦截
if (slowpath(isTaggedPointer())) return (id)this;
//散列表是否加锁
bool sideTableLocked = false;
//是否转录到散列表
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
//获取isa
oldisa = LoadExclusive(&isa.bits);
//variant不等于FastOrMsgSend所以不会走
if (variant == RRVariant::FastOrMsgSend) {
///它们在这里是为了避免重新加载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));
}
}
//不是nonpointer的isa时
if (slowpath(!oldisa.nonpointer)) {
//isa是指向元类时
if (oldisa.getDecodedClass(false)->isMetaClass()) {
ClearExclusive(&isa.bits);
return (id)this;
}
}
//dowhile处理引用计数
do {
transcribeToSideTable = false;
newisa = oldisa;
//不是nonpointer的isa时
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
//tryRetain是false,所以走sidetable_retain直接存散列表中的引用计数表
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain(sideTableLocked);
}
//是否正在析构,都在析构了,还想retain我?
if (slowpath(newisa.isDeallocating())) {
ClearExclusive(&isa.bits);
if (sideTableLocked) {
ASSERT(variant == RRVariant::Full);
sidetable_unlock();
}
if (slowpath(tryRetain)) {
return nil;
} else {
return (id)this;
}
}
/*
到这里说明是NONPOINTER_ISA了
*/
//carry 进位标识,表示是否溢出
uintptr_t carry;
//isa对应的表示引用计数的位extra_rc做加1操作,同时返回是否carry
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
//如果carry,说明isa对应的表示引用计数的位装满了,溢出了
if (slowpath(carry)) {
//传入的RRVariant是Fast,所以走rootRetain_overflow
if (variant != RRVariant::Full) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
//rootRetain_overflow传入是RRVariant是Full,就间接来到这里
if (!tryRetain && !sideTableLocked) sidetable_lock();
//散列表需要加锁,需要转录到散列表,
sideTableLocked = true;
transcribeToSideTable = true;
//isa的extra_rc位存储的值拿走一半,isa的has_sidetable_rc位开启
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
//当发生进位溢出时,走sidetable_addExtraRC_nolock,isa的extra_rc位存储的值存进去一半
if (variant == RRVariant::Full) {
if (slowpath(transcribeToSideTable)) {
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
} else {
ASSERT(!transcribeToSideTable);
ASSERT(!sideTableLocked);
}
//处理完引用计数返回自己
return (id)this;
}
-
如果不是
nonpointer
的isa
,引用计数直接存引用计数表 -
是
nonpointer
的isa
时,如果isa
的指向正在析构,就不需要处理引用计数 -
如果不再析构,
isa
的引用计数的位做加1操作,同时返回是否carry
-
如果
carry
,调用rootRetain_overflow
把一半存储到引用计数表
分析其中几个比较重要的地方:
- sidetable_retain
id
objc_object::sidetable_retain(bool locked)
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
//根据当前对象在SideTables取出对应的SideTable
SideTable& table = SideTables()[this];
//加锁
if (!locked) table.lock();
//得到已经存储的引用计数
size_t& refcntStorage = table.refcnts[this];
//如果引用计数没有越界,还够存储
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
//对应的引用计数的的位做加1操作
refcntStorage += SIDE_TABLE_RC_ONE;
}
//解锁
table.unlock();
return (id)this;
}
总的来看,sidetable_retain
是对散列表中的引用计数表做加1
操作
先根据当前对象从SideTables
取出SideTable
,那为什么不把所有对象都存在一张全局的SideTable
,而是需要SideTables
来获取对应的SideTable
呢?看下SideTable
的结构:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
}
SideTable
有三个属性:
slock
,锁refcnts
,引用计数表weak_table
,weak
表
因为每次操作SideTable
都需要加锁解锁,如果所有的对象都存在一张全局的SideTable
,每次增删改查时都需要操作这一整张大表,后续的操作都要等待前一次操作的完成,非常影响性能。
所以苹果使用一张SideTables
存储64张SideTable
的形式,每个对象通过hash找到自己的SideTable
进行操作,以空间换时间的方式提高了性能。
- RC_ONE
# define ISA_BITFIELD \
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 unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
//1+1+1+33+6+1+1+1 = 45
# define RC_ONE (1ULL<<45)
RC_ONE
根据不同的架构做位移,虽然各种架构的值不同,但最终都是得到isa
的extra_rc
所在的位
- rootRetain_overflow
objc_object::rootRetain_overflow(bool tryRetain)
{
//再次调用rootRetain,标识为Full
return rootRetain(tryRetain, RRVariant::Full);
}
rootRetain_overflow
再次调用rootRetain
,只是改变了RRVariant
的标识为Full
,保证内部来到sidetable_addExtraRC_nolock
做溢出时存值到引用计数表的操作。
retain
如何处理引用计数 总结:
如果
retain
的对象是TaggedPointer
,retain
的对象isa
指向是元类不作处理。如果是普通的isa
,引用计数只存引用计数表中,如果是NONPOINTER_ISA
,如果retain
的对象正在析构不作处理,否则引用计数先存isa
的extra_rc
中,如果extra_rc
存满了,拿一半存引用计数表中,依次反复。
4.2.2 release
明白了retain
的流程,release
也就好理解了。
跟一下release
的调用,
release -> _objc_rootRelease -> rootRelease
,
最后会来到rootRelease
:
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
//TaggedPointer拦截
if (slowpath(isTaggedPointer())) return false;
//散列表是否加锁标识
bool sideTableLocked = false;
isa_t newisa, oldisa;
//获取isa
oldisa = LoadExclusive(&isa.bits);
//RRVariant等于FastOrMsgSend时才走,传的是Fast所以不走
if (variant == RRVariant::FastOrMsgSend) {
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;
}
}
//不是NONPOINTER_ISA时
if (slowpath(!oldisa.nonpointer)) {
//判断是不是元类,元类就不处理了
if (oldisa.getDecodedClass(false)->isMetaClass()) {
ClearExclusive(&isa.bits);
return false;
}
}
retry:
//dowhile处理引用计数
do {
newisa = oldisa;
//不是NONPOINTER_ISA时,说明isa没有存储引用计数
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
//直接调用sidetable_release减少sidetable中引用计数表的引用计数
return sidetable_release(sideTableLocked, performDealloc);
}
//如果正在析构,release没有意义,不处理
if (slowpath(newisa.isDeallocating())) {
ClearExclusive(&isa.bits);
//如果表被锁了,就解锁
if (sideTableLocked) {
ASSERT(variant == RRVariant::Full);
sidetable_unlock();
}
return false;
}
//到这里说明是NONPOINTER_ISA了
//carry 进位标识,表示是否溢出
uintptr_t carry;
//isa对应的表示引用计数的位extra_rc做减1操作,同时返回是否carry,这里的进位是指isa中的引用计数减完时
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
if (slowpath(carry)) {
// don't ClearExclusive()
//发生溢出时,跳转underflow分支
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
//isa存在析构标识时,跳转deallocate分支
if (slowpath(newisa.isDeallocating()))
goto deallocate;
if (variant == RRVariant::Full) {
if (slowpath(sideTableLocked)) sidetable_unlock();
} else {
ASSERT(!sideTableLocked);
}
return false;
//underflow分支
underflow:
newisa = oldisa;
//判断isa的has_sidetable_rc位是否使用,使用说明引用计数表里有引用计数
if (slowpath(newisa.has_sidetable_rc)) {
//RRVariant不是Full时
if (variant != RRVariant::Full) {
ClearExclusive(&isa.bits);
//调用rootRelease_underflow,再走一次rootRelease流程,RRVariant标识传Full
return rootRelease_underflow(performDealloc);
}
//再走一次的rootRelease流程时来到这里,下面的操作就是isa中的引用计数减完时,把引用计数表中的引用计数转移到isa中
//如果sideTable没加锁,需要对sideTable加锁,重新开始获取isa以避免指针转换,然后再去retry分支
if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
oldisa = LoadExclusive(&isa.bits);
goto retry;
}
// 尝试从引用计数表中借一些引用计数
auto borrow = sidetable_subExtraRC_nolock(RC_HALF);
//如果借完剩下是0,标识清除引用计数表
bool emptySideTable = borrow.remaining == 0;
//如果借出来的引用计数大于0,
if (borrow.borrowed > 0) {
//是否过渡释放的标识
bool didTransitionToDeallocating = false;
//isa中存借出来的引用计数-1
//为什么需要减1,看下上面给的extra_rc位的定义就知道了
newisa.extra_rc = borrow.borrowed - 1;
//has_sidetable_rc位是否开启取决于引用计数表中是否还有引用计数
newisa.has_sidetable_rc = !emptySideTable;
//StoreReleaseExclusive内部调用__c11_atomic_compare_exchange_weak
//当前值与期望值相等时,修改当前值为设定值,返回true
//当前值与期望值不等时,将期望值修改为当前值,返回false
//isa更新是否成功标识
bool stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
//更新失败且是NONPOINTER_ISA时,往isa中回填引用计数
if (!stored && oldisa.nonpointer) {
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();
}
}
}
//更新失败且不是NONPOINTER_ISA时,把引用计数放回引用计数表,再走retry分支
if (!stored) {
ClearExclusive(&isa.bits);
sidetable_addExtraRC_nolock(borrow.borrowed);
oldisa = LoadExclusive(&isa.bits);
goto retry;
}
//走到这里,就是从引用计数表借值后减量成功
//如果emptySideTable为空,就清除引用计数表
if (emptySideTable)
sidetable_clearExtraRC_nolock();
if (!didTransitionToDeallocating) {
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
}
}
else {
}
}
//deallocate分支
deallocate:
ASSERT(newisa.isDeallocating());
ASSERT(isa.isDeallocating());
if (slowpath(sideTableLocked)) sidetable_unlock();
//在同一个线程(函数)中执行的线程和信号处理程序之间的栅栏,dealloc要一个个来
__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
//来到deallocate分支时,发送dealloc消息
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
其中几个关键的地方说明下:
- sidetable_release
uintptr_t
objc_object::sidetable_release(bool locked, bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
//依然是在SideTables中找到对应SideTable
SideTable& table = SideTables()[this];
//是否要做dealloc操作的标识符
bool do_dealloc = false;
//加锁
if (!locked) table.lock();
//判断该引用计数是否为引用计数表中的最后一个,如果是则表示release后需要执行dealloc清除对象所占用的内存
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) {
do_dealloc = true;
refcnt |= SIDE_TABLE_DEALLOCATING;
} else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
refcnt -= SIDE_TABLE_RC_ONE;
}
table.unlock();
//发送dealloc消息
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return do_dealloc;
}
- rootRelease_underflow
objc_object::rootRelease_underflow(bool performDealloc)
{
//再次调用rootRelease,标识为Full
return rootRelease(performDealloc, RRVariant::Full);
}
release
如何处理引用计数总结:
如果
release
的对象是TaggedPointer
,release
的对象isa
指向是元类不作处理。如果是普通的isa
,从引用计数表中删除引用计数,删完了发送dealloc
消息;如果是NONPOINTER_ISA
,如果release
的对象正在析构不作处理,否则先从isa
中删除引用计数,不够删时,从引用计数表取出来到isa
中删除,两者都删完时发送dealloc
消息。
4.2.3 retainCount
alloc
的对象引用计数是多少?
现在来分析下引用计数是如何计算的。
跟一下retainCount
的调用,
retainCount -> _objc_rootRetainCount -> rootRetainCount
最后会来到rootRetainCount
:
需要注意的是,
objc4-818.2
的源码和objc4-7
系列rootRetainCount
实现有比较大的区别,下面给出两个版本的源码做对比避免入坑
4.2.3.1 objc4-818.2
inline uintptr_t
objc_object::rootRetainCount()
{
//TaggedPointer拦截
if (isTaggedPointer()) return (uintptr_t)this;
//加锁
sidetable_lock();
//获取isa
isa_t bits = __c11_atomic_load((_Atomic uintptr_t *)&isa.bits, __ATOMIC_RELAXED);
//如果是NONPOINTER_ISA
if (bits.nonpointer) {
//引用计数等于extra_rc存的值
uintptr_t rc = bits.extra_rc;
//如果has_sidetable_rc位有值,说明引用计数表有存储
if (bits.has_sidetable_rc) {
//引用计数加上引用计数表中存的值
rc += sidetable_getExtraRC_nolock();
}
//解锁
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
当是NONPOINTER_ISA
时,引用计数的值等于isa
中extra_rc
的值加上引用计数表中存的值,不是NONPOINTER_ISA
时,来到sidetable_retainCount
uintptr_t
objc_object::sidetable_retainCount()
{
//依然是SideTables找到对应的SideTable
SideTable& table = SideTables()[this];
//至少为1
size_t refcnt_result = 1;
//加锁
table.lock();
//迭代累加获取引用计数
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
// this is valid for SIDE_TABLE_RC_PINNED too
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
//解锁
table.unlock();
return refcnt_result;
}
如果是普通的isa
时,引用计数只等于引用计数表中的值。
4.2.3.2 objc4-781
inline uintptr_t
objc_object::rootRetainCount()
{
//TaggedPointer拦截
if (isTaggedPointer()) return (uintptr_t)this;
//加锁
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
//如果是NONPOINTER_ISA
if (bits.nonpointer) {
//引用计数为extra_rc的值 + 1
uintptr_t rc = 1 + bits.extra_rc;
////如果has_sidetable_rc位有值,说明引用计数表有存储
if (bits.has_sidetable_rc) {
//引用计数加上引用计数表中存的值
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
//源码一样
return sidetable_retainCount();
}
当是NONPOINTER_ISA
时,引用计数的值等于isa
中extra_rc
的值加---1---
在加上引用计数表中存的值,不是NONPOINTER_ISA
时,来到sidetable_retainCount
4.2.3.3 读二者区别引发的思考
有不少面试题会问 alloc的对象引用计数是多少。
这题乍看简单,但实际是有坑的。
先分析二者最本质的区别:
在于
NONPOINTER_ISA
时,是否有手动加1
的操作。
因为这个操作会直接决定了 alloc的对象引用计数是多少 的答案!
如果是老版本的源码,alloc的对象引用计数是
0
如果是新版本的源码,alloc的对象引用计数是
1
关于alloc
的对象引用计数是0
,相信不少同学也看过相关文章里面有这个结论:
alloc
后当获取引用计数时,引用计数打印1,这只是因为调用retainCount
时,内部手动做了个+1
的操作,extra_rc
中实际存储的是0
,所以对象实际的引用计数是0
。
所以早期的文章说alloc的对象引用计数是0是正确的。
那新版的retainCount
为什么不需要加1呢?既然这里不加1
,那可能是alloc
时就做了操作。于是我回头看了新版的alloc
流程,发现在initIsa
多了一个操作:
果然,新版的alloc
在开辟空间时就把extra_rc
赋值为1
。感兴趣的同学可以看下旧版的alloc
流程确认下有没有这个赋值。
所以现在alloc的对象引用计数是1才是正确答案!
4.2.4 dealloc
什么时候调用dealloc,流程是怎样的?
现在来分析下这个面试常见题。
根据release
的流程已经得出,无论是哪种isa
,引用计数为0
时都会发送dealloc
消息。
至于流程,直接来到dealloc
的源码:
dealloc -> _objc_rootDealloc -> rootDealloc
inline void objc_object::rootDealloc()
{
//TaggedPointer拦截
if (isTaggedPointer()) return;
//是NONPOINTER_ISA时
//weakly_referenced,has_assoc,has_cxx_dtor,has_sidetable_rc都不存在直接free
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
#if ISA_HAS_CXX_DTOR_BIT
!isa.has_cxx_dtor &&
#else
!isa.getClass(false)->hasCxxDtor() &&
#endif
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
//其中之一存在,就走object_dispose
object_dispose((id)this);
}
}
TaggedPointer
拦截,不作处理- 如果是
NONPOINTER_ISA
时,且弱引用表,关联对象表,c++析构函数,引用计数表都没有使用到这个对象,直接free
快速释放对象 - 其中之一存在时,走
object_dispose
慢速释放
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
调用objc_destructInstance
后,free
释放对象
void *objc_destructInstance(id obj)
{
if (obj) {
//是否存在c++析构标识
bool cxx = obj->hasCxxDtor();
//是否有添加到关联对象表标识
bool assoc = obj->hasAssociatedObjects();
//存在,调用c++析构函数
if (cxx) object_cxxDestruct(obj);
//存在,从关联对象表中删除
if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
obj->clearDeallocating();
}
return obj;
}
- 获取是否存在
c++
析构标识和添加到关联对象表标识 - 哪个存在,就调用
c++
析构函数或者从关联对象表中删除 - 调用
clearDeallocating
inline void objc_object::clearDeallocating()
{
//是普通的isa,调用sidetable_clearDeallocating
if (slowpath(!isa.nonpointer)) {
sidetable_clearDeallocating();
}
//是NONPOINTER_ISA,且weakly_referenced或has_sidetable_rc存在时,调用clearDeallocating_slow
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
clearDeallocating_slow();
}
assert(!sidetable_present());
}
- 是普通的
isa
,调用sidetable_clearDeallocating
- 是
NONPOINTER_ISA
,且weakly_referenced
或has_sidetable_rc
存在时,调用clearDeallocating_slow
sidetable_clearDeallocating
和clearDeallocating_slow
的处理是一样的,只是因为isa
类型不同,获取标志位的方式也不同,所以写成了两个函数表示,所以只看多数情况下的isa
处理
NEVER_INLINE void objc_object::clearDeallocating_slow()
{
ASSERT(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
//依然是从SideTables获取对应的SideTable
SideTable& table = SideTables()[this];
//加锁
table.lock();
//如果有weak标识
if (isa.weakly_referenced) {
//清空weak表中对应的entry数组
weak_clear_no_lock(&table.weak_table, (id)this);
}
//如果有has_sidetable_rc标识
if (isa.has_sidetable_rc) {
//清空引用计数表
table.refcnts.erase(this);
}
table.unlock();
}
- 从
SideTables
获取对应的SideTable
,然后加锁 - 如果有
weak
标识,清空weak
表中对应的entry
数组,entry
数组中存在所有对该对象的弱引用 - 如果有
has_sidetable_rc
标识,抹去引用计数表,注意是引用计数表,而不是引用计数(表都删了也不需要处理数据了)
dealloc流程总结:
如果
TaggedPointer
类型,不作处理。如果对象的弱引用,关联对象,c++
析构函数,引用计数标识都不存在,直接释放。否则存在c++
析构函数标识就调用c++
析构函数,存在关联对象标识就从关联对象表中删除对象,存在弱引用标识就从弱引用表中删除对应的数组,存在引用计数标识就抹去对应的引用计数表。最后释放对象。
骑枪弓策剑
4.2.5 剩余面试题解答
根据以上所有的流程分析,还剩下的两道面试题也就有了答案:
- 引用计数是如果存储的?
如果不是
NONPOINTER_ISA
,引用计数只存在散列表中的引用计数表中;如果是NONPOINTER_ISA
,引用计数优先存储在isa
的extra_rc
位中,存满后,在使用引用计数表存储。
- 为什么优先存
isa
中?
引用计数存入引用计数表时,需要加锁解锁,
hash
查找一系列过程,而存入isa
时,只需要位操作改变对应的值,所以优先存isa
中效率更高。
5.内存管理之循环引用和强引用
以下代码是否会引起循环引用?
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
很明显,答案是会。那继续追问:
以下方式可以解决循环引用嘛?
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
大部分同学看到__weak
关键字第一反应就会觉得__weak
可以解决循环引用。
其实,__weak
可以用来解决block
带来的循环引用,但是timer
的却不行。这是为什么?
看下关于timer
的api
的官方文档:
译: 当定时器触发时将Selector指定的消息发送到的对象。计时器保持对该对象的强引用,直到它(计时器)失效。
target
会对传入的参数进行一次强引用。那强引用为什么不能使用__weak
解决呢?
要搞清楚这个问题,先看下相关打印:
很明显,self
和weakSelf
指向的是同一个地址空间,也就是同一个对象,timer
操作weakSelf
实际上就是操作self
,所以使用weak
解决timer
的循环引用是无效的。
那为什么block
的循环引用解决不存在这个这个问题呢?继续打印:
虽然self
和weakSelf
指向的地址相同,但它们自身的指针地址是不同的。
看下block
捕获对象的源码:
block
捕获对象的不同之处在于:
block
捕获的是**
类型,也就是捕获指向传入对象destArg
的指针地址。
所以,block
持有的是weakself
的指针地址,不是指向的对象本身,而timer
持有的是对象本身,这造成二者循环引用有本质的区别的。
至于timer
的解决方案也比较多,不是本文重点,这里只推荐使用NSProxy
的方案,有兴趣的同学可以自行研究下。
6.内存相关的代码面试题
1.内存分区之全局静态变量面试题
关于全局静态变量
有个相关的面试题:
定义一个值为100
的全局静态变量,如下操作后输出:
//直接打印主类的personNum,对应第一次打印
NSLog(@"ViewController内部,&personNum = %p--personNum = %d\n",&personNum,personNum);
personNum = 10000;
//修改personNum等于10000后打印,对应第二次打印
NSLog(@"ViewController内部,&personNum = %p--personNum = %d\n",&personNum,personNum);
//内部对personNum做加1操作,内部也打印personNum,对应第三次打印
[[CJPerson new] add];
//打印personNum,对应第四次打印
NSLog(@"ViewController内部,&personNum = %p--personNum = %d\n",&personNum,personNum);
//内部无特殊操作,直接打印personNum,对应第五次打印
[[CJPerson alloc] cate_method];
—————————————————————————————————————————————
-
第一次输出为
100
,地址为0x10ea9d338
-
第二次输出为
10000
,地址为0x10ea9d338
-
第三次输出为
101
,地址为0x10ea9d328
-
第四次输出为
10000
,地址为0x10ea9d338
-
第五次输出
100
,地址为0x10ea9d33c
—————————————————————————————————————————————
由①到②,可得:全局静态变量是可以被修改的。
由②到③,可得:生成新的全局静态变量地址,修改只针对自身文件的全局静态变量有效。
由③到④,可是:其他文件的全局静态变量修改不对自身文件的产生影响。
由④到⑤,可得:分类也是新文件,也会生成新的全局静态变量地址。
由以上可得总结:
全局静态变量的值是可以被
修改
的,全局静态变量的值只针对文件
有效,每个文件都生成自己的变量。
2.TaggedPointer面试题
以下两段代码执行后分别有什么结果?
self.queue = dispatch_queue_create("com.juejin.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"juejin"];
NSLog(@"%@",self.nameStr);
});
}
self.queue = dispatch_queue_create("com.juejin.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"juejin_iOS底层--内存管理分析"];
NSLog(@"%@",self.nameStr);
});
}
第一段正常执行,第二段执行的过程中崩溃。
为什么代码看似简单,内容也基本一样,结果却不一样?
因为这题其实还涉及了多线程下setter
的使用。
先说下第二种崩溃的原因如下:
setter
方法主要做的事是对newvalue
进行retian
,对oldvalue
进行realase
,在多线程的环境中,对写操作没有加锁,那么一定会有多条线程竞争使用资源。某一时刻,当某条线程已经realase
了旧值,另一条线程这时候也开始setter
,对已经释放的值进行retain
,这也就导致了程序的崩溃。
而第一种情况的特殊之处在于:
因为nameStr
的长度较短,系统把它设为TaggedPointer
对象,虽然也是处于多线程未加锁竞争资源的条件下,但因为realase
和retain
在操作时,第一步就对TaggedPointer
进行了拦截,自然也就不会奔溃了。
ALWAYS_INLINE id
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
if (slowpath(isTaggedPointer())) return false;
...
}
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
if (slowpath(isTaggedPointer())) return (id)this;
...
}
7.写在后面
以上是对iOS
内存相关的一些分析,而内存分析远不止于此,比如autoreleasePool
也是内存优化的重要组成部分。但因为担心行文过长影响阅读,就到此为止吧。
所以下一章是autoreleasePool
的底层分析。
敬请关注。