AutoreleasePool

Objective-C Autorelease Pool 的实现原理

自动释放池的前世今生 ---- 深入解析 Autoreleasepool

AutoreleasePool是通过一个以AutoreleasePoolPage为结点的双向链表来实现的。AutoreleasePoolPage满后会自动创建下一个AutoreleasePoolPage。

Autorelease Pool Blocks

使用clang -rewrite-objc命令将下面的Objective-C代码重写成C++代码:

@autoreleasepool {

}

只保留了部分代码:

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时,构造函数被执行,即执行atautoreleasepoolobj = objc_autoreleasePoolPush();。当出了当前作用域时(即大括号),析构函数~__AtAutoreleasePool()被执行,即调用用objc_autoreleasePoolPop(atautoreleasepoolobj);。(微信团队对特殊字符的处理也用到了相同的技巧)。也就是说@autoreleasepool{}的代码实现可以简化为如下:

/* @autoreleasepool */ {

void *atautoreleasepoolobj = objc_autoreleasePoolPush();

 // 用户代码,所有接收到 autorelease 消息的对象会被添加到这个 autoreleasepool 中

    objc_autoreleasePoolPop(atautoreleasepoolobj);

}

objc_autoreleasePoolPush和objc_autoreleasePoolPop的实现:

void *objc_autoreleasePoolPush(void) {

    return AutoreleasePoolPage::push();

}

void objc_autoreleasePoolPop(void*ctxt) { 

    AutoreleasePoolPage::pop(ctxt);

}

由此可见内部其实调用的是AutoreleasePoolPage中相关的方法

那这里的 AutoreleasePoolPage 是什么东西呢?

其实,autoreleasepool 是没有单独的内存结构的,它是通过以 AutoreleasePoolPage 为结点的双向链表来实现的。

一个空的AutoreleasePool的内存结构如下:


1. magic 用来校验 AutoreleasePoolPage 的结构是否完整;
2. next 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() ;
3. thread 指向当前线程;
4. parent 指向父结点,第一个结点的 parent 值为 nil ;
5. child 指向子结点,最后一个结点的 child 值为 nil ;
6. depth 代表深度,从 0 开始,往后递增 1;
7. hiwat 代表 high water mark 。

当next==begin()时,表示AutoreleasePage为空,当next==end()时表示AutoreleasePage已满。每一个AutoreleasePage的大小都是4096字节

。我们打开 runtime 的源码工程,在 NSObject.mm 文件的第 438-932 行可以找到 autoreleasepool 的实现源码。

通过阅读源码,我们可以知道:

每一个线程的 autoreleasepool 其实就是一个指针的堆栈;

每一个指针代表一个需要 release 的对象或者 POOL_SENTINEL(哨兵对象,代表一个 autoreleasepool 的边界);

一个 pool token 就是这个 pool 所对应的 POOL_SENTINEL 的内存地址。当这个 pool 被 pop 的时候,所有内存地址在 pool token 之后的对象都会被 release ;

这个堆栈被划分成了一个以 page 为结点的双向链表。pages 会在必要的时候动态地增加或删除;

Thread-local storage(线程局部存储)指向 hot page ,即最新添加的 autoreleased 对象所在的那个 page 。

双向链表:


push操作

    其实是创建一个新的AutoreleasePool,对应AutoreleasePoolPage就是在AutoreleasePoolPage的next位置插入一个POOL_SENTINEL指针(哨兵指针)。next移动至下一个地址。POOL_SENTINEL作为push函数的返回值。

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

autorelease操作

其实就是将自己的地址插入AutoreleasePoolPage。

[obj autorelease],NSObject中-autorelease方法的实现:

- (id)autorelease {

return ((id)self)->rootAutorelease();

}

通过查看((id)self)->rootAutorelease()的方法调用,发现调用的其实是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;

}

Pop操作

同理,前面提到的objc_autoreleasePoolPop(void *) 函数本质上也是调用的AutoreleasePoolPage的pop函数。

void objc_autoreleasePoolPop(void *ctxt)

{

if (UseGC) return;

 // fixme rdar://9167170

if (!ctxt) return;

    AutoreleasePoolPage::pop(ctxt);

}

pop函数的参数就是push的返回值,也就是POOL_SENTINEL。当执行pop操作时,内存地址POOL_SENTINEL之后的对象都会被release。直到POOL_SENTINEL所在page的next指向POOL_SENTINEL为止。

下图是某个线程的AutoreleasePool堆栈的内存结构图,在这个堆栈中有两个POOL_SENTINEL,也就是表示有两个autoreleasepool。该堆栈由三个AutoreleasePoolPage节点组成,第一个AutoreleasePoolPage为ColdPage(),最后一个为HotPage()。其中前两个AutoreleasePoolPage已经满了,最后一个结点保存了最新添加的autoreleased对象objr3的内存地址。


如果执行pop(token1)操作,那么该堆栈的内存结构将会变成如下图所示:


第二个POOL_SENTINEL指针之后的对象调用release方法,内存得到释放。

什么情况下需要手动添加AutoreleasePool:

Each thread in a Cocoa application maintains its own stack of autorelease pool blocks. 

If you are writing a Foundation-only program or if you detach a thread, you need to create your own autorelease pool block.

If your application or thread is long-lived and potentially generates a lot of autoreleased objects, you should use autorelease pool blocks (like AppKit and UIKit do on the main thread); otherwise, autoreleased objects accumulate and your memory footprint grows. If your detached thread does not make Cocoa calls, you do not need to use an autorelease pool block.

1.有大量的零时对象,例如编写的循环中创建了大量的零时对象

2.编写的的程序不是基于UI框架的,比如说命令行工具

3.创建一个辅助线程

AutoreleasePool的创建和销毁

即将进入Runloop的时候创建AutoreleasePool。

RunLoop即将进入休眠的时候释放并创建新的Runloop。

即将退出Runloop的时候释放自动释放池。

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush()创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠)时调用_objc_autoreleasePoolPop()和 _objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop)时调用_objc_autoreleasePoolPop()来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

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

推荐阅读更多精彩内容