autoreleasepool-你懂的,你不懂的这里都有

不管你是新手还是老鸟,工作中肯定逃不开autoreleasepool这个天使,因为她实在是一门艺术,为我们现在的编码省去了很大一部分工作,这里要感谢苹果爸爸对我们这些苹果开发者的贴心照料。废话不多说  @autoreleasepool到底是什么?

@autoreleasepool{}

我们在命令行中使用clang-rewrite-objc main.m让编译器重新改写这个文件:

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);  extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *); struct __AtAutoreleasePool {        __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}                          ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj); }  void * atautoreleasepoolobj;                                                                                                         };                                                                                                                 /* @autoreleasepool */  { __AtAutoreleasePool __autoreleasepool;}

看到了吧,不得不说,苹果对 @autoreleasepool {} 的实现真的是非常巧妙,真正可以称得上是代码的艺术。苹果通过声明一个 __AtAutoreleasePool 类型的局部变量 __autoreleasepool 来实现 @autoreleasepool {} 。

当声明 __autoreleasepool 变量时,构造函数 __AtAutoreleasePool() 被调用,即执行 atautoreleasepoolobj = objc_autoreleasePoolPush(); ;

当出了当前作用域时,析构函数 ~__AtAutoreleasePool() 被调用,即执行 objc_autoreleasePoolPop(atautoreleasepoolobj); 。

也就是说 @autoreleasepool {} 的实现代码可以进一步简化如下:

/* @autoreleasepool */                {    void *atautoreleasepoolobj = objc_autoreleasePoolPush();    // 用户代码,所有接收到 autorelease 消息的对象会被添加到这个 autoreleasepool 中                                               objc_autoreleasePoolPop(atautoreleasepoolobj);                                                                           }

那么关键点来了objc_autoreleasePoolPush()和objc_autoreleasePoolPop()这两个函数就是最主要的接下来我们进一步分析: 

首先还是要先提一下objc4,不知道大家有没有亲自编译过runtime源码,建议没有了解过的童鞋到苹果开源的代码库找一下与你的mac系统相应的objc4源代码自己编译研究一下,从中你会学到很多,下面我总结了几点:

1.NSObject到底是什么。

2.类和对象是怎样构造出来的。

3.category到底什个什么鬼,是何时被创建的,以及为什么category不能添加属性却可以添加方法和protocol。

4.autoreleasepool是如何构造如何工作的。

5.+(void)load方法是如何被调用的。

总而言之只要你肯踏下心来去细细研读,我敢保证你一定会茅塞顿开,对objct-c有一个重新的认识。

扯得有点远,回过头来,我们继续学习autorelease

这时候我们再来看AutoreleasePoolPage的定义(也是出自于NSObject.mm)

class AutoreleasePoolPage {

      magic_t const magic;

      id *next;

      pthread_t const thread;

      AutoreleasePoolPage * const parent;

      AutoreleasePoolPage *child;

      uint32_t const depth;

      uint32_t hiwat;

};

magic:用来校验 AutoreleasePoolPage 的结构是否完整;next:指向栈顶,也就是最新入栈的autorelease对象的下一个位置;thread:指向当前线程;

parent:指向父节点

child:指向子节点

depth:表示链表的深度,也就是链表节点的个数

hiwat:表示high water mark(最高水位标记)每一个自动释放池都是由一系列的AutoreleasePoolPage组成的,并且每一个AutoreleasePoolPage的大小都是4096字节(16 进制 0x1000)

void * objc_autoreleasePoolPush(void) {  

          return AutoreleasePoolPage::push();

}

void objc_autoreleasePoolPop(void *ctxt) {  

       AutoreleasePoolPage::pop(ctxt);

}

这是NSObject.mm中的两个函数,另外需要说明一下runtime源码大部分都是C++和C语言混编的,如果研读的话,建议先了解一下C++的基本语法。

我们继续跟进

static inline void *push()      {         id *dest;        

        if (DebugPoolAllocation) {             // Each autorelease pool starts on a new pool page.                      dest = autoreleaseNewPage(POOL_BOUNDARY);      

        } else {      

               dest = autoreleaseFast(POOL_BOUNDARY);      

       }      

       assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);      

       return dest;  

  }

我们先看else中的autoreleaseFast()函数,我们继续在源码中寻找

static inline id *autoreleaseFast(id obj)     {        

       AutoreleasePoolPage *page = hotPage();        

      if (page && !page->full()) {            

           return page->add(obj);        

       } else if (page) {          

          return autoreleaseFullPage(obj, page);    

    } else {          

          return autoreleaseNoPage(obj);      

    }    

}

到了这里,你可能想要知道POOL_BOUNDARY到底是什么,还有它为什么在栈中。

首先回答第一个问题:POOL_BOUNDARY只是nil的别名。

#define POOL_BOUNDARY nil  //分界对象 (以前叫POOL_SENTINEL(哨兵对象)总之是一样的作用)

在每个自动释放池初始化调用objc_autoreleasePoolPush的时候,都会把一个POOL_BOUNDARY push 到自动释放池的栈顶,并且返回这个POOL_BOUNDARY边界对象。

int main(int argc, const char * argv[]) {

     { void * atautoreleasepoolobj = objc_autoreleasePoolPush(); // do whatever you want             objc_autoreleasePoolPop(atautoreleasepoolobj);

    }

    return 0;

}

上面的atautoreleasepoolobj就是一个POOL_BOUNDARY。

而当方法objc_autoreleasePoolPop调用时,就会向自动释放池中的对象发送release消息,直到第一个POOL_BOUNDARY:(以下的图中POOL_SENTINEL均为POOL_BOUNDARY为了省事直接粘的图,大家不要学我😯)

autoreleaseFast 函数在执行一个具体的插入操作时,分别对三种情况进行了不同的处理:

上述方法分三种情况选择不同的代码执行:

1.有hotPage并且当前page不满,调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中

2.有hotPage并且当前page已满,调用autoreleaseFullPage初始化一个新的页,调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中

3.无hotPage,调用autoreleaseNoPage创建一个hotPage,调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中,最后的都会调用page->add(obj)将对象添加到自动释放池中。

hotPage可以理解为当前正在使用的AutoreleasePoolPage。

每调用一次 push 操作就会创建一个新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一个 POOL_BOUNDARY ,并且返回插入的 POOL_BOUNDARY 的内存地址。

page->add 添加对象

id *add(id obj)将对象添加到自动释放池页中:

id*add(id obj){

     id*ret=next;*next=obj;next++;

     returnret;

}

笔者对这个方法进行了处理,更方便理解。

这个方法其实就是一个压栈的操作,将对象加入AutoreleasePoolPage然后移动栈顶的指针。

autoreleaseFullPage(当前 hotPage 已满)

autoreleaseFullPage会在当前的hotPage已满的时候调用:

staticid*autoreleaseFullPage(id obj,AutoreleasePoolPage*page){

do{

     if(page->child)page=page->child;

     else page=newAutoreleasePoolPage(page);

 }while(page->full());

 setHotPage(page);

returnpage->add(obj);

}

它会从传入的page开始遍历整个双向链表,直到:

查找到一个未满的AutoreleasePoolPage

使用构造器传入parent创建一个新的AutoreleasePoolPage

在查找到一个可以使用的AutoreleasePoolPage之后,会将该页面标记成hotPage,然后调动上面分析过的page->add方法添加对象。

autoreleaseNoPage(没有 hotPage)

如果当前内存中不存在hotPage,就会调用autoreleaseNoPage方法初始化一个AutoreleasePoolPage:

staticid*autoreleaseNoPage(id obj){

      AutoreleasePoolPage*page=newAutoreleasePoolPage(nil);

     setHotPage(page);

     if(obj!=POOL_BOUNDARY){

     page->add(POOL_BOUNDARY);}

     returnpage->add(obj);

}

既然当前内存中不存在AutoreleasePoolPage,就要从头开始构建这个自动释放池的双向链表,也就是说,新的AutoreleasePoolPage是没有parent指针的。

初始化之后,将当前页标记为hotPage,然后会先向这个page中添加一个POOL_BOUNDARY对象,来确保在pop调用的时候,不会出现异常。

最后,将obj添加到自动释放池中。

objc_autoreleasePoolPop 方法

同样,回顾一下上面提到的objc_autoreleasePoolPop方法:

void objc_autoreleasePoolPop(void*ctxt){

       AutoreleasePoolPage::pop(ctxt);

}

看起来传入任何一个指针都是可以的,但是在整个工程并没有发现传入其他对象的例子。不过在这个方法中传入其它的指针也是可行的,会将自动释放池释放到相应的位置。

我们一般都会在这个方法中传入一个边界对象POOL_BOUNDARY,如下图一样释放对象:

对 objc_autoreleasePoolPop 行为的测试

在继续分析这个方法之前做一个小测试,在objc_autoreleasePoolPop传入非边界对象,测试一下这个方法的行为。

下面是main.m文件中的源代码:

int main(intargc,constchar*argv[]){

      @autoreleasepool{

              NSString*s=@"Draveness";

              [s stringByAppendingString:@"-Suffix"];

}

       return0;

}

在代码的这一行打一个断点,因为这里会调用autorelease方法,将字符串加入自动释放池:

当代码运行到这里时,通过 lldb 打印出当前hotPage中的栈内容:

通过static方法获取当前hotPage

打印AutoreleasePoolPage中的内容

打印当前next指针指向的内容,以及之前的内容,-2时已经到了begin()位置

使用print()和printAll()打印自动释放池中内容

然后将字符串@"Draveness-Suffix"的指针传入pop方法,测试pop方法能否传入非边界对象的参数。

再次打印当前AutoreleasePoolPage的内容时,字符串已经不存在了,这说明向pop方法传入非哨兵参数是可行的,只是我们一般不会传入非哨兵对象。

让我们重新回到对objc_autoreleasePoolPop方法的分析,也就是AutoreleasePoolPage::pop方法的调用:

static inline void pop(void*token){

      AutoreleasePoolPage*page=pageForPointer(token);

      id*stop=(id*)token;page->releaseUntil(stop);

      if(page->child){

           if(page->lessThanHalfFull()){

                 page->child->kill();       

           }else if(page->child->child){

                  page->child->child->kill();

           }

    }

}

在这个方法中删除了大量无关的代码,以及对格式进行了调整。

该静态方法总共做了三件事情:

使用pageForPointer获取当前token所在的AutoreleasePoolPage

调用releaseUntil方法释放栈中的对象,直到stop

调用child的kill方法

我到现在也不是很清楚为什么要根据当前页的不同状态kill掉不同child的页面。

if(page->lessThanHalfFull()){

      page->child->kill();

}else if(page->child->child){

      page->child->child->kill();

}

pageForPointer 获取 AutoreleasePoolPage

pageForPointer方法主要是通过内存地址的操作,获取当前指针所在页的首地址:

static AutoreleasePoolPage*pageForPointer(constvoid*p){

        return pageForPointer((uintptr_t)p);

}

static AutoreleasePoolPage*pageForPointer(uintptr_t p){

      AutoreleasePoolPage*result;

      uintptr_t offset=p%SIZE;

      assert(offset>=sizeof(AutoreleasePoolPage));

      result=(AutoreleasePoolPage*)(p-offset);

      result->fastcheck();

      return result;

}

将指针与页面的大小,也就是 4096 取模,得到当前指针的偏移量,因为所有的AutoreleasePoolPage在内存中都是对齐的:

p = 0x100816048

p % SIZE = 0x48

result = 0x100816000

而最后调用的方法fastCheck()用来检查当前的result是不是一个AutoreleasePoolPage。

通过检查magic_t结构体中的某个成员是否为0xA1A1A1A1。

releaseUntil 释放对象

releaseUntil方法的实现如下:

void releaseUntil(id*stop){

       while(this->next!=stop){

              AutoreleasePoolPage*page=hotPage();

              while(page->empty()){page=page->parent;

                      setHotPage(page);

              } page->unprotect();

             id obj=*--page->next;

             memset((void*)page->next,SCRIBBLE,sizeof(*page->next));

             page->protect();

             if(obj!=POOL_SENTINEL){objc_release(obj);

             }

      }

      setHotPage(this);

}

它的实现还是很容易的,用一个while循环持续释放AutoreleasePoolPage中的内容,直到next指向了stop。

使用memset将内存的内容设置成SCRIBBLE,然后使用objc_release释放对象。

kill() 方法

到这里,没有分析的方法就只剩下kill了,而它会将当前页面以及子页面全部删除:

void kill(){

       AutoreleasePoolPage*page=this;

       while(page->child)page=page->child;

       AutoreleasePoolPage*deathptr;

      do{

             deathptr=page;page=page->parent;

             if(page){

             page->unprotect();

             page->child=nil;

             page->protect();

             }

             delete deathptr;

      }while(deathptr!=this);

}

双向链表

自动释放池中的AutoreleasePoolPage是以双向链表的形式连接起来的:

parent和child就是用来构造双向链表的指针。

自动释放池中的栈

如果我们的一个AutoreleasePoolPage被初始化在内存的0x100816000~ 0x100817000中,它在内存中的结构如下:

其中有 56 bit 用于存储AutoreleasePoolPage的成员变量,剩下的0x100816038~ 0x100817000都是用来存储加入到自动释放池中的对象。

begin()和end()这两个类的实例方法帮助我们快速获取0x100816038~ 0x100817000这一范围的边界地址。

next指向了下一个为空的内存地址,如果next指向的地址加入一个object,它就会如下图所示移动到下一个为空的内存地址中:

关于hiwat和depth在文章中并不会进行介绍,因为它们并不影响整个自动释放池的实现,也不在关键方法的调用栈中。

autorelease

在ARC中这是我们最常见的,但是它和autoreleasepool又有什么关系呢?下面我们先看一下MRC下autorelease的调用栈如下所示:(注意粗体字的方法调用)

- [NSObject autorelease]

└── id objc_object::rootAutorelease() └── id objc_object::rootAutorelease2()

└── static id AutoreleasePoolPage::autorelease(id obj)

└── static id AutoreleasePoolPage::autoreleaseFast(id obj)

├── id *add(id obj)

├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)

│ ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)

│ └── id *add(id obj)

└── static id *autoreleaseNoPage(id obj

├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)

└── id *add(id obj)

我们很兴奋的发现autoreleaseFast(id obj),对,就是他,这不就是autoreleasepool中的实现代码吗,哈哈,到这里你就都明白了,在autorelease方法的调用栈中,最终都会调用上面提到的autoreleaseFast方法,将当前对象加到AutoreleasePoolPage中。

这些方法的实现都非常容易,只是进行了一些参数上的检查,最终还要调用autoreleaseFast方法讲到这里我相信你应该已经明白autoreleasepool到底是咋回事了吧,老铁们,欢迎评论,指正。

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

推荐阅读更多精彩内容