开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情
一、内存五大区
1.堆区(Heap)
堆区是由程序员动态分配和释放的,如果程序员不释放,程序结束后,可能由操作系统回收。
只要用于存放:
OC中使用alloc或者使用new开辟空间创建的对象。C语言中使用malloc、calloc、realloc分配的空间,需要free释放。
优缺点
- 优点:方便灵活,数据适应面广泛。
- 缺点:需要
手动管理,速度慢、容易产生内存碎片。
当需要访问堆中内存时,一般需要先通过对象读取到栈区的指针地址,然后通过指针地址访问堆区。
2.栈区(Stack)
栈是由编译器自动分配并释放的,它是一块连续的内存区域的系统数据结构,遵循先进先出(FILO)原则,其对应的进程或线程是唯一的。
栈一般在运行时分配,向高地址向底地址扩展的数据结构,地址空间在iOS是以0X7开头。
存储
- 栈是由
编译器自动创建和释放的。 - 存储局部变量,一旦离开作用于就会销毁释放
- 存储函数参数,包括隐藏函数,比如
(id self, SEL _cmd)。
优缺点
- 优点:由于栈是由
编译器自动分配并释放的,不会产生内存碎片,不需要手动管理,所以快速高效。 - 缺点:由于是一块连续的内存区域,所以栈的
内存大小有限制,数据不灵活。
iOS主线程大小是1MB,其他线程是512KB;MAC只有8M。实际上我们也可以通过线程的stackSpace去修改,但是成本有些大。
以上内存大小的说明,在Threading Programming Guide中有相关说明
3.全局区 (静态区,即.bss & .data)
全局区是编译时分配的内存空间,在iOS中一般以0X1开头,在程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放。
主要存放:
未初始化的全局变量和静态变量,即BSS区(.bss)。已初始化的全局变量和静态变量,即数据区(.data)。
其中,全局变量是指变量值可以在运行时被动态修改,而静态变量是static修饰的变量,包含静态局部变量和静态全局变量。
4.常量区
一般存储的是 常量-const 0x1
常量区的内存在编译阶段完成分配,程序运行时会一直存在内存中,只有当程序结束后才会由操作系统释放,主要存放已经使用了的,且没有指向的字符串常量。
字符串常量因为可能在程序中被多次使用,所以在程序运行之前就会提前分配内存。
5.代码区
代码区是编译时分配,主要用于存放程序运行时的代码,代码会被编译成二进制存进内存的。
一般存储的是编译生成的二进制代码.
关系图:
iOS中的内存管理方案:
有三种方案: NONPOINTER_ISA , Tagged Pointer , SideTable
二、tagged point
tagged point (64位下的概念)占8个字节,小对象 NSDate NSString NSNumber 也是一个指针 是被打上了 tagged标记的指针 ,是iphone5s后的概念。
NSString *firstString = @"helloworld";
NSString *secondString = [NSString stringWithFormat:@"helloworld"];
NSString *thirdString = @"hello";
NSString *fourthSting = [NSString stringWithFormat:@"hello"];
NSLog(@"%p %@",firstString,[firstString class]);
NSLog(@"%p %@",secondString,[secondString class]);
NSLog(@"%p %@",thirdString,[thirdString class]);
NSLog(@"%p %@",fourthSting,[fourthSting class]);
我们看到NSString在不同方式的创建下,打印的string类型不一样。有这样3种:__NSCFConstantString(0x1 在常量区)、__NSCFString(0x6 在堆区)、NSTaggedPointerString(0xa 栈区)。
[NSString stringWithFormat:@"hello"];在字符小于9的时候,str是一个NSTaggedPointerString对象。
我在看NSTaggedPointerString的指针打印结果(通过如下代码):
uintptr_t ny_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
ny_objc_decodeTaggedPointer(id ptr)
{
uintptr_t value = (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
uintptr_t basicTag = (value >> ny_OBJC_TAG_INDEX_SHIFT) & lg_OBJC_TAG_INDEX_MASK;
value &= ~(lg_OBJC_TAG_INDEX_MASK << lg_OBJC_TAG_INDEX_SHIFT);
value |= ny_objc_obfuscatedTagToBasicTag(basicTag) << lg_OBJC_TAG_INDEX_SHIFT;
return value;
}
NSNumber *number = [NSNumber numberWithInt:7];
NSLog(@"0x%lx",ny_objc_decodeTaggedPointer(fourthSting));
NSLog(@"0x%lx",ny_objc_decodeTaggedPointer(number));
什么是
tagged point?在总是为0的位置大上了标记的指针。
可以看到tagged point 的指针地址直接保存了值信息。
tagged point 它会与初始化的进程随机值进行混淆,intel最低位为1 arm最高位为1 在iOS13后用最低位的3位来标识。 未位010就等于2在objC源码中(fourthSting):
未位011就等于3在objC源码中(NSNumber):
0:char 1:short 2:int 4:float 5:double
三、retain&release 函数
在源码中搜索"objc_retain"然后进入代码:
一步一步跟踪进入:
objc_retain->objc_object::retain()->objc_object::rootRetain()
这里是存储对象的引用计数的三种情况
- 不是nonpointerisa 存储在sidetable
- 是nonpointerisa 存储在extrc_rc并且能够存的下
- 是nonpointerisa 存储在extrc_rc存不下的情况,需要借存储在sidetable中extrc_rc的一半
//直接上objc_object::rootRetain() 核心代码
do {
//不是nonpointerisa -- 存储在sidetable
transcribeToSideTable = false;
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain(sideTableLocked);
}
// 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;
}
}
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // 存得下直接extra_rc++
if (slowpath(carry)) {//是nonpointerisa 存储在extrc_rc存不下的情况
// newisa.extra_rc++ overflowed
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.
if (!tryRetain && !sideTableLocked) sidetable_lock();//锁操作
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;//只保存一半的extra_rc
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
retain核心流程梳理如下:
- 首次进入
rootRetain(tryRetain, variant):参数tryRetain=false,variant=FastOrMsgSend。 - 判断如果是
isTaggedPointer,则直接return this;反之继续3. variant=FastOrMsgSend,执行objc_msgSend(this, @selector(retain)),继续4。- 二次进入
rootRetain(tryRetain, variant):参数tryRetain=false,variant=Fast。 - 判断如果
isa.nonpointer==0,执行sidetable_retain():引用计数全部全部存储在sidetable中;直接根据当前对象找到存储该对象的table,然后找到原有的refcntStorage+=SIDE_TABLE_RC_ONE即可。 - 判断如果
isa.nonpointer==1,应用计数存储在isa.extra_rc和sidetable中。引用计数+1会优先添加到isa.extra_rc上。如果存满了,则先保存一半RC_HALF在extra_rc中,并标记has_sidetable_rc=true已使用引用计数表,处理完isa后更新isa的数据。再将另一半RC_HALF追加到sidetable中,保存到side_table的流程与2同。 retain的最后返回this指针。
源码中release的核心流程objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)中:
release核心流程梳理如下:
- 首次进入
rootRelease(performDealloc, variant):参数performDealloc=true,variant= FastOrMsgSend。 - 判断如果是
isTaggedPointer,则直接return false;反之继续3。 variant=FastOrMsgSend,执行objc_msgSend)(this, @selector(release)),继续4。- 二次进入
rootRelease(performDealloc, variant):参数performDealloc=true,variant= Fast。 - 判断如果
isa.nonpointer==0,执行sidetable_release():引用计数全部存在sidetable中;根据当前的对象找到存储该对象引用计数的table,然后找到原有的refcnt -= SIDE_TABLE_RC_ONE;即可。满足dealloc条件的,继续执行dealloc流程。 - 判断如果
isa.nonpointer==1,应用计数存储在isa.extra_rc和sidetable中。引用继续-1会先从isa.extra_rc上减。如果不够减了,会进入underflow流程7. - 判断如果该对象有
has_sidetable_rc,执行rootRelease_underflow流程,三次进入rootRelease(performDealloc, variant):参数performDealloc=true,variant= Full。 - 执行
auto borrow = sidetable_subExtraRC_nolock(RC_HALF);也就是问sidetable借RC_HALF,返回借到的数量和剩余的数量。 - 如果借到了则将借到的数量-1保存到
isa.extrac_rc中。如果sidetable中剩余为0则标记isa.has_sidetable_rc=0,再存储新的isa.bits的数据。处理存储失败的情况。 - 如果没有借到或者根本就没有再sidetable中存储则执行
dealloc相关流程。
四、dealloc流程
源码中release的核心流程objc_object::rootDealloc()中:
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&//是否是nonpointer
!isa.weakly_referenced &&//是否是弱引用
!isa.has_assoc &&//是否有关联对象
#if ISA_HAS_CXX_DTOR_BIT
!isa.has_cxx_dtor &&//是否有cxx析构函数
#else
!isa.getClass(false)->hasCxxDtor() &&
#endif
!isa.has_sidetable_rc))//是否有sidetable
{
assert(!sidetable_present());
free(this);//直接释放内存
}
else {
object_dispose((id)this);//上述条件不成立,执行object_dispose
}
}
rootDealloc函数:先判断了是否TaggedPointer?是直接返回。接着判断(是否是nonpointer,是否是弱引用,是否有关联对象,是否有cxx析构函数,是否有sidetable)如果都没有的情况就执行free(this)直接释放内存。
上述条件不成立,执行object_dispose函数:
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
//进入objc_destructInstance(obj);
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);//arc模式下释放成员变量
if (assoc) _object_remove_assocations(obj, /*deallocating*/true);//移除关联对象
obj->clearDeallocating();
}
return obj;
}
//进入clearDeallocating();
inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {//是否是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());
}
dealloc核心流程梳理如下:
- 先判断了如果是否
TaggedPointer?是直接返回。 - 如果是一个
nonpointerISA而且没有弱引用,没有关联对象,没有cxx析构函数,没有散列表引用计数 就直接释放。 - 如果是一个
nonpointerISA,并且不是第二步的情况。就执行objc_destructInstance函数。 - 删除关联对象,调用C++析构函数,清空弱引用表里面的数据,清空散列表里面的引用计数的信息。
五、总结
内存五大区:堆区(Heap),栈区(Stack),全局区 (静态区,即.bss & .data),常量区,代码区。tagged point:可以看到tagged point的指针地址直接保存了值信息。 tagged point 它会与初始化的进程随机值进行混淆,intel最低位为1 arm最高位为1 在iOS13后用最低位的3位来标识。retain&release 函数:- retain核心流程:(1)如果是
isTaggedPointer直接返回,(2)是nonpointerisa 存储在extrc_rc并且能够存的下,(3)是nonpointerisa 存储在extrc_rc存不下的情况,需要借存储在sidetable中extrc_rc的一半 - release核心流程:(1)如果是
isTaggedPointer直接返回,(2)如果是nonpointerisa会在isa.extra_rc上减,如果不够减了或者到0从sidetable中取。(3)如果isa.extrac_rc=0了,isa.has_sidetable_rc=0了,则执行dealloc相关流程。
- retain核心流程:(1)如果是
dealloc流程:
(1)如果是isTaggedPointer直接返回,
(2)是nonpointerISA而且没有弱引用,没有关联对象,没有cxx析构函数,没有散列表引用计数 就直接释放。
(3)是nonpointerISA并不是2情况,删除关联对象,调用C++析构函数,清空弱引用表里面的数据,清空散列表里面的引用计数的信息。