OC高级-autoreleasepool的实现原理

目录

  • autorelease的本质
  • autorelease对象什么时候释放?
  • autoreleasepool的工作原理
  • autoreleasepool的内部结构
  • autoreleasepool的嵌套
  • autoreleasePoolPage
  • NSThread、NSRunLoop 和 NSAutoreleasePool三者之间的关系
  • 其他autorelease相关知识点
  • 面试题
  • 参考文章

autorelease的本质

  • autorelease本质上就是延迟调用release方法
  • MRC环境,通过调用[obj autorelease]延迟内存的释放
  • ARC环境,甚至可以完全不知道autorelease也能管理好内存

autorelease对象什么时候释放?

实验环境

  • ARC
  • 测试机型:iPhone 4S模拟器
  • 注意:在苹果一些新的硬件设备上,本实验的结果已经不再成立
__weak NSString *_weakStr = nil;
- (void)viewDidLoad
{
    [super viewDidLoad];

    // 场景 1
    NSString *string = [NSString stringWithFormat:@"yanhoo"];
    _weakStr = string;

    // 场景 2
//    @autoreleasepool {
//        NSString *string = [NSString stringWithFormat:@"yanhoo"];
//        _weakStr = string;
//    }

    // 场景 3
//    NSString *string = nil;
//    @autoreleasepool {
//        string = [NSString stringWithFormat:@"yanhoo"];
//        _weakStr = string;
//    }

    NSLog(@"string1: %@", _weakStr);
}

- (void)viewWillAppear:(BOOL)animated 
{
    [super viewWillAppear:animated];

    NSLog(@"string2: %@", _weakStr);
}

- (void)viewDidAppear:(BOOL)animated 
{
    [super viewDidAppear:animated];

    NSLog(@"string3: %@", _weakStr);
}

测试结果

场景1
2016-08-19 01:30:01.686 test[8866:554553] string1: yanhoo
2016-08-19 01:30:01.687 test[8866:554553] string2: yanhoo
2016-08-19 01:30:01.695 test[8866:554553] string3: (null)

场景2
2016-08-19 01:32:07.020 test[8886:556042] string1: (null)
2016-08-19 01:32:07.021 test[8886:556042] string2: (null)
2016-08-19 01:32:07.032 test[8886:556042] string3: (null)

场景3
2016-08-19 01:32:57.038 test[8900:557349] string1: yanhoo
2016-08-19 01:32:57.038 test[8900:557349] string2: (null)
2016-08-19 01:32:57.048 test[8900:557349] string3: (null)

分析

  • 首先来了解下__weak修饰符

    • 不会影响所指向对象的生命周期,即:使用__weak修饰的变量不会导致所引用的对象的引用计数+1
    • __weak修饰的变量所指向的对象被释放时,__weak修饰的变量的值会被置为nil不存在野指针问题
  • 场景1分析

    • 当使用[NSString stringWithFormat:@"yanhoo"]创建一个autorelease对象时,这个对象的引用计数为1,并且这个对象被系统自动添加到了最近autoreleasepool
    • 当使用局部变量string(在 ARC 下不指定变量所有权修饰符的情况下,默认为__strong)指向这个对象时,这个对象的引用计数 +1 ,变成了 2
    • 所以在viewDidLoad方法返回之前,这个对象是一直存在的,且引用计数为2
    • viewDidLoad方法返回之后,局部变量 string被回收,其所指向对象的引用计数 -1 ,变成了1
    • viewWillAppear方法中,我们仍然可以打印出这个对象的值,说明这个对象并没有被释放,说明到此autoreleasepool还未释放,从而导致autorelease对象未释放,因为只有当这个autoreleasepool自身被drain的时候,autoreleasepool中的 autoreleased 对象才会被 release
    • viewDidAppear中再打印这个对象的时候,对象的值变成了 nil,说明此时autoreleased对象已经被释放了,可以大胆猜测autoreleasepool一定在viewWillAppearviewDidAppear方法之间的某个时候被drain
  • 场景2和场景3请读者自行分析,这里就不再啰嗦了

  • 可以通过lldbwatchpoint命令来观察,具体参考这篇文章

  • 总结

    • 场景1出现得最多,就是不需要我们手动添加@autoreleasepool {}的情况,直接使用系统维护autoreleasepool
    • 场景2就是需要我们手动添加@autoreleasepool {}的情况,手动干预 autoreleased对象的释放时机,在一些很耗内存的循环调用的场景下有时需要手动干预autoreleased 对象的释放时机,不然会导致内存暴增,最终导致程序奔溃;
    • 场景3是为了区别于场景2而引入的,在这种场景下并不能达到出了@autoreleasepool {}的作用域时autoreleased 对象被释放的目的

autoreleasepool的工作原理

  • ARC环境下,@autoreleasepool{ }被编译器编译后,生成如下代码(以下代码是简化版
// push
void *poolToken = objc_autoreleasePoolPush();

// 这中间为写在{...}中的代码

// pop
objc_autoreleasePoolPop(poolToken);
  • 在运行循环开始前,系统会自动创建一个autoreleasepool(一个autoreleasepool会存在多个AutoreleasePoolPage),此时会调用一次objc_autoreleasePoolPush函数,runtime会向当前的AutoreleasePoolPage中add进一个POOL_SENTINEL哨兵对象,值为0,也就是个nil,代表autoreleasepool的起始边界),并返回此哨兵对象的内存地址poolToken
  • 在运行循环结束时autoreleasepool会被drain掉,此时会调用objc_autoreleasePoolPop(poolToken)函数,入参是之前产生的POOL_SENTINEL的内存地址poolToken,对在POOL_SENTINEL之后添加的所有autoreleased对象调用一次release,可以向前跨越若干个page,直到哨兵对象所在的page,并向回移动next指针哨兵对象所在位置
  • 中间{...}所产生的autoreleased对象都会被插入到最近的autoreleasepool中(因为autoreleasepool存在嵌套的情况)
  • 单个autoreleasepool的运行过程可以简单地理解为以下三个过程
    • objc_autoreleasePoolPush()
      • objc_autoreleasePoolPush()本质上就是调用的 AutoreleasePoolPage的push函数,如下所示
      void *
      objc_autoreleasePoolPush(void)
      {
          if (UseGC) return nil;
          return AutoreleasePoolPage::push();
      }
      
      • 每执行一次push操作就会新建一个autoreleasepool,对应的具体实现就是往AutoreleasePoolPage中的next位置插入一个POOL_SENTINEL,并且返回插入的POOL_SENTINEL的内存地址poolToken,这个地址在执行pop操作的时候作为函数的入参,下面是AutoreleasePoolPage的push函数代码
      static inline void *push()
      {
          id *dest = autoreleaseFast(POOL_SENTINEL);
          assert(*dest == POOL_SENTINEL);
          return dest;
      }
      
      • push 函数通过调autoreleaseFast函数来执行具体的插入操作
      static inline id *autoreleaseFast(id obj)
      {
          AutoreleasePoolPage *page = hotPage();
          if (page && !page->full()) {// 当前 page 存在且没有满时,直接将对象添加到当前 page 中,即 next 指向的位置
              return page->add(obj);
          } else if (page) {// 当前 page 存在且已满时,创建一个新的 page ,并将对象添加到新创建的 page 中
              return autoreleaseFullPage(obj, page);
          } else {// 当前 page 不存在时,即还没有 page 时,创建第一个 page ,并将对象添加到新创建的 page 中
              return autoreleaseNoPage(obj);
          }
      }
      
    • [对象 autorelease]
      • 本质上就是调用AutoreleasePoolPage的autorelease函数
      __attribute__((noinline,used))
      id
      objc_object::rootAutorelease2()
      {
          assert(!isTaggedPointer());
          return AutoreleasePoolPage::autorelease((id)this);
      }
      
      • AutoreleasePoolPage 的 autorelease 函数的实现对我们来说就比较好理解了,它跟 push 操作的实现非常相似。只不过 push 操作插入的是一个 POOL_SENTINEL ,而 autorelease 操作插入的是一个具体的 autoreleased 对象
      static inline id autorelease(id obj)
      {
          assert(obj);
          assert(!obj->isTaggedPointer());
          id *dest __unused = autoreleaseFast(obj);
          assert(!dest  ||  *dest == obj);
          return obj;
      }
      
    • objc_autoreleasePoolPop(poolToken)
      • objc_autoreleasePoolPop(poolToken) 函数本质上也是调用的AutoreleasePoolPage的 pop 函数
      void
      objc_autoreleasePoolPop(void *ctxt)
      {
          if (UseGC) return;
      
          // fixme rdar://9167170
          if (!ctxt) return;
      
          AutoreleasePoolPage::pop(ctxt);
      }
      
      • pop 函数的入参就是 push 函数的返回值,也就是 POOL_SENTINEL 的内存地址poolToken。当执行pop操作时,在POOL_SENTINEL内存地址在之后添加的所有autoreleased 对象都会被release,可以向前跨越若干个page,直到哨兵对象所在的page,并向回移动next指针哨兵对象所在位置

autoreleasepool的内部结构

  • autoreleasepool本质上就是一个指针堆栈
  • 指针堆栈中存放的是autoreleased对象的内存地址 或者 POOL_SENTINEL的内存地址
  • 内部结构是由若干个以page为结点的双向链表组成,系统会在需要的时候动态地增加或删除page节点,这里说的page就是下面即将说到的AutoreleasePoolPage对象

autoreleasepool的嵌套

  • 每产生一个autoreleasePool,就会产生一个哨兵对象,作为pool的边界
  • pool的嵌套其实就是产生多个哨兵对象而已
  • pop的时候可以向前跨越若干个page,直到指定哨兵对象所在的page为止

AutoreleasePoolPage

  • 一个空的 AutoreleasePoolPage 的内存结构如下图所示:

    AutoreleasePoolPage.png

    • magic用来校验AutoreleasePoolPage的结构是否完整
    • next指向下一个即将产生的autoreleased对象的存放位置(当next == begin()时,表示AutoreleasePoolPage为空;当next == end()时,表示AutoreleasePoolPage已满
    • thread指向当前线程一个AutoreleasePoolPage只会对应一个线程,但一个线程可以对应多个AutoreleasePoolPage;
    • parent指向父结点,第一个结点的 parent 值为 nil;
    • child指向子结点,最后一个结点的 child 值为 nil;
    • depth代表深度,第一个page的depth为0,往后每递增一个page,depth会加1;
    • hiwat代表 high water mark
  • 前面所说的autoreleasepool的内部结构是由若干个AutoreleasePoolPage为结点的双向链表组成,这个双向链表就是通过上述结构中的parent指针child指针连接起来的

  • 每个AutoreleasePoolPage对象会开辟4KB内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autoreleased对象的内存地址

  • 一个page的空间被占满时,会新建一个page,通过parent指针child指针连接链表,之后的autoreleased对象会加入到新建的page中

NSThread、NSRunLoop 和 NSAutoreleasePool三者之间的关系

  • NSThread 和 NSRunLoop是一一对应的关系
  • 在NSRunLoop对象的每个运行循环(event loop)开始前,系统会自动创建一个autoreleasepool,并在运行循环(event loop)结束时drain掉这个pool,同时释放所有autoreleased对象
  • autoreleasepool只会对应一个线程,每个线程可能会对应多个autoreleasepool,比如autoreleasepool嵌套的情况

Autorelease返回值的快速释放机制

  • ARC下,runtime有一套对autorelease返回值的优化策略
  • 通过objc_autoreleaseReturnValueobjc_retainAutoreleasedReturnValue的配合使用可以达到最优化程序运行的目的
  • Thread Local Storage(TLS)线程局部存储,在返回值返回前调用objc_autoreleaseReturnValue方法时,runtime会将这个返回值储存在TLS中,然后直接返回返回值不调用autorelease),
  • 外部接收这个返回值时通过调用objc_retainAutoreleasedReturnValue发现TLS中已存在这个返回值,就直接返回(不调用retain),免去了对返回值的内存管理,达到优化目的

其他Autorelease相关知识点

  • 使用容器的block版本的枚举器时,内部会自动添加一个autoreleasePool
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) 
{ 
      // 这里被一个局部@autoreleasepool{ }包围着
}];
  • 普通for循环for in循环没有,所以,还是新版的block版本枚举器更加方便,但是性能还是for in循环最高

  • 下面三种情况是需要我们手动添加autoreleasepool

    • 如果你编写的程序不是基于 UI 框架的,比如:命令行工具;
    • for循环中遍历产生大量autorelease变量时,就需要手动添加加局部autoreleasePool来进行手动干预
    • 如果你创建了一个子线程,一般会自定义继承自NSOperation的操作,在main方法中要加上@autoreleasepool{...},这段代码是在子线程上执行是无法访问主线程的自动释放池的,所以得自己创建
    - (void)main
    {
        // 自己创建自动释放池,如果这段代码是在子线程上执行是无法访问主线程的自动释放池的,所以得自己创建
        @autoreleasepool
        {
            // 代码逻辑
        }
    }
    

面试题

  • autoreleasepool的实现原理

参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容