1、内存分布
当程序运行时,系统会开辟 内核区、程序使用的内存五大区和保留区
1.1、数据类型
-
值类型
- 基本数据类型
- 存入栈
- 无需内存管理
-
引用类型
- 继承自NSObject的
- 存入堆
- 需要内存管理
1.2 内核区
- 操作系统分为两种运行级别,分别是内核态与用户态,当程序运行,系统会分配出
4G虚拟内存,其中内核区占用1G,用于进行内核处理操作的区域,剩余3G,会预留给程序使用的内存五大区和保留区
1.3、内存5大区
- 堆区 Heap
- OC 中,使用
alloc或new创建的对象 - C 中,使用
malloc、calloc、realloc开辟的空间,需要手动调用free函数对其释放 - 一般以
0x6开头
- OC 中,使用
- 栈区 Stack
- 局部变量、方法的参数、对象的指针
- 栈是系统数据结构,栈所对应的进程或线程是唯一的,主线程的栈区为1M,子线程为512K
- 一般以
0x7开头
- 常量区 Constant
- 编译阶段确定后不再改变
- 常量区存储了程序中定义的死值,如 int a = 10;
- 存储在
.rodata区 - 一般以
0x1开头
- 全局区 Global(静态区 Static)
- 全局变量、静态变量
- 编译阶段确定后不再改变
- 一般也以
0x1开头 - 未初始化的变量存储在
BSS区(.bss),已初始化的变量存储在数据区(.data)
- 代码区
- 编译生成的二进制文件
- 存储在
.text区
2、iOS内存管理方案
2.1、Tagged Pointer(无需引用计数)
- 针对小对象(存储内容小):NSDate、NSNumber、NSString、NSIndexPath 等
- 使用后不再存储地址,而是真正的值;3倍的空间效率,106倍的访问速度
- 在0的位置打上标记(Intel最低位为1,ARM最高位为1)
- 初始化时会与一个随机值进行混淆
概述
-
以 NSString 为例,读取一个常规的 NSString,通过 栈区存储的指针地址,找到堆区空间,再从堆区读取到字符串的值,整个读取流程效率较低,所以,系统对其进行优化,如果 NSString 存储的 字符串长度较短(
<= 9),会使用 Tagged Pointer 存储 -
Tagged Pointer 也是一个指针,指针中包含
Tagged标记,用于区分存储的数据类型,同时 将值也存储在指针中,通过位运算将其编码成一个指针格式
NSString 内存管理的三种类型
NSTaggedPointerString:字符串优化类型,存储在 常量区__NSCFConstantString:字符串常量,是一种 编译时常量,retainCount 值很大,对其操作不会引起引用计数变化,存储在 字符串常量区__NSCFString:运行时 创建的 NSString 子类,创建后引用计数默认为 1,存储在 堆区
示例
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str1 = @"Thank you!!!";
NSString *str2 = @"Thank";
NSString *str3 = @"谢谢你!";
NSString *str4 = [NSString stringWithFormat:@"Thank you!!!"];
NSString *str5 = [NSString stringWithFormat:@"Thank"];
NSString *str6 = [NSString stringWithFormat:@"谢谢你!"];
NSNumber *num = [NSNumber numberWithShort:5];
NSLog(@"%p %@",str1,[str1 class]);
NSLog(@"%p %@",str2,[str2 class]);
NSLog(@"%p %@",str3,[str3 class]);
NSLog(@"%p %@",str4,[str4 class]);
NSLog(@"%p %@",str5,[str5 class]);
NSLog(@"%p %@",str6,[str6 class]);
NSLog(@"%p %@",num,[num class]);
// 打印结果
TaggedPointerTest[1951:76449] 0x10766c028 __NSCFConstantString
TaggedPointerTest[1951:76449] 0x10766c048 __NSCFConstantString
TaggedPointerTest[1951:76449] 0x10766c068 __NSCFConstantString
TaggedPointerTest[1951:76449] 0x60000026f760 __NSCFString
TaggedPointerTest[1951:76449] 0xb8814bf3e7169112 NSTaggedPointerString
TaggedPointerTest[1951:76449] 0x60000026f6a0 __NSCFString
TaggedPointerTest[1951:76449] 0xa8814d4501001406 __NSCFNumber
TaggedPointerTest[1951:76449] 0xa00006b6e6168545
}
-
字符串直接通过
=号赋值的不管中文还是英文都是 __NSCFConstantString 类型,存在 字符串常量区 -
当字符串是由 数字、英文字母(存的是ASCII码,所以ASCII表中的都可以) 组合且长度
<= 9,会自动成为NSTaggedPointerString类型,存储在常量区 -
当字符串中包含 中文或者其他特殊符号,会直接成为
__NSCFString类型,存储在堆区
2.1.1、tagged pointer解码
-
_objc_encodeTaggedPointer:objc源码中tagged pointer的 混淆编码方法
static inline void * _Nonnull _objc_encodeTaggedPointer(uintptr_t ptr) { // 与随机值 objc_debug_taggedpointer_obfuscator 进行异或来混淆 uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr); // ARM64架构会进行额外操作 #if OBJC_SPLIT_TAGGED_POINTERS if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK) return (void *)ptr; uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK; uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag); value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT); value |= permutedTag << _OBJC_TAG_INDEX_SHIFT; #endif return (void *)value; } -
与随机值
objc_debug_taggedpointer_obfuscator进行异或来混淆,而这个随机数是在以前探索dyld读取image时的_read_images方法中的initializeTaggedPointerObfuscator()方法进行的生成void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) { ...... if (DisableTaggedPointers) { disableTaggedPointers(); } // 进程启动时初始化一个随机的值,用于混淆tagged pointer指针 initializeTaggedPointerObfuscator(); ...... }
2.1.2、tagged pointer解码
-
_objc_decodeTaggedPointer:objc源码中tagged pointer的 混淆解码方法static inline uintptr_t _objc_decodeTaggedPointer(const void * _Nullable ptr) { uintptr_t value = _objc_decodeTaggedPointer_noPermute(ptr); #if OBJC_SPLIT_TAGGED_POINTERS uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK; value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT); value |= _objc_obfuscatedTagToBasicTag(basicTag) << _OBJC_TAG_INDEX_SHIFT; #endif return value; } -
随机数
objc_debug_taggedpointer_obfuscator为全局静态变量,我们使用extern修饰,将其导出,自己实现一个解码函数,使用相同的值,将指针再次按位 异或 即可还原 -
objc源码中的解码我们无法拿来直接用,因此我们将内容拷贝出来加以修改,在自己的项目中当做一个解码函数,但要注意因为模拟器与真机架构不同,上边代码中也提到了ARM64架构下的处理更多,因此这个解码函数在真机下的修改方式更繁琐,直接给出使用代码(粘贴就完事了~):
// 模拟器下解码函数 extern uintptr_t objc_debug_taggedpointer_obfuscator; uintptr_t // lz_objc_decodeTaggedPointer() 解码函数 lz_objc_decodeTaggedPointer(id ptr) { return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator; }// 真机下解码函数 #define lz_OBJC_TAG_INDEX_MASK 0x7UL #define lz_OBJC_TAG_INDEX_SHIFT 0 extern uintptr_t objc_debug_taggedpointer_obfuscator; extern uint8_t objc_debug_tag60_permutations[8]; uintptr_t lz_objc_obfuscatedTagToBasicTag(uintptr_t tag) { for (unsigned i = 0; i < 7; i++) if (objc_debug_tag60_permutations[i] == tag) return i; return 7; } // lz_objc_decodeTaggedPointer() 解码函数 uintptr_t lz_objc_decodeTaggedPointer(id ptr) { uintptr_t value = (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator; uintptr_t basicTag = (value >> lz_OBJC_TAG_INDEX_SHIFT) & lz_OBJC_TAG_INDEX_MASK; value &= ~(lz_OBJC_TAG_INDEX_MASK << lz_OBJC_TAG_INDEX_SHIFT); value |= lz_objc_obfuscatedTagToBasicTag(basicTag) << lz_OBJC_TAG_INDEX_SHIFT; return value; } static inline uintptr_t lz_objc_basicTagToObfuscatedTag(uintptr_t tag) { return objc_debug_tag60_permutations[tag]; } // lz_objc_encodeTaggedPointer() 编码函数 void * lz_objc_encodeTaggedPointer(uintptr_t ptr) { uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr); uintptr_t basicTag = (value >> lz_OBJC_TAG_INDEX_SHIFT) & lz_OBJC_TAG_INDEX_MASK; uintptr_t permutedTag = lz_objc_basicTagToObfuscatedTag(basicTag); value &= ~(lz_OBJC_TAG_INDEX_MASK << lz_OBJC_TAG_INDEX_SHIFT); value |= permutedTag << lz_OBJC_TAG_INDEX_SHIFT; return (void *)value; } -
使用
p/t命令打印二进制内容(图中为模拟器下读取方式,真机读取规则与模拟器不同)0b表示二进制,将二进制数据分段打印(po命令)时也要在前边加上 0b- 而且我们从图中可以看出,存储值的区域能容纳7个字符,
>=7、<=9个时虽然还是 NSTaggedPointerString 类型,但会改变存储方式: - Tagged Pointer 结构,在
ARM64架构下:- 高地址第一位,标志位,表示该 isa 为 Tagged Pointer 类型,可在 objc_tag_index_t 中找到对应
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, ...... } - 低地址最后三位,表示存储的类型,如:NSNumber、NSIndexPath、NSDate、NSString
- 低地址
4~7位,记录扩展信息,例如:存储 NSString 记录字符串长度、存储 NSNumber 记录基本数据类型的枚举值 - 其余
56位,用于存储值
- 高地址第一位,标志位,表示该 isa 为 Tagged Pointer 类型,可在 objc_tag_index_t 中找到对应
2.2、引用计数
-
在前边探索alloc原理时我们探索过创建isa的方法
initIsa,搜索流程:objc_alloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone -->initIsa,在里边有一个extrc_rc就是用于记录对象的引用计数的inline void objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor) { ASSERT(!isTaggedPointer()); isa_t newisa(0); if (!nonpointer) { // 非nonpointer newisa.setClass(cls, this); } else { ASSERT(!DisableNonpointerIsa); ASSERT(!cls->instancesRequireRawIsa()); #if SUPPORT_INDEXED_ISA ASSERT(cls->classArrayIndex() > 0); newisa.bits = ISA_INDEX_MAGIC_VALUE; // isa.magic is part of ISA_MAGIC_VALUE // isa.nonpointer is part of ISA_MAGIC_VALUE newisa.has_cxx_dtor = hasCxxDtor; newisa.indexcls = (uintptr_t)cls->classArrayIndex(); #else newisa.bits = ISA_MAGIC_VALUE; // isa.magic is part of ISA_MAGIC_VALUE // isa.nonpointer is part of ISA_MAGIC_VALUE # if ISA_HAS_CXX_DTOR_BIT newisa.has_cxx_dtor = hasCxxDtor; # endif newisa.setClass(cls, this); #endif // 创建isa,引用计数赋值为1 newisa.extra_rc = 1; } isa = newisa; } -
储存对象的引用计数有下面几种情况:
- 非nonpointerIsa,直接存在
sidetable中 - nonpointerIsa
- 对象正在被释放,不做操作
- extrc_rc 能存下,存在 extrc_rc 字段中
- extrc_rc 存不下,将一半存到 sidetable 中,并将
has_sidetable_rc标志位置为 true
- 非nonpointerIsa,直接存在
2.2.1、nonpointerIsa(开启指针优化的指针)
- 现在大部分指针都是nonpointerIsa
- 非纯指针类型的 isa,isa 中包含了 类信息、对象的引⽤计数 等
2.2.2、retain、release原理
-
retain 底层搜索 objc_retain
__attribute__((aligned(16), flatten, noinline)) objc_retain(id obj) { // 如果是 tagged pointer 就直接返回对象,也就是说无需引用计数管理 if (obj->isTaggedPointerOrNil()) return obj; return obj->retain(); } -
objc_retain 中的关键方法是
rootRetainALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant) { if (slowpath(isTaggedPointer())) return (id)this; bool sideTableLocked = false; bool transcribeToSideTable = false; isa_t oldisa; isa_t newisa; oldisa = LoadExclusive(&isa.bits); if (variant == RRVariant::FastOrMsgSend) { // These checks are only meaningful for objc_retain() // They are here so that we avoid a re-load of the 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)); } } if (slowpath(!oldisa.nonpointer)) { // a Class is a Class forever, so we can perform this check once // outside of the CAS loop if (oldisa.getDecodedClass(false)->isMetaClass()) { ClearExclusive(&isa.bits); return (id)this; } } do { 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; // 是nonpointerIsa,且 extra_rc 能容纳 newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++ if (slowpath(carry)) { // 是nonpointerIsa,但 extra_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; // extra_rc改为只存一半引用计数 newisa.extra_rc = RC_HALF; // 一半存到 sidetable 中,要打上标记 newisa.has_sidetable_rc = true; } } while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits))); if (variant == RRVariant::Full) { if (slowpath(transcribeToSideTable)) { // Copy the other half of the retain counts to the side table. // sidetable中存入剩余一半引用计数 sidetable_addExtraRC_nolock(RC_HALF); } if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock(); } else { ASSERT(!transcribeToSideTable); ASSERT(!sideTableLocked); } return (id)this; } -
release 底层搜索 objc_release,流程与retain相反,无需赘述,需要注意的是当引用计数减为0时,会向对象 发送dealloc消息
__attribute__((aligned(16), flatten, noinline)) void objc_release(id obj) { // 如果是 tagged pointer 就不做任何操作,无需引用计数管理 if (obj->isTaggedPointerOrNil()) return; return obj->release(); }ALWAYS_INLINE bool objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant) { ...... deallocate: // Really deallocate. ASSERT(newisa.isDeallocating()); ASSERT(isa.isDeallocating()); if (slowpath(sideTableLocked)) sidetable_unlock(); __c11_atomic_thread_fence(__ATOMIC_ACQUIRE); if (performDealloc) { // 发送dealloc消息 ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc)); } return true; }
2.2.3、dealloc
- dealloc 底层搜索 _objc_rootDealloc
当 对象为nonpointerIsa,并且没有弱引用、没有关联对象、没有C++析构函数、没有sidetable借位 时才对对象进行释放,否则先将这些内容抹掉再free对象inline void objc_object::rootDealloc() { if (isTaggedPointer()) return; // fixme necessary? // 是nonpointer 并且同时满足以下条件时才会free if (fastpath(isa.nonpointer && // 没有弱引用 !isa.weakly_referenced && // 没有关联对象 !isa.has_assoc && #if ISA_HAS_CXX_DTOR_BIT // 没有C++的析构函数 !isa.has_cxx_dtor && #else !isa.getClass(false)->hasCxxDtor() && #endif // 没有 sidetable 借位 !isa.has_sidetable_rc)) { assert(!sidetable_present()); // 释放对象 free(this); } else { // 调用C++析构函数、删除关联对象、清空散列表中引用计数信息、清空弱引用表中相关数据 object_dispose((id)this); } }