内存管理上篇(TaggedPointer、retain、release、dealloc、retainCount 底层源码分析)

  • 什么是内存管理

    不同系统版本对App运行时占用的内存限制不同。当程序所占用的内存较多时,系统就会发出内存警告,这时就得回收一些不需要再使用的内存空间。比如回收一些不需要使用的对象、变量等。如果程序占用内存过大,系统可能会强制关闭程序,造成程序崩溃、闪退现象,影响用户体验。所以,我们需要对内存进行合理的分配内存、清除内存,回收那些不需要再使用的对象。从而保证程序的稳定性。

  • 内存布局

    知道如何内存管理之前先要知道内存的布局。文章内存五大区中介绍了内存的五大区依次是栈、堆、常量区、全局区、代码区,其实除了这五大区还有保留区和内核区,内核区主要是系统进行内核操作的(例如:系统分给程序的内存是4GB,其中3GB是用于五大区和保留区,剩下1GB是用于内核区),而保留区主要是预留给系统处理nil等。内存布局图如下:
    未命名.jpg

  • 内存管理方案

    • 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);
               });
           }
       }
      复制代码

      调用这个方法运行发现可以正常打印如下图:
      image.png
      此时再添加一个方法

       - (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);
               });
           }
       }
      复制代码

      点击屏幕发现崩溃了
      image.png其实崩溃反而更好理解,我们知道在赋值的过程中调用set方法是需要对新值的retain和旧值的release,但是此时是多线程的并且也没有加锁做线程安全,所以就会出现多个线程同时访问这个变量的情况,同时赋值同时release旧值就造成了过度释放的问题所以崩溃,但是第一种情况就奇怪了没有崩溃,此时分别下断点查看两种赋值情况下nameStr的类型
      image.png
      image.png
      发现第一种情况nameStr的类型是NSTaggedPointerString也就是小对象类型,而第二种情况是NSCFString也就是字符串类型。
      这里就引申出来TaggedPointer小对象类型。那么同样是通过stringWithFormat方法创建字符串为什么第一种情况是小对象类型而第二种情况不是呢,又为什么第一种情况不会崩溃,第二种情况会崩溃呢?带着问题我们可以先来了解一下什么是小对象类型。

      1. 源码探索小对象类型
        首先创建一个小对象类型打印它的地址如下图:
        image.png发现这个小对象的地址是0x8b168fada8723f5a,按照内存五大区文章中的内存分段发现不知道是归属于那一块
        在看_read_images函数中有个initializeTaggedPointerObfuscator函数,查看该函数的源码发现是初始化小对象类型混淆器,这里的做法就是先与上_OBJC_TAG_MASK(ios12以后)
        image.png
        image.png
        然后全局搜索objc_debug_taggedpointer_obfuscator发现如下代码
        image.png发现了小对象类型的编解码函数,得知底层混淆小对象是进行了异或操作,编码是使用objc_debug_taggedpointer_obfuscator异或小对象,解码时是用小对象异或解码时objc_debug_taggedpointer_obfuscator所以上文中打印的小对象的地址是编码后的地址,得到真实小对象的地址则需要解码如下图得到解码后的地址:
        image.png发现是0xa000000000000611发现61刚好就是aASCII码,不知道是不是应为凑巧我们可以多试几个小对象类型:
        image.png发现地址中就存在着值那么0xa0xb又是什么呢,此时我们查看一下判断小对象类型的源码也就是查看_objc_isTaggedPointer函数的源码发现
        image.png发现是通过最高位是否是1来判断是否是小对象类型0xa0xb转为二进制分别是1010、1011,都是1所以都是小对象类型,后三位主要是用来标记tagType0xa的后三位转成二进制是2,0xb的后三位转二进制是3,此时我们再看_objc_makeTaggedPointer的源码
        image.png发现入参就有一个tag,查看这个tag的枚举类型
        image.png发现2就是字符串的小对象类型,3就是NSNumber的小对象类型
      2. 小对象类型不会出现过度释放崩溃的原因
        上文中发现小对象类型的值其实就在地址中,并不是存在堆区而是常量区,所以小对象类型的释放是系统处理的,也可以查看retainrelease源码发现
        image.pngimage.png
        如果是小对象类型直接返回对象了,所以set方法中不存在旧值的释放也就不会存在过度释放崩溃
      3. 情况1是小对象的原因
        应为情况一复制的是a内存暂用过小,oc优化处理变成小对象类型,占用多大内存是小对象类型,多大又是oc对象了呢如下表:
        image.png
      4. 小对象总结
        1. 小对象并不是个真正的对象不存在堆区,是存在常量区的
        2. 小对象类型不会进入retainrelease方法中
        3. 小对象类型的64位地址中,前4位代表类型,后4位主要适用于系统做一些处理,中间56位用于存储值
        4. 小对象的有点:应为不存储在堆区所以节省了空间,可以直接进行读取,在内存读取上有着3倍的效率,创建时⽐以前快106倍。
    • 散列表sideTable

      在文章通过源码分析isa知道了extra_rc使用来存储引用计数的,但是也是有大小限制的如果extra_rc存满了此时就会分出一半存到SideTables中,此时我们就可以通过retainrelease的源码探索来验证。

      • retain源码分析

        首先过滤掉小对象类型
        image.png
        image.png
        retain步骤:

        1. 先判断如果是小对象类型直接返回对象
        2. 判断Nonpointer_isa如果没有开启指针优化则引用计数直接存到散列表中
        3. 如果当前类正在执行Dealloc方法也就是则直接返回当前isa
        4. 这一步就可以对引用计数进行加一操作了,先对extra_rc进行加一操作
        5. 判断extra_rc是否已经存满了
        6. 如果没有存满则直接返回isa
        7. 如果存满了则分出一半存到extra_rc,另一半存到散列表

        使用extra_rc的原因是节省性能,如果都存在散列表每次读写散列表都要进行解锁和加锁的操作所以耗费性能。

      • release源码分析

        image.png
        image.png
        release步骤:

        1. 先判断如果是小对象类型直接返回对象
        2. 判断nonpointer_isa如果没有开启指针优化则散列表中的引用计数减一,如果散列表中的引用计数减到0则调用dealloc方法
        3. 如果正在执行dealloc方法则直接返回false
        4. extra_rc进行减一操作。
        5. 如果extra_rc当前的值等于0,则判断has_sidetable_rc是否为true如果不是则执行dealloc方法否则跳转到第六步。
        6. 取出散列表中的一半的引用计数减一后存储到extra_rc参数。如果此时散列表的引用计数为0则清空散列表中的引用计数has_sidetable_rc参数置为false
      • 散列表相关问题
        1. 散列表多张还是一张
          答案是多张(8张),如果仅仅是一张表会不安全只要解锁所有数据都能看到,但是个对象一张表又耗性能冲下面源码可以看出具体散列表个数
          第一步先找到散列表的get方法,看get方法的源码
          image.png
          image.png
          冲这里发现是8张散列表
    • dealloc源码分析

      dealloc底层源码调用流程dealloc->_objc_rootDealloc->rootDealloc
      image.png
      再看object_dispose函数源码
      image.png
      image.png
      image.pngimage.png
      dealloc具体步骤:

      1. 先判断如果是tagged pointer直接返回
      2. 如果开启指针优化、没有关联对象、没有弱引用表、没有c++析构函数、散列表中没有存储相应的引用计数直接free。否则走下一步
      3. 如果存在C++析构函数则调用析构函数
      4. 如果存在关联对象则删除关联对象
      5. 如果没有开启指针优化则直接清空散列表中的引用计数然后free.否则走到下一步
      6. 判断如果存在弱引用或者散列表中有引用计数则清空弱引用表和散列表中的引用计数
      7. 最后再free
    • retainCount源码分析

      先看一段代码

      NSObject *objc = [NSObject alloc];
      NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));
      复制代码

      两个问题alloc之后的引用计数是多少,NSLog打印的值是多少
      答案是可能是1可能是0,目前最新的源码显示alloc方法是会给extra_rc赋值为1如下图
      image.png
      但是老的源码是没有赋值的,我记得781的源码是没有赋值的,所以打印的是0,(具体alloc源码探索可以参考下面这个文章alloc & init & new 源码分析);
      第二个问题:
      答案肯定是1了。先看retainCount源码
      源码调用路径为:retainCount->_objc_rootRetainCount->rootRetainCount
      image.png
      源码还是挺简单的主要是下面几个步骤:

      1. 判断是不是小对象类型,如果是直接返回,不是则走下一步
      2. 判断是否做了指针优化,如果没做则直接返回散列表中的引用计数,否则走下一步
      3. 此时判断散列表中是否存储了引用计数,没有存直接返回extra_rc否则返回两者之和

      注意:781的源码再此基础上进行了加一操作所以返回的也是1

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享