@autoreleasepool的底层实现

由于markdown会把两个__ 之间的内容当成粗体,所以下文 __ autoreleasing等词语会在 __ 后面加空格

@autoreleasepool本质是一个C++结构体:

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

@autoreleasepool的具体实现:

编译器会把

@autoreleasepool{

}

转换成:

void *atautoreleasepoolobj = objc_autoreleasePoolPush();

__AtAutoreleasePool实例被创建的时候,把创建的对象插入数组,同时兼顾着AutoreleasePoolPage的管理,详情查看https://draveness.me/autoreleasepool - objc_autoreleasePoolPush 方法

{}中的代码

缓存池中创建的对象,编译器不会在对象所在作用域结束的时候插入release消息发送,而是把release留到__AtAutoreleasePool释放时,实现autorelease

objc_autoreleasePoolPop(atautoreleasepoolobj);

__AtAutoreleasePool实例销毁的时候,循环弹出对象调用objc_autorelease把对象release,理论上来说,传入其他对象到objc_autoreleasePoolPop可行的,释放池会释放到传入的对象位置然后结束

autoreleasepool

autoreleasepool的本质是一个队列,由AutoreleasePoolPage组成的双向链表,AutoreleasePoolPage内部又是一个类似于数组的结构

AutoreleasePoolPage

AutoreleasePoolPage是用C++实现的类,每次被创建都会占据4096字节内存(也就是虚拟内存一页的大小),除了存放下面的成员(放在内存低位),其余空间都用来存放@autoreleasepool中添加的对象,一般在私有成员后会再紧跟着存放一个POOL_SENTINEL(详情看下面)作为AutoreleasePoolPage的begin()

成员:

pthread_t const thread;
    //当前指向的线程

id *next;
    //指向当前对象栈的栈顶,如果在pool中对对象发送autorelease,就会插入到对象栈顶部(当前next指向的位置),如果next指向了对象栈的end,那么就会创建新AutoreleasePoolPage

AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
    //双向链表结构

magic_t const magic;
    //用于对当前 AutoreleasePoolPage 完整性的校验

uint32_t const depth;
uint32_t hiwat;

所以objc_autoreleasePoolPush和objc_autoreleasePoolPop只是简单包装AutoreleasePoolPage的操作而已

objc_autoreleasePoolPush会往最后的page中插入一个POOL_SENTINEL对象(哨兵对象,等价于nil)并返回其地址指针(也就是上面的atautoreleasepoolobj,下面简称sentine)

objc_autoreleasePoolPop会从最后一个page的next开始,不停发送release直到遇到第一个sentine为止,之所以说是第一个,是因为存在嵌套的autoreleasepool就会存在多个sentine,所以每一层autoreleasepool结束就只会释放最后一个sentine后面的对象

ARC下runtime对autoreleasepool的优化:

对返回值的优化:
假设现在有:

- (instancetype)createSark {
    return [self new];
}
// caller
Sark *sark = [Sark createSark];

按照autoreleasepool的设计思想(谁创建谁释放的原则),返回值需要是一个autorelease对象才能配合调用方(需要retain)正确管理内存,这个思路下上面的代码会被改写成类似于:

- (instancetype)createSark {
    id tmp = [self new];//这里不会用objc_retainAutoreleasedReturnValue获取
    return objc_autoreleaseReturnValue(tmp); // 代替我们调用autorelease
}
// caller
id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替我们调用retain
Sark *sark = tmp;
objc_storeStrong(&sark, nil); // 相当于代替我们调用了release

但为了减少autoreleasepool的开销,系统会稍微取巧地利用TLS进行优化:

TLS:Thread Local Storage,线程局部存储,将一块内存作为某个线程专有的存储,以key-value的形式进行读写

在调用objc_autoreleaseReturnValue方法时,runtime检测到函数栈中存在objc_retainAutoreleasedReturnValue,就会将返回值object储存在TLS中,然后直接返回这个object(不调用autorelease);同时,在外部接收这个返回值的objc_retainAutoreleasedReturnValue里,发现TLS中正好存了这个对象,就直接获取这个object(不调用retain)。

于是乎,调用方和被调方利用TLS做中转,免去了对返回值的内存管理操作(少了一次autoreleasepool存放,也就少了一次autoreleasepool的retain,返回的地方少了一次release)
这个优化跟C++的右值引用有点类似

与objc_retainAutoreleasedReturnValue相对的是objc_retain函数 , 因为使用了TLS做中转 , 所以objc_retainAutoreleasedReturnValue不需要注册到autoreleasepool中而返回对象,也能够正确地获取对象

ARC中基本上所有创建变量的行为都会使用TLS优化(不管是变量是__strong还是__weak都会),当然也有特例:

  1. 当遇到alloc/new/copy/mutableCopy前缀的类方法时,编译器是不会使用TLS优化的,因为以这些开头的方法遵循内存管理的原则的【自己持有自己生成的对象】原则,所以编译器会为了坚持内存管理原则,不会扩大它们的作用域,该release就会release,所以创建变量的地方也不会通过objc_retainAutoreleasedReturnValue获取

测试下来,通过[[Class alloc]init]创建的对象也不会进行TLS优化
alloc等类方法没有优化的结果是:一般情况下变量在出了作用域就会被回收

  1. 当返回值本身有调用autorelease方法时,不会使用return objc_autoreleaseReturnValue(tmp);

比如返回[NSArray new]时会调用objc_autoreleaseReturnValue,而返回[NSArray array]时会,因为+array内部返回前会调用autorelease方法,已经放到了autoreleasepool就不需要TLS优化了
但如果创建一个变量(默认是__ strong id)缓存,用autorelease的对象赋值给这个变量,然后返回这个变量,还是会使用objc_autoreleaseReturnValue返回的(因为__strong id抵消掉了autorelease带来的优化),如:

+ (id)Object{
    return [NSMutableArray array];

等价于:    
    id obj = [[[NSMutableArray alloc]init]autorelease];
    return obj;
}

+ (id)Object{
    id  obj = [NSMutableArray array];
    return obj;

等价于:
    id obj = [[NSMutableArray alloc]init];
    objc_retainAutoreleasedReturnValue(obj);

    objc_retain(obj);
    objc_storeStrong(&obj,nil);//这里相当于执行release,和前面的objc_retain方法相消,所以这两行会被优化掉
    return objc_autoreleaseReturnValue(obj);

上面优化后等价于:
    id obj = [[NSMutableArray alloc]init];
    return objc_autoreleaseReturnValue(obj);
}

内存管理原则,

任何内存行为都至少会遵循其中一种:

自己生成的对象,自己所持有。
非自己生成的对象,自己也能持有。
不需要自己持有的对象是释放。
非自己持有的对象无法释放

而上面的+createSark,并不遵循上面的原则所以会用TLS优化
而类似于id obj = [[testObj create] getObj];的写法,实际上会调用两次objc_retainAutoreleasedReturnValue
如果不创建obj,即只调用[[testObj create] getObj];则只有调用一次

涉及到ARC和MRC混编

系统会再通过__builtin_return_address这个内建函数判断是否有调用过objc_autoreleaseReturnValue,如果有就是ARC,通过上面的TLS判断需不需要retain,如果没有就是MRC,直接retain一次

char *__builtin_return_address(int level)//传入的level从0开始代表本函数,1代表调用的函数,以此类推

判断是否有用TLS优化的办法:

创建对象后通过_objc_rootRetainCount(obj)函数查看对象的计数器,一般来说如果为2则代表是TLS优化,如果为1则为alloc等方法创建的没有使用TLS

关于__autoreleasing

__ autoreleasing 和 __ strong和 __ weak和__ unsafe_unretained是相互对立的

__ autoreleasing是用在ARC下代替autorelease方法,本质是:

__attribute__((objc_ownership(autoreleasing)))

当使用__autoreleasing修饰变量时,编译器会在变量赋值后进行

objc_autorelease(obj);

把这个变量加到autoreleasepool,把变量的释放时机强制设定到autoreleasepool结束(即autoreleasepool持有了该变量),防止编译器优化直接把对象返回而且没做release处理(因为编译器优化,拿到这个变量也不会进行retain,所以就少了一次retain,autoreleasepool会帮忙retain一次)

也就是说,当遇到编译器进行TLS优化时,如果想要避免变量出了当前作用域就被销毁的话,前缀一个 __autoreleasing ,把释放时机改到缓存池结束,就不会崩了

__ autoreleasing的另一个作用时修饰指针的指针,比如最常见的:

NSError **error其实是NSError * __autoreleasing *error:
-(void)performOperationWithError:(NSError *__autoreleasing *)error;

而平常写

NSError *error = nil;
BOOL result = [self performOperationWithError:&error];

编译器会改写成:

NSError *error = nil;
NSError __autoreleasing *tmp = error;
BOOL result = [obj performOperationWithError:&tmp];
error = tmp;

所以error指针本身在这时候也会变化

之所以传入指针的指针需要__ autoreleasing只是为了遵循内存管理原则而已,写成:

-(void)performOperationWithError:(NSError * __ strong *)error;

也是可以正常运行的,但为了遵循内存管理原则还是老老实实写上__ autoreleasing把指针吧

测试下来,改成__strong后,即使把error作为返回值也不会崩,作用域结束后error也能被正常释放

通过汇编查看也只是对*error赋值是使用_objc_release而不是_objc_autorelease和传入&error通过上面的方式改写而已,只能猜测是为了内存占用才使用__autoreleasing把,毕竟传入指针的指针这种行为除了传入一个NSError之外,更多的是用在循环遍历的时候,比如NSArray的enumerateObjectsUsingBlock方法,系统会为block加上@autoreleasepool,降低循环的内存使用

ps:

NSError *error = nil;
NSError **pError = &error;

会报错,因为error是__ stong的,而 pError是 __ autoreleasing,需要手动指明两个变量为同一个类型才不会报错

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

推荐阅读更多精彩内容