程序运行分配的内存是一定的,因此内存管理就是很有必要,开发中避免内存泄漏,我们因及时释放。那么内存是如何管理的?
1. ARC和MRC
在iOS开发中,我们有大致2种内存管理方式:MRC
(手动管理)和ARC
(自动管理)
- MRC:在手动内存管理环境下遵循以下规则
- 对象被
创建
,引用计数+1
; - 对象被其它指针引用时,
引用计数+1
,需要手动调用【objc retain】
; - 当指针变量不再引用对象时,要手动释放,调用
【objc release】
,引用计数-1; - 当引用计数为0时,系统会
自动销毁
这个对象。
大体上就是在MRC环境下,对象谁创建,谁释放;谁引用,谁管理
。
- ARC:自动内存管理,
编译器
帮我操作引用计数
,进行retain
,release
等操作。
2. 内存五大区
我们之前知道程序运行的时候系统会给程序分配虚拟内存
,32位操作系统最大分配4G内存,64位操作系统最多最大分配8G内存,为了保持兼容通常程序还是申请4g虚拟内存。在iOS开发中,以4g内存为例,程序加载的内存分布
如下
内核区
占有1GB,主要系统进行内核处理操作的区域。- 五大区
栈区
:函数,方法;栈区的内存地址一般是0x7开头堆区
:通过alloc分配的对象,block copy;堆区的内存地址一般为0x6开头BBS段
:未初始化的全局变量,静态变量;内存地址一般为0x1开头数据段
:已初始化的全局变量,静态变量;内存地址一般为0x1开头代码段
:程序代码,加载到内存中。
- 保留区:主要原因是
0x00000000
表示nil
,不能直接用nil表示一个段,所以单独给了一段内存用于处理nil
等情况
3. 内存管理方案
内存管理方案还有:
taggedPointer
: 专门用来处理小对象,例如NSNumber、NSDate、小NSString等Nonpointer_isa
: 非指针类型的isaSideTables
:散列表,包括引用计数列表和弱引用表
3.1 TaggedPointer
我们先看看原有的对象为什么会浪费内存。假设我们要存储一个 NSNumber
对象,其值是一个整数。正常情况下,如果这个整数只是一个 NSInteger
的普通变量,那么它所占用的内存是与 CPU 的位数
有关,在 32 位 CPU 下占 4 个字节,在 64 位 CPU 下是占 8 个字节的。而指针类型的大小通常也是与 CPU 位数相关,一个指针所占用的内存在 32 位 CPU 下为 4 个字节,在 64 位 CPU 下也是 8 个字节。
为了改进上面提到的内存占用和效率问题,苹果提出了Tagged Pointer
对象。由于 NSNumber、NSDate 一类的变量本身的值需要占用的内存大小常常不需要 8 个字节
我们也可以在 WWDC2013 的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer
特点的介绍:
Tagged Pointer
专门用来存储小的对象,例如NSNumber
和NSDate
Tagged Pointer
指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free。- 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。
由此可见,苹果引入Tagged Pointer
,不但减少了 64 位机器下程序的内存占用,还提高了运行效率。完美地解决了小内存对象在存储和访问效率上的问题
之后在 WWDC2020年 上对arm64上标记指针格式进行了更改
由于对齐要求,低位总是为零
。指针大小的倍数,地址高位总是为零
,因为地址空间有限。我们实际上不会一直到2到64这些高位和低位总是零位
。因此,让我们从这些总是零的位中选择一个
,并把它变成一个这可以立即告诉我们这不是一个真正的对象指针
然后我们可以为所有其他位分配一些其他含义。我们称之为标记指针
。
3.1.1 模拟器环境下TaggedPointer
我们运行下面代码,地址是0x9开头和我们之前说的在堆区栈区都不同,类型是NSTaggedPointerString
NSCFConstantString
:字符串常量,是一种编译时常量
,retainCount值很大,对其操作,不会引起引用计数变化,存储在字符串常量区
NSCFString
:是在运行时
创建的NSString
子类,创建后引用计数会加1,存储在堆上
NSTaggedPointerString
:标签指针,是苹果在64位环境下对NSString
、NSNumber
等对象做的优化。对于NSString
对象来说- 当字符串是由数字、英文字母组合且长度小于等于9时,会自动成为
NSTaggedPointerString
类型,存储在常量区 - 当有中文或者其他特殊符号时,会直接成为
__NSCFString
类型,存储在堆区
- 当字符串是由数字、英文字母组合且长度小于等于9时,会自动成为
- 对于
NSString
来说,当字符串较小
时,建议直接通过@""
初始化,因为存储在常量区
,可以直接进行读取。会比WithFormat初始化方式
更加快速
我们查看源码TaggedPointer
搜索
看到这里有关于playload的位置运算,说明这里有加密和解密的过程。
// payload = (decoded_obj << payload_lshift) >> payload_rshift
// Payload signedness is determined by the signedness of the right-shift.
复制代码
那么接下来就去搜索decoded
:加密。
static inline uintptr_t
_objc_decodeTaggedPointer_noPermute(const void * _Nullable ptr)
{
uintptr_t value = (uintptr_t)ptr;
#if OBJC_SPLIT_TAGGED_POINTERS
if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
return value;
#endif
return value ^ objc_debug_taggedpointer_obfuscator;//异或处理
}
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
进行异或处理,我们继续查看混淆器起初始化我们曾经在探索源码readImags中也初始化过
static void
initializeTaggedPointerObfuscator(void)
{
if (!DisableTaggedPointerObfuscation) {
// Pull random data into the variable, then shift away all non-payload bits.
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;//将随机数据放入变量中,然后移走所有非有效负载因子。
#if OBJC_SPLIT_TAGGED_POINTERS
// The obfuscator doesn't apply to any of the extended tag mask or the no-obfuscation bit.
objc_debug_taggedpointer_obfuscator &= ~(_OBJC_TAG_EXT_MASK | _OBJC_TAG_NO_OBFUSCATION_MASK);
// Shuffle the first seven entries of the tag permutator.
int max = 7;
for (int i = max - 1; i >= 0; i--) {
int target = arc4random_uniform(i + 1);
swap(objc_debug_tag60_permutations[i],
objc_debug_tag60_permutations[target]);
}
#endif
} else {
// Set the obfuscator to zero for apps linked against older SDKs,
// in case they're relying on the tagged pointer representation.
objc_debug_taggedpointer_obfuscator = 0;
}
}
复制代码
这里如果开启了混淆,那么就会得到一个随机的objc_debug_taggedpointer_obfuscator
值,否则就为0。
继续看下解码encode,进行一次异或
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
#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;
}
复制代码
通过实现,我们可以得知,在编码和解码部分,经过了两层异或
,其目的是得到小对象自己
,例如以 1010 0001
为例,假设mask
为 0101 1000
1010 0001
^0101 1000 mask(编码)
1111 1001
^0101 1000 mask(解码)
1010 0001
复制代码
我们之前打印的就是标记指针解码后的值
而之前的左移右移操作,则是在_objc_makeTaggedPointer
里面。这里可以看出来先进行移动,在进行加密。
因此我们要向右移动3位,去掉tag,首位是空的因此不存储值,从第二位开始每8位存储一个字节表示一个字母,小端模式从左到右
对照ASCII
表:test
taggedPointer的是特殊的指针,我们看下64位为1表示
taggedPointer的标记,其中63~61
表示类型
。上图中的NSString类型就是1010
其中63-61位就是2
。
{
// 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,
// 52-bit payloads
OBJC_TAG_Photos_1 = 8,
OBJC_TAG_Photos_2 = 9,
OBJC_TAG_Photos_3 = 10,
OBJC_TAG_Photos_4 = 11,
OBJC_TAG_XPC_1 = 12,
OBJC_TAG_XPC_2 = 13,
OBJC_TAG_XPC_3 = 14,
OBJC_TAG_XPC_4 = 15,
OBJC_TAG_NSColor = 16,
OBJC_TAG_UIColor = 17,
OBJC_TAG_CGColor = 18,
OBJC_TAG_NSIndexSet = 19,
OBJC_TAG_NSMethodSignature = 20,
OBJC_TAG_UTTypeRecord = 21,
// When using the split tagged pointer representation
// (OBJC_SPLIT_TAGGED_POINTERS), this is the first tag where
// the tag and payload are unobfuscated. All tags from here to
// OBJC_TAG_Last52BitPayload are unobfuscated. The shared cache
// builder is able to construct these as long as the low bit is
// not set (i.e. even-numbered tags).
OBJC_TAG_FirstUnobfuscatedSplitTag = 136, // 128 + 8, first ext tag with high bit set
OBJC_TAG_Constant_CFString = 136,
OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
复制代码
3.1.2 真机下TaggedPinter
原理和模拟器类似,储存位置有些不一样
- 第
1位
是标记taggedPointer - 类型是存储
低三位
char 0,short 1,int 2,3 long,4 float,5 double。 - 再住走
四位
,存储的是数据的长度
- 再往后是数据的
内容
3.1.3 面试题
//MARK: - taggedPointer 面试题1
- (void)taggedPointerDemo {
dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(queue, ^{
self.nickName = [NSString stringWithFormat:@"test"];
NSLog(@"%@",self.nickName);
});
}
}
//MARK: - taggedPointer 面试题2
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"来了");
// 多线程 读和写
// setter -> retian release
dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<100000; i++) {
dispatch_async(queue, ^{
self.nickName = [NSString stringWithFormat:@"无敌超级海景哈哈哈哈哈"];
NSLog(@"%@",self.nickName);
});
}
}
复制代码
运行面试题1和2会怎么样,面试1不会发生崩溃,面试题2发生崩溃。我们之前的学习中知道,我们在多线程异步队列
下,同时对一个值进行读写会发生崩溃,我们赋值的时候会对对新值进行retain
,旧值进行release
操作,因为是异步的导致同一时间会发生2次release
,再次读取就会因为野指针
报错。
- 我们看下为什么面试题1没有发生崩溃。
这里表明这个nickName是NSTaggedPointerString
,我们上面也探讨过对于taggedPointer它的值直接存储在指针中,没有指针指向的内存,因此不对对指针指向的内存操作,因此不会发生崩溃。看下源码的说明:
- 在demo2中我们看下NSString的类型
类型为NSCFString
,存放在堆区不是taggedPointer
类型因此会发生上述我们分析的情况。
3.2 NONPOINTER_ISA
我们之前isa学习的时候知道了这里简单说明下:
-
nonpointer
:表示是否对 isa 指针开启指针优化
0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等 -
has_assoc
:关联对象标志位,0没有,1存在 -
has_cxx_dtor
:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象 -
shiftcls
:存储类指针的值(相当于taggedPointer的payload)。开启指针优化的情况下,在arm64架构中有33位⽤来存储类指针。 -
magic
:⽤于调试器判断当前对象是真的对象还是没有初始化的空间 -
weakly_referenced
:志对象是否被指向或者曾经指向⼀个ARC的弱变量,没有弱引⽤的对象可以更快释放。 -
deallocating
:标志对象是否正在释放内存 -
has_sidetable_rc
:当对象引⽤技术⼤于10
时,则需要借⽤该变量存储进位 -
extra_rc
:当表示该对象的引⽤计数值,实际上是引⽤计数值减1,例如,如果对象的引⽤计数为10,那么extra_rc为9。如果引⽤计数⼤于10,则需要使用到has_sidetable_rc
。
3.3 散列表SideTable
当对象引⽤技术⼤于10
时,则需要借⽤该变量存储进位
我们就来继续探索引用计数retain的底层实现
3.1 retain
我们通过源码分析下retain的流程
- objc_retain
2. retain()
3. rootRetain
我们主要看下rootRetain
- 先判断是否是
taggedPointer
,是的话直接返回。 - 判断是否是
nonpointer
,不是的话 判断类的话,不是对象,不对引用计数操作 - 进行dowhile循环
- 不是nonpointer,即是
纯的isa
,直接对散列表
操作 - 正在
释放
,判断是不是在析构,如果在析构就没必要-1操作,在多线程的情况下,已经在释放,还有可能-1 - bits中进行++操作,并给一个引用计数的
状态标识carry
,用于表示extra_rc
是否满了 - 如果
carray
的状态表示extra_rc的引用计数满
了,此时需要操作散列表
,即 将满状态的一半拿出来存到extra_rc
,另一半存在 散列表的rc_half
。这么做的原因是因为如果都存储在散列表,每次对散列表操作都需要开解锁,操作耗时,消耗性能大,这么对半分
操作的目的在于提高性能
- 不是nonpointer,即是
3.2 relseas
release流程和retain类似,通过setProperty -> reallySetProperty -> objc_release -> release -> rootRelease -> rootRelease
顺序,进入rootRelease
源码
其流程和retain相反
- 先判断是否是
taggedPointer
,是的话直接返回。 - 判断是否是
nonpointer
,不是的话 判断是类的话,不是对象,不对引用计数操作 - 进行dowhile循环
非nonpointer
的话直接对散列表处理,引用计数-1isDeallocating
:正在析构,就不处理,去执行dealloc流程extra_rc--
进行引用计数-1操作,即extra_rc-1
,如果此时extra_rc
的值为0
了,则走到underflow
。
- underflow
has_sidetable_rc
:判断散列表中是否存储了一半的引用计数,没有的话自动销毁
。sidetable_subExtraRC_nolock
:从散列表取出一半的引用计数,进行-1操作
,然后存储到extra_rc
中
3.3 dealloc
查看dealloc -> _objc_rootDealloc -> rootDealloc
源码实现
- 根据条件
判断是否有isa、cxx、关联对象、弱引用表、引用计数表
,如果没有,则直接free释放内存
- 如果有,则进入
object_dispose
方法
- 继续查看
objc_destructInstance
- 查看
clearDeallocating
4. 总结
- 关于内存管理我们知道了苹果通过对
ARC
,MRC
,taggedPointer
,引用计数的retain
和release
进行管内存理释放对象。在ARC环境下,不需要我们手动管理对象的引用计数。 - 对于一些小的对象比如
NSString
,NSNumber
,NSdate
等进行了指针优化,把值存储到指针中,通过特定的标记进行区分TaggedPointer
以及它的类型
。 - 对象的isa中根据是否是
纯的isa
进行相对应处理,纯的isa引用计数存放在散列表中
,非纯的isa则存在isa中的extra_rc
中。在retain
中,存满了取出一半存放在散列表
中,extra_rc++
; - 在
release
中,extra_rc--
后为0
的话判断散列表是否为空,为空走delloc
流程,不为空则取出散列表出一半的引用计数
,-1
操作之后存储到extra_rc
中。 - delloc主要是对对象的
引用计数列表
,关联对象
列表,弱引用表
进行销毁,释放内存。