autoreleasepool探究

引言

开始之前先看一段代码,猜猜输出结果是什么?

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        __weak id refStr1 = nil;
        __weak id refStr2 = nil;
        @autoreleasepool {
            NSString *str = [NSString stringWithFormat:@"hello"];
            refStr1 = str;
            refStr2 = [str stringByAppendingString:@" world"];
        }
        NSLog(@"refStr1:%@",refStr1);
        NSLog(@"refStr2:%@",refStr2);
    }
    return 0;
}

测试输出结果如下:

refStr1:hello
refStr2:(null)

refStr1为何不是空,不合常理啊!难道我所理解的自动释放池是错的!autoreleasepool到底做了什么?

autoreleasepool探究

我们知道自动释放池用于存放那些稍后在某个时刻需要释放的对象,清空自动释放池,会向其中的对象发送release消息,释放对象。上面测试中refStr1是个弱引用,不会递增str引用计数,autoreleasepool作用域结束后,str应该释放,但refStr1仍会输出"hello"。难道str没有被释放?str有没有被加入到自动释放池中?autoreleasepool本质又是什么?
使用clang命令转化为C++代码,如果当前环境不支持weak引用,可将weak声明改为__unsafe_unretained后再执行转换,__unsafe_unretained也不会增加对象的引用计数,但所指向的对象释放后,其值不会置为空,再访问可能导致意想不到的错误

 clang -rewrite-objc main.m

找到main入口函数

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

        __attribute__((objc_ownership(none))) id refStr1 = __null;
        __attribute__((objc_ownership(none))) id refStr2 = __null;


        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
            NSString *str = ((NSString *(*)(id, SEL, NSString *, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders_1g_jhwq37q510q1d75qmq_bhrfc0000gn_T_main_8dcd7c_mi_0);
            refStr1 = str;
            refStr2 = ((NSString *(*)(id, SEL, NSString *))(void *)objc_msgSend)((id)str, sel_registerName("stringByAppendingString:"), (NSString *)&__NSConstantStringImpl__var_folders_1g_jhwq37q510q1d75qmq_bhrfc0000gn_T_main_8dcd7c_mi_1);
        }
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_1g_jhwq37q510q1d75qmq_bhrfc0000gn_T_main_8dcd7c_mi_2,refStr1);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_1g_jhwq37q510q1d75qmq_bhrfc0000gn_T_main_8dcd7c_mi_3,refStr2);
    }
    return 0;
}

其中@autoreleasepool{}块被转换成了{__AtAutoreleasePool __autoreleasepool;},那__AtAutoreleasePool又是什么呢?继续查找其定义

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

__AtAutoreleasePool是个结构体,定义很简单,一个构造函数,一个析构函数,一个指针。其中在构造函数中调用了objc_autoreleasePoolPush(),在析构函数中调用了objc_autoreleasePoolPop(atautoreleasepoolobj)。至于atautoreleasepoolobj又是什么东东,下文再说。可以看出@autoreleasepool{}其实等价于下面代码

void* pt =objc_autoreleasePoolPush();
//your code
objc_autoreleasePoolPop(pt);

objc_autoreleasePoolPush与objc_autoreleasePoolPop这两个函数实现很简单,分别调用了AutoreleasePoolPage的静态方法push与pop

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

终于揭开autoreleasepool的神秘面纱,原来AutoreleasePoolPage才是其中的核心,其主要结构如下:

class AutoreleasePoolPage
{
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
}

至于AutoreleasePoolPage的具体实现,在此不再做详细介绍,具体可参考sunnyxx《黑幕背后的Autorelease》和Draveness的《自动释放池的前世今生》。这里主要引述其结论:

  • AutoreleasePool与线程一一对应,结构中的thread指向当前线程
  • AutoreleasePoolPage每个对象内存大小为4096字节,除了存储其本身的实例变量外,剩下的空间用来储存加入到自动释放池中的对象的地址
  • AutoreleasePoolPage以双向链表的形式组合,parent指针指向上一个page,child指针指向下一个page
  • 每次调用objc_autoreleasePoolPush时,会返回一个哨兵对象,也就是上文提到的autoreleasepoolobj,指向当前AutoreleasePoolPage中next指针指向的地址。
  • 向一个对象发送autorelease消息,会把这个对象的地址加入到当前AutoreleasePoolPage中next指针指向的位置,之后next指针指向新加入对象的下一位置
  • 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,child指针指向新建的page,后来加入到自动释放池中的对象添加到新的page
  • 自动释放池释放时,根据push时创建的哨兵对象,找到对应的自动释放池。从最新加入的对象一直向前清理(发送release消息),可以向前跨越若干个page,直至哨兵对象所指向的地址。

AutoreleasePoolPage调试

了解autoreleasepool的原理后,回到开始的问题,我们的疑惑还没解决。结合调试,来看看到底发生了生么?调试需要编译后objc源码,有网友已编译好了,这里下载
修改开始的代码,输出refStr1、refStr2地址,便于对照

@autoreleasepool {
  NSString *str = [NSString stringWithFormat:@"hello"];
  refStr1 = str;
  refStr2 = [str stringByAppendingString:@" world"];
  NSLog(@"refStr1=%p,refStr2=%p",refStr1,refStr2);
}

在大括号结束前插入断点,在控制台执行下面命令

expression AutoreleasePoolPage::hotPage() //获取当前AutoreleasePoolPage指针$0
p *$0 //查看AutoreleasePoolPage结构
p $0->printAll() //输出AutoreleasePoolPage信息

运行结果如下图



我们发现str并没有被加入到自动释放池中,所以refStr1最后仍能输出"hello"。为何str没有被加入到自动释放池中呢?
记得以前在《Effective Objective-C》中看过一段话(在"不要使用retainCount"一节),当时只做了标注,并未深思,至此方有所悟。

  • 系统会尽可能把NSString实现成单例对象,这种对象的保留及释放操作都是'空操作'。编译器会把NSString对象所表示的数据放到应用程序的二进制文件里,这样的话,运行程序时就可以直接使用了,无须再创建NSString对象。
  • NSNumber也类似,它使用了一种叫做'标签指针'(tagged pointer)的概念来标注特定类型的数值。这种做法不使用NSNumber对象,而是把与数值有关的全部消息都放在指针里面。运行期系统会在消息派发期间检测到这种标签指针,并对它执行相应操作,使其行为看上去和真正的NSNumber对象一样。这种优化只在某些场合使用,同样是NSNumber对象,整数做了优化,浮点数对象就没有优化。

修改上面代码,又进行了测试,果然如此!

__weak id refStr1 = nil;
__weak id refStr2 = nil;
__weak id refNum1 = nil;
__weak id refNum2 = nil;
__weak id refObj = nil;
@autoreleasepool {
  NSString *str = [NSString stringWithFormat:@"hello"];
  refStr1 = str;
  refStr2 = [str stringByAppendingString:@" world"];

  NSNumber *number1 = [NSNumber numberWithInt:32];
  NSNumber *number2 = [NSNumber numberWithFloat:3.2];
  refNum1 = number1;
  refNum2 = number2;

  NSObject *obj = [NSObject new];
  refObj = obj;
}
NSLog(@"refStr1:%@",refStr1);
NSLog(@"refStr2:%@",refStr2);
NSLog(@"refNum1:%@",refNum1);
NSLog(@"refNum2:%@",refNum2);
NSLog(@"refObj:%@",refObj);

输出结果

refStr1:hello
refStr2:(null)
refNum1:32
refNum2:(null)
refObj:(null)

至此,我们解决了开始的疑惑。

结论

  • autorelease块在开始和结束时,分别调用了objc_autoreleasePoolPush和objc_autoreleasePoolPop方法
  • 自动释放池功能由AutoreleasePoolPage类实现,向一个对象发送autorelease消息,会把对象对象加入到当前的AutoreleasePoolPage中。
  • 自动释放池清理时,会从当前最新加入的对象开始,直至push时创建的哨兵对象结束。
  • NSString和NSNumber部分对象的保留及释放操作可能是空操作,释放时不会被加入到自动释放池。

结语

刚看sunnyxx的《黑幕背后的Autorelease》,感觉甚是深奥,不解其义,自己的测试结果也与文中开始实验的结果不同,未明白是怎么回事。其实sunnyxx的文章发布至今三年多都过去了,苹果说不一定已做了优化。其测试环境是真机还是模拟器,也不得而知,不同的测试环境,其结果也可能会有差异。
后来又读到Draveness的《自动释放池的前世今生》,学习了其中的调试技巧,结合调试、测试,终于搞明白了autoreleasepool的原理,解决了以前的困惑,但目前所知只是一角。
纸上得来终觉浅,绝知此事要躬行。自己动动手,你所学到的远比你看到的多!

思考

留个问题,ref1、ref2分别在什么时候释放?
在viewDidLoad方法结束时释放?还是在当前RunLoop即将休眠或结束时释放?或者不会释放?
诸位怎么看

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

推荐阅读更多精彩内容