Objective-C内存篇(三) - Autorelease Pool的实现原理

什么是自动释放池

自动释放,也是延迟释放。
自动释放池的实现原理或者说作用:在自动释放池被销毁或耗尽时,会向池中的所有对象发送release消息,释放所有autorelease对象。

自动释放池中的使用

//MRC下
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    id obj = [[NSObject alloc] init];
    [obj autorelease];
    [pool drain];   //相当于[obj release];
//ARC下
    @autoreleasepool{
        NSArray __autorelasing * arr = [[NSArray alloc] init];
        或者
        NSArray * arr = [NSArray arrayWithObject:@""]; // alloc/new/copy/mutableCopy之外的方法,生成的对象,默认是自动释放池管理(MRC与ARC下)
    }

Autorelease Pool的实现原理

# NSAutoreleasePool对应的AutoreleasePoolPage类 - 相关代码

ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool,随后clang -rewrite-objc可编译成下面代码:

//被编译的代码
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
{
    @autoreleasepool{
        id obj = [[NSObject alloc] init];
    }
    return 0;
}

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;
};

//略作改动,对应MRC下的代码
NSAutoreleasePool *pool=[[NSAutoreleasePool alloc) init];/*等同于objc autoreleasePoolPush ( ) */
id obj = [[NSObject alloc) init];
[obi autorelease];/*等同于objc autorelease (obi ) */
[pool drain];/*等同于obic autoreleasePoolPop (pool ) */

而这几个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类

可通过objc4库的runtime/objc-arr.mm来确认苹果中autorelease的实现。
objo4/runtime/objc-arr.mm class AutoreleasePoolPage:

class AutoreleasePoolPage
{
    static inline void *push (){  //相当于生成或持有NSAutoreleasePool类对象; 
    }
    static inline void *pop (void *token){  //相当于废弃NSAutoreleasePool类对象; 
        releaseAll(); 
    }
    static inline id autorelease (id obj){  //相当于NSAutoreleasePoo1类的addobject类方法
        AutoreleasePoolPage *autoreleasePoolPage =取得正在使用的AutoreleasePoolPage实例;
        autoreleasePoolPage->add (obj );
    }
    id *add (id obj){//将对象追加到内部数组中;
    }
    void releaseAll (){//调用内部数组中对象的release实例方法;
    }
};

void *objc autoreleasePoolPush (void){
    return AutoreleasePoolPage: :push ();
}
void objc autoreleasePoolPop (void *ctxt){
    AutoreleasePoolPage: :pop (ctxt);
}
id *objc autorelease (id obj){
    return AutoreleasePoolPage: :autorelease (obj);
}

# AutoreleasePoolPage类

AutoreleasePoolPage是一个C++实现的类


AutoreleasePoolPage
  • AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage作为结点双向链表的形式组合而成,会在一个Page空间占满时进行增加,objc_autoreleasePoolPop(哨兵对象)的时候进行删除
  • AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址、以及哨兵对象的地址(见下文 释放时机 部分)

参数解读:

  • parent 指向父结点,第一个结点的 parent 值为 nil ;
  • child 指向子结点,最后一个结点的 child 值为 nil ;
  • thread指针指向当前线程,每个AutoreleasePool只对应一个线程
  • id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置,初始化时指向begin();
  • magic 用来校验 AutoreleasePoolPage 的结构是否完整
  • depth 代表深度,从 0 开始,往后递增 1;
AutoreleasePoolPage 的存储结构

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

# 释放机制

每当进行一次objc_autoreleasePoolPush调用时,runtime就向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),那么这一个page就变成了下面的样子:

image

objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是:

  1. 根据传入的哨兵对象地址找到哨兵对象所处的page
  2. 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
  3. 补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page

# 嵌套的AutoreleasePool

pop的时候总会释放到上次push的位置(上次push时返回的哨兵对象地址)为止,多层的pool就是多个哨兵对象而已,互不影响。

__autoreleasing所有权修饰符

ARC有效时,用@autoreleasepool块替代NSAutoreleasePool类,用附有__autoreleasing修饰符的变量替代autorelease方法

  • id的指针或对象的指针在没有显示指定时会被附加上__autoreleasing修饰符,赋值给对象指针时,所有权修饰符必须一致
  • 只能自动变量,才可以显式指定__autoreleasing修饰符(包括局部变量、函数、方法参数)

加入自动释放池的几种方法

  • 调用autorelease的对象(MRC下)、用__autoreleasing修饰的对象(ARC下)
  • 用alloc/new/copy/mutableCopy之外的方法,生成的对象,默认是自动释放池管理(MRC与ARC下)
NSArray * array = [NSArray arrayWithCapacity: 1];
等同于
NSArray * array = [[[NSArray alloc] initWithCapacity:1] autorelease];
  • __weak的变量指向一个__strong的对象,每次使用这个变量的时候,都会把这个变量加入到自动释放池中一次(ARC下)
因为_weak 修饰符只持有对象的弱引用,而且在访问引用对象的过程中,该对象可能被废弃。如果把要访问的对象注册到autoreleasepool中,那么在@autoreleasepool块结束之前都能确保对象的存在。

# 打印自动释放池中的对象

可通过NSAutoreleasePool类中的调试用非公开类方法showPools来确认已被autorelease的对象的状况。showPools会将现在的NSAutoreleasePool的状况输出到控制台。

[NSAutoreleasePool showPools];  

或者直接使用_objc_autoreleasePoolPrint()函数来打印(无论ARC是否有效)

/* 函数声明 */
extern _objc_autoreleasePoolPrint();

/* 调用 */
_objc_autoreleasePoolPrint();

自动释放池的释放时机

  • 主线程中的最外层@autoreleasepool {} :
    runloop默认开启,每一次运行循环开始,也就是每当事件被触发时都会创建自动释放池。运行循环结束前会释放自动释放池,还有池子满了也会销毁。(无论ARC是否有效,NSRunloop都能随时释放注册到autoreleasepool中的对象
  • 子线程中的最外层@autoreleasepool {} :
    runloop默认不开启,不会自动创建自动释放池,在需要使用自动释放池的时候,需要我们手动创建、添加自动释放池,此时如果所有的异步代码都写在自动释放池中,也可以理解为当子线程销毁的时候,自动释放池释放
  • 线程中在一些代码场景中,自己创建的自动释放池,比如
1. 生成大量的临时变量
2. 生成大容量对象
    1. UIImage转NSData : UIImageJPEGRepresentation / UIImagePNGRepresentation 这两个方法在转为NSData的时候,这些Data都会写到内存中,如果图片太多,太大,就会导致内存暴涨
    2. [UIImage imageNamed: ]  会读入内存,所以相对的,速度也是最快的,Interface Builder(sb,xib)就是通过这个方法来加载的,图片被缓存,导致内存过大
以上这些,都需要我们及时清理对象、内存,避免造成内存占用过高

for (int i = 0; i < 10000; ++i) {
       @autoreleasepool{
          NSString *str = @"Hello World";
          str = [str stringByAppendingFormat:@"- %d",i];
        } //此时,自动释放池的释放时机就是在此处:大括号完成的时候
 }

注意:使用容器的block版本的枚举器时,内部会自动添加一个AutoreleasePool:
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // 这里被一个局部@autoreleasepool包围着
}];

参考链接:
http://blog.sunnyxx.com/2014/10/15/behind-autorelease
http://www.cocoachina.com/ios/20150610/12093.html
参考书籍:《Objective-C 高级编程》

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