内存管理上篇(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转为二进制分别是10十、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
相关文章
相关标签/搜索