-
什么是内存管理
不同系统版本对App运行时占用的内存限制不同。当程序所占用的内存较多时,系统就会发出内存警告,这时就得回收一些不需要再使用的内存空间。比如回收一些不需要使用的对象、变量等。如果程序占用内存过大,系统可能会强制关闭程序,造成程序崩溃、闪退现象,影响用户体验。所以,我们需要对内存进行合理的分配内存、清除内存,回收那些不需要再使用的对象。从而保证程序的稳定性。
-
内存布局
知道如何内存管理之前先要知道内存的布局。文章内存五大区中介绍了内存的五大区依次是栈、堆、常量区、全局区、代码区,其实除了这五大区还有保留区和内核区,内核区主要是系统进行内核操作的(例如:系统分给程序的内存是
4GB
,其中3GB
是用于五大区和保留区,剩下1GB
是用于内核区),而保留区主要是预留给系统处理nil等。内存布局图如下:
-
内存管理方案
-
TaggedPointer
先看一段代码:
- (void)taggedPointerDemo { self.queue = dispatch_queue_create("com.tudou.cn", DISPATCH_QUEUE_CONCURRENT); for (int i = 0; i<10000; i++) { dispatch_async(self.queue, ^{ self.nameStr = [NSString stringWithFormat:@"tudou"]; // NSLog(@"%@",self.nameStr); }); } } 复制代码
调用这个方法运行发现可以正常打印如下图:
此时再添加一个方法- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ for (int i = 0; i<10000; i++) { dispatch_async(self.queue, ^{ self.nameStr = [NSString stringWithFormat:@"tudou_好好学习天天向上"]; NSLog(@"%@",self.nameStr); }); } } 复制代码
点击屏幕发现崩溃了
其实崩溃反而更好理解,我们知道在赋值的过程中调用
set
方法是需要对新值的retain
和旧值的release
,但是此时是多线程的并且也没有加锁做线程安全,所以就会出现多个线程同时访问这个变量的情况,同时赋值同时release
旧值就造成了过度释放的问题所以崩溃,但是第一种情况就奇怪了没有崩溃,此时分别下断点查看两种赋值情况下nameStr
的类型
发现第一种情况nameStr
的类型是NSTaggedPointerString
也就是小对象类型,而第二种情况是NSCFString
也就是字符串类型。
这里就引申出来TaggedPointer
小对象类型。那么同样是通过stringWithFormat
方法创建字符串为什么第一种情况是小对象类型而第二种情况不是呢,又为什么第一种情况不会崩溃,第二种情况会崩溃呢?带着问题我们可以先来了解一下什么是小对象类型。- 源码探索小对象类型
首先创建一个小对象类型打印它的地址如下图:
发现这个小对象的地址是
0x8b168fada8723f5a
,按照内存五大区文章中的内存分段发现不知道是归属于那一块
在看_read_images
函数中有个initializeTaggedPointerObfuscator
函数,查看该函数的源码发现是初始化小对象类型混淆器,这里的做法就是先与上_OBJC_TAG_MASK
(ios12以后)
然后全局搜索objc_debug_taggedpointer_obfuscator
发现如下代码
发现了小对象类型的编解码函数,得知底层混淆小对象是进行了异或操作,编码是使用
objc_debug_taggedpointer_obfuscator
异或小对象,解码时是用小对象异或解码时objc_debug_taggedpointer_obfuscator
所以上文中打印的小对象的地址是编码后的地址,得到真实小对象的地址则需要解码如下图得到解码后的地址:
发现是
0xa000000000000611
发现61
刚好就是a
的ASCII
码,不知道是不是应为凑巧我们可以多试几个小对象类型:
发现地址中就存在着值那么
0xa
、0xb
又是什么呢,此时我们查看一下判断小对象类型的源码也就是查看_objc_isTaggedPointer
函数的源码发现
发现是通过最高位是否是1来判断是否是小对象类型
0xa
、0xb
转为二进制分别是1010、1011
,都是1所以都是小对象类型,后三位主要是用来标记tagType
的0xa
的后三位转成二进制是2,0xb
的后三位转二进制是3,此时我们再看_objc_makeTaggedPointer
的源码
发现入参就有一个
tag
,查看这个tag
的枚举类型
发现2就是字符串的小对象类型,3就是
NSNumber
的小对象类型 - 小对象类型不会出现过度释放崩溃的原因
上文中发现小对象类型的值其实就在地址中,并不是存在堆区而是常量区,所以小对象类型的释放是系统处理的,也可以查看retain
和release
源码发现
如果是小对象类型直接返回对象了,所以set方法中不存在旧值的释放也就不会存在过度释放崩溃 - 情况1是小对象的原因
应为情况一复制的是a内存暂用过小,oc优化处理变成小对象类型,占用多大内存是小对象类型,多大又是oc对象了呢如下表:
- 小对象总结
- 小对象并不是个真正的对象不存在堆区,是存在常量区的
- 小对象类型不会进入
retain
和release
方法中 - 小对象类型的64位地址中,前4位代表类型,后4位主要适用于系统做一些处理,中间56位用于存储值
- 小对象的有点:应为不存储在堆区所以节省了空间,可以直接进行读取,在内存读取上有着3倍的效率,创建时⽐以前快106倍。
- 源码探索小对象类型
-
散列表
sideTable
在文章通过源码分析isa知道了
extra_rc
使用来存储引用计数的,但是也是有大小限制的如果extra_rc
存满了此时就会分出一半存到SideTables
中,此时我们就可以通过retain
和release
的源码探索来验证。-
retain
源码分析首先过滤掉小对象类型
retain
步骤:- 先判断如果是小对象类型直接返回对象
- 判断
Nonpointer_isa
如果没有开启指针优化则引用计数直接存到散列表中 - 如果当前类正在执行
Dealloc
方法也就是则直接返回当前isa
- 这一步就可以对引用计数进行加一操作了,先对
extra_rc
进行加一操作 - 判断
extra_rc
是否已经存满了 - 如果没有存满则直接返回isa
- 如果存满了则分出一半存到
extra_rc
,另一半存到散列表
使用
extra_rc
的原因是节省性能,如果都存在散列表每次读写散列表都要进行解锁和加锁的操作所以耗费性能。 -
release
源码分析
release
步骤:- 先判断如果是小对象类型直接返回对象
- 判断
nonpointer_isa
如果没有开启指针优化则散列表中的引用计数减一,如果散列表中的引用计数减到0则调用dealloc方法 - 如果正在执行
dealloc
方法则直接返回false
- 对
extra_rc
进行减一操作。 - 如果
extra_rc
当前的值等于0,则判断has_sidetable_rc
是否为true
如果不是则执行dealloc
方法否则跳转到第六步。 - 取出散列表中的一半的引用计数减一后存储到
extra_rc
参数。如果此时散列表的引用计数为0则清空散列表中的引用计数has_sidetable_rc
参数置为false
-
散列表相关问题
- 散列表多张还是一张
答案是多张(8张),如果仅仅是一张表会不安全只要解锁所有数据都能看到,但是个对象一张表又耗性能冲下面源码可以看出具体散列表个数
第一步先找到散列表的get
方法,看get
方法的源码
冲这里发现是8张散列表
- 散列表多张还是一张
-
-
dealloc
源码分析dealloc
底层源码调用流程dealloc
->_objc_rootDealloc
->rootDealloc
再看object_dispose
函数源码
dealloc
具体步骤:- 先判断如果是
tagged pointer
直接返回 - 如果开启指针优化、没有关联对象、没有弱引用表、没有
c++
析构函数、散列表中没有存储相应的引用计数直接free
。否则走下一步 - 如果存在
C++
析构函数则调用析构函数 - 如果存在关联对象则删除关联对象
- 如果没有开启指针优化则直接清空散列表中的引用计数然后
free
.否则走到下一步 - 判断如果存在弱引用或者散列表中有引用计数则清空弱引用表和散列表中的引用计数
- 最后再
free
- 先判断如果是
-
retainCount
源码分析先看一段代码
NSObject *objc = [NSObject alloc]; NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc)); 复制代码
两个问题
alloc
之后的引用计数是多少,NSLog
打印的值是多少
答案是可能是1可能是0,目前最新的源码显示alloc方法是会给extra_rc赋值为1如下图
但是老的源码是没有赋值的,我记得781的源码是没有赋值的,所以打印的是0,(具体alloc源码探索可以参考下面这个文章alloc & init & new 源码分析);
第二个问题:
答案肯定是1了。先看retainCount
源码
源码调用路径为:retainCount
->_objc_rootRetainCount
->rootRetainCount
源码还是挺简单的主要是下面几个步骤:- 判断是不是小对象类型,如果是直接返回,不是则走下一步
- 判断是否做了指针优化,如果没做则直接返回散列表中的引用计数,否则走下一步
- 此时判断散列表中是否存储了引用计数,没有存直接返回
extra_rc
否则返回两者之和
注意:781的源码再此基础上进行了加一操作所以返回的也是1
-
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END
喜欢就支持一下吧