自动释放池源码分析

ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool

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

       @autoreleasepool {

       }

}

随后编译器将其改写成下面的样子(clang -rewrite-objc main.m):

报错 main.m:9:9: fatal error: 'UIKit/UIKit.h' file not found,

#import <UIKit/UIKit.h>

尴尬!换个命令 xcrun -sdk iphonesimulator clang -rewrite-objc main.m(指定模拟器/SDK)

如果报错 xcodebuild[3632:229732] [MT] PluginLoading: Required plug-in compatibility UUID E0A62D1F-3C18-4D74-BFE5-A4167D643966 for plug-in at path '~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/CocoaPods.xcplugin' not present in DVTPlugInCompatibilityUUIDs

则Finder 前往这个地址,显示包内存在Info.plist文件的DVTPlugInCompatibilityUUIDs数组中添加一item,值为E0A62D1F-3C18-4D74-BFE5-A4167D643966

~>

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

/* @autoreleasepool */   { __AtAutoreleasePool __autoreleasepool;

     }

}

我们写的@autoreleasepool{}变成了 @autoreleasepool  与 {}两部分,第一部分@autorelease被注释,很明显它的作用对于编译器只是一个标示。第二部分{}


__AtAutoreleasePool 是一结构体

struct __AtAutoreleasePool {

     __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}

     ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}

      void * atautoreleasepoolobj;

};

__AtAutoreleasePool __autoreleasepool;  这一句声明,多少有点C++11 中的trivial的身影(可是看到了结构体中的构造和析构函数,一脸懵逼)

构造函数与类名相同,没有返回值。析构函数与类名相同,前面加上~,没有返回值和参数,对象销毁时自动调用。

未定义构造函数,那么编译器会添加有一个无参的构造函数,而如果定义了构造函数,不会默认生成,如果你还想允许无参构造,就必须显式的声明一个

尽管有点诡异,但是构造函数和析构函数还在那摆着呢。以上代码等价于

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

{

      void * autoreleasepoolobj = objc_autoreleasePoolPush();

      objc_autoreleasePoolPop(autoreleasepoolobj);

}

结构体会在初始化时调用 objc_autoreleasePoolPush() 方法,会在析构时(出作用域)调用 objc_autoreleasePoolPop 方法


@autoreleasepool 只是帮助我们少写了这两行代码而已,让代码看起来更美观。我们下面就要依据这两个方法,展开

void *objc_autoreleasePoolPush(void) {

      return AutoreleasePoolPage::push();

}

void objc_autoreleasePoolPop(void *ctxt) {

      AutoreleasePoolPage::pop(ctxt);

}

AutoreleasePoolPage 的静态方法push 和 pop,两个函数都是对AutoreleasePoolPage的简单封装

AutoreleasePoolPage is who?

AutoreleasePoolPage是一个C++实现的类

class AutoreleasePoolPage {

      magic_t const magic;

      id *next;

      pthread_t const thread;

      AutoreleasePoolPage * const parent;

      AutoreleasePoolPage *child;

      uint32_t const depth;

      uint32_t hiwat;

};

1 magic 用于对当前 AutoreleasePoolPage 完整性的校验

2 AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)

3 AutoreleasePool(自动释放池)并没有单独的结构,而是由若干个AutoreleasePoolPage组合。每一个 AutoreleasePoolPage 的大小都是4096 字节, 前56 bit 用于存储 AutoreleasePoolPage 的成员变量,剩下的用来存储加入到自动释放池中的对象(autorelease对象的地址)

4 上面的next指针作为游标,指向栈顶最新add进来的autorelease对象的下一个位置

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


5 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入

6 AutoreleasePoolPage 是双向链表的形式连接起来的(parent 和 child 用来构造双向链表的指针)

7 begin() 和 end() 这两个类的实例方法获取一个AutoreleasePoolPage的存储autorelease对象地址的区域边界


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

#define POOL_SENTINEL nil

POOL_SENTINEL 只是 nil 的别名,是一个哨兵对象。

static inline void *push() {

       return autoreleaseFast(POOL_SENTINEL);

}

static inline id *autoreleaseFast(id obj)

{

     AutoreleasePoolPage *page = hotPage();

     //有 hotPage 并且 hotPage 不满

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

          //调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中

          return page->add(obj);

      } else if (page) {//有 hotPage 并且hotPage 已满

             return autoreleaseFullPage(obj, page);

      } else {

         //无 hotPage

              return autoreleaseNoPage(obj);   

      }

}

1 objc_autoreleasePoolPush -> AutoreleasePoolPage::push --> autoreleaseFast(POOL_SENTINEL)

因此,调用objc_autoreleasePoolPush,最终调用autoreleaseFast 方法,并传入哨兵对象 POOL_SENTINEL

2 该函数分三种情况选择不同的代码执行(hotPage 可以理解为当前正在使用的 AutoreleasePoolPage)

将哨兵对象添加到hotPage中(page->add 添加对象),对函数整理简化

id *add(id obj) {

     id *ret = next;

     *next = obj;

      next++;

      return ret;

}

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


autoreleaseFast(POOL_SENTINEL),函数返回的是哨兵对象的地址



然后,顺便看下autoreleaseFullPage()与autoreleaseNoPage()函数(page->child指向下一页page)

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

       do { 

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

              else page = new AutoreleasePoolPage(page);

        } while (page->full());

       setHotPage(page);

       return page->add(obj);

}

会从传入的 page 开始遍历整个双向链表,直到查找到一个未满的 AutoreleasePoolPage,否则使用构造器传入 parent 创建一个新的 AutoreleasePoolPage。得到一个可用的 AutoreleasePoolPage 之后,会将该页面标记成 hotPage,然后调动上面分析过的 page->add 方法添加对象


如果不存在 hotPage,就会调用 autoreleaseNoPage 方法初始化一个 AutoreleasePoolPage,从头开始构建双向链表,也就是说,新的 AutoreleasePoolPage 是没有 parent 指针的

static id *autoreleaseNoPage(id obj) {

      AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);

      setHotPage(page);

      if (obj != POOL_SENTINEL) {

//可见成员变量之上必然是哨兵对象,无论传入与否。来确保在 pop 调用的时候,不会出现异常

             page->add(POOL_SENTINEL);

       }

       return page->add(obj);

}

综上,autoreleaseFast()将哨兵对象添加到hotpage中,并返回哨兵对象的地址


3  objc_autoreleasePoolPush之后,autorelease对象必然执行类似操作进栈,纷纷添加到page中,直到objc_autoreleasePoolPop()(参数对应push返回值,哨兵对象的地址)

static inline void pop(void *token) {

    //获取哨兵对象所在page(首地址)

    AutoreleasePoolPage *page = pageForPointer(token);

    id *stop = (id *)token;

    //释放本次push进的对象

     page->releaseUntil(stop);

    //根据当前页的不同状态 kill 掉不同 child 的页面

    if (page->child) {

        if (page->lessThanHalfFull()) {

               page->child->kill();

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

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

         }

     }

}

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

static AutoreleasePoolPage *pageForPointer(const void *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)取模,得到指针在page中的偏移量(因为所有的 AutoreleasePoolPage 在内存中都是对齐的)减去偏移量得到page的首地址,最后调用fastCheck() 用来检查此地址是不是一个 AutoreleasePoolPage(通过检查成员magic)

回到 pop函数,然后 page->releaseUntil(stop); 哨兵对象所在页page调用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);

}

这里,需要明白一点:

可能此次push进page的对象(指针)比较多,哨兵对象所在的page已不是hotpage

但是,由autoreleaseFullPage()可以看出,hotpage必然是哨兵对象所在的page的子节点(子子节点~)

找到当前hotpage,如果已空,通过parent指针遍历双向链表,找到不空的page作为hotpage(这个就是对应上面的情况,将hotpage清空,然后找到父节点继续,直到来到哨兵对象地址)

通过page的next指针, memset将内存的内容设置成 SCRIBBLE,然后使用 objc_release 释放对象,next指针必然前移。继续循环释放对象

每次进栈,add位置依据next指针指向,之后next指向后移。进栈依据next指针,出栈也是依据next指针。出栈之后,之后next指向前移。push可能用到多页page,释放必然也可能释放多页page,因此push进栈涉及到hotpage的变换,pop出栈也涉及到hotpage的切换。

这是一个不断出栈的过程,只要哨兵对象所在页page的next指针不指向此哨兵对象,循环继续


以上,就是源码分析。我们应该知道:

1 如果存在多个自动释放池,那么它们在page之间的界限就是哨兵对象

2 自动释放池的嵌套

@autoreleasepool {//pool 1

      obj1 = xxx autorelease;

       @autoreleasepool {//pool 2

                __autoreleasing obj2 = xxx;

                       @autoreleasepool {//pool 3

                                   obj3 = alloc;

                        }

                }

     }

它们在page中的位置,如下(假设同一个page)


next指针指向

pool 3哨兵对象 nil

obj2地址

pool 2哨兵对象 nil

obj1地址

pool 1哨兵对象 nil


加入释放池,也是依据代码顺序,加入到对应的哨兵对象之后。释放池废弃时,也是先依据代码顺序,先释放最内测的释放池。当然这仅仅是以一个page为例

我们之前说过,RunLoop每次循环会创建自动释放池,这个自动释放必然是很外侧的自动释放池,而我们手动创建的相对于该自动释放池是相对内侧的释放池,该自动释放池释放时,内侧的自动释放池s必然一同释放(循环)

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

推荐阅读更多精彩内容