[iOS] __autorelease的碎碎念&疑惑

这个事儿我大概四五月之前想写来着,拖了这么久也是醉了。。我觉得我最近脑子基本是废了

※ 1. __autoreleasing + pool

- (void)testAutoRelease
{
    __autoreleasing UIView* myView;
    
    @autoreleasepool {
        myView = [UIView new];
    }
    NSLog(@"outside autoreleasepool myView:%@", myView);
}

这段代码执行的结果是啥呢? => 代码会在 NSLog 的地方crash哈

如果把 __autoreleasing UIView* myView; 改成 UIView* myView; 就不会crash啦,或者保留autorelease修饰但把autoreleasepool去掉也是不会crash的。

autorelease pool 大家应该都很熟悉了(如果不是很清楚它是啥可以参考://www.greatytc.com/p/b6cfbeabfb14),但是 __autoreleasing 好像除了在 error 的地方遇到就很少看到啦,上面的小实验说明了啥呢?

其实上面的代码可以被认为是酱紫的,类似编译器会自动补全属性,其实编译期也会check修饰符,在赋值的时候补全一些代码:

@autoreleasepool {
  (__autoreleasing) myView = [UIView new]; // 生成的view会被注册到autorelease pool
  [myView autorelease];
}

因为这里用 new 来创建的对象,其实myView在被赋值的那一刻引用计数是1,因为它不像用 stringWithFormat 那种创建方式返回的是一个 autorelease 对象,并且计数在那一瞬间其实是2。所以当我们用 new 创建并且在把它放到了autorelease pool的时候,在出了pool作用域的瞬间,这个对象会执行一次 release,而这个对象本身只有一个引用计数,于是就被释放掉了。并且 __autorelease 修饰符并不像 weak 有个弱引用表,他不会被自动清理指针,于是就成为了野指针。

※ 让我们试一试用非init的方式初始化一个用__autorelease修饰的对象:
- (void)testAutoRelease
{
    __autoreleasing NSString* str;

    NSLog(@"str retain count: %@", [str valueForKey:@"retainCount"]);
    @autoreleasepool {
        str = [NSString stringWithFormat:@"%@", @"ssuuuuuuuuuuuuuuuuuuuu"];
        NSLog(@"str retain count: %@", [str valueForKey:@"retainCount"]);
    }
    NSLog(@"str retain count: %@", [str valueForKey:@"retainCount"]);
}

输出:
2021-04-17 19:12:02.964321+0800 Example1[43861:5631015] str retain count: (null)
2021-04-17 19:12:02.964471+0800 Example1[43861:5631015] str retain count: 2

并且crash到了最后一次log的地方~

这里虽然在给 str 赋值的时候他其实是有两个引用的,但是初始化会 autorelease 两次,分别因为修饰符以及初始化方式,autorelease 其实对应的是 push 到自动释放page里面,所以其实同一个对象push了两次,在pool销毁的时候,会push两次,执行两次release

于是其实到了pool结束的地方,str已经释放了0.0 这不是因为__autorelease 不是强引用哦,只是因为他会触发对象赋值的时候,把对象push到自动释放池,一定要记得 __autorelease 也是强引用哦~


※ 2. strong + pool

然后来看一个旧文儿里面的:

NSString *str = nil;
@autoreleasepool {
    str = [NSString stringWithFormat:@"%@", @"ssuuuuuuuuuuuuuuuuuuuu"];
}
NSLog(@"out pool string: %@", str);

这个会不会crash呢?

  • 答案是不会哒,因为在 str 被赋值的瞬间,stringWithFormat 返回的是一个 autorelease 对象,相当于创建的函数里面持有一个引用,并且 str 也有一个引用,于是这个值有俩引用,在出 pool 的时候会 release 了一次,还剩一个 str 引用,所以对象木有被销毁。

注意如果用init / new / copy / mutableCopy 开头啥的创建的对象,就不会被注册到 autorelease pool 的,只有用非以上关键字开头的创建对象 or __autorelease 修饰的对象创建的时候会注册哦,注册以后才会在出 pool 的时候执行对象的 release 哦~


※ 3. __autorelease + block

让我来强烈推荐一篇夹杂很多姿势点的:https://www.debugger.wiki/article/html/1570775013637318

- (void)testAutoRelease2 {
    NSError *error; //尽管这里默认是strong,但是downloadUrl函数里给error赋值的时候会根据函数的形参的修饰符来去决定是__strong还是__autorelease
    [self downloadUrl:@"http://xxx.png" error:&error];
}

- (void)downloadUrl:(NSString*)url error:(NSError**)error {
//这里的NSError*默认是autorelease的,相当于(NSError * __autorelease *)error, 要解决这个问题可以强制把它变成strong的,如(NSError* __strong*)error
//    @autoreleasepool {
//        *error = [[NSError alloc] init];
//    }
    
    for (NSInteger i = 0; i < 10; i++) {
        *error = [[NSError alloc] init];
    }
    NSLog(@"error:%@", error); //crash,EXC_BAD_ACCESS
}

这里涉及的知识点主要是形参的指针默认是__autorelease的,于是在for循环赋值的时候默认是 *error = [[[NSError alloc] init] autorelease];,而for循环默认又是带一个 autorelease pool 的,于是就在 for 循环的末尾,打NSLog的时候会有野指针错误。

同理,如果用注释的 autorelease pool 的代码一样会crash的0.0 下面就换成strong来试一下,但不顺便加了一下block和GDC康康效果:

- (void)downloadUrl:(NSString*)url error:(__strong NSError**)error {
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_main_queue(), ^{
        *error = [[NSError alloc] initWithDomain:@"domain" code:1 userInfo:nil];
        dispatch_group_leave(group);
    });

//    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//        *error = [[NSError alloc] initWithDomain:@"domain" code:1 userInfo:nil]; //crash
//        dispatch_group_leave(group);
//    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
        NSLog(@"error ggg is %@", error); //crash
    });
}

先忽略注释的部分,运行以后会在wait之后的log crash,原因还是野指针。因为其实block里面捕获的不是真正的error,只是一个复制品,毕竟你也没__block,于是他的引用其实在这个函数结束的时候就已经木有啦,async里面执行的时候可能error还没清理,但是wait执行的时候它已经很久以后啦,所以会报野指针错误。

同理,如果你直接dispatch_after一个0.5s,也会在block里面报野指针crash的。

这里就需要__block,__block的修饰可以将变量从栈空间的作用域提升到堆上。但是注意哦,虽然__block可以将变量从栈空间的作用域提升到堆上,但它这个时机是在block被copy的时候才发生的也就是不是你声明这个变量的时候拷贝到堆的哦,需要block拷贝的时候一起~


※ 4. __autorelease 的各种状况 & 优化(参考别人)

找资料的时候看到了一篇文章列了比较多的情况以及相应会咋样,其实这个比较语言特性了,不是很想深入去说,毕竟换swift又是另一套,大家自己康康叭。

引用 sunny 的一段话:在返回值身上调用objc_autoreleaseReturnValue方法时,runtime将这个返回值object储存在TLS中,然后直接返回这个object(不调用autorelease);同时,在外部接收这个返回值的objc_retainAutoreleasedReturnValue里,发现TLS中正好存了这个对象,那么直接返回这个object(不调用retain)。

于是乎,调用方和被调方利用TLS做中转,很有默契的免去了对返回值的内存管理

这里我比较感兴趣的部分其实是autorelease的优化,之前看源码的时候木有仔细讲这一趴:

objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this; // tagged pointer直接返回不走这一套流程
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}

prepareOptimizedReturn这个函数如果是 yes 的话,其实 autorelease 不会把自己注册到自动释放池,那么啥时候会是 YES 呢?

// Try to prepare for optimized return with the given disposition (+0 or +1).
// Returns true if the optimized path is successful.
// Otherwise the return value must be retained and/or autoreleased as usual.
static ALWAYS_INLINE bool 
prepareOptimizedReturn(ReturnDisposition disposition)
{
    ASSERT(getReturnDisposition() == ReturnAtPlus0);

    if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) {
        if (disposition) setReturnDisposition(disposition);
        return true;
    }

    return false;
}
  • __builtin_return_address
    __builtin_return_address(0)的含义是,得到当前函数返回地址,即此函数被别的函数调用,然后此函数执行完毕后,返回,所谓返回地址就是那时候的地址。其实就是函数TCB那一套里面的栈帧
    __builtin_return_address(1)的含义是,得到当前函数的调用者的返回地址。注意是调用者的返回地址,而不是函数起始地址。

  • callerAcceptsOptimizedReturn
    调用方是否接受优化的返回,那么编译器就得通过一些手段来知道调用方是怎么处理返回值的,来决定是否去做优化。不同的系统架构,该方法的实现也都不一样

  The callee's recognition of the optimized caller is architecture-dependent.
  x86_64: Callee looks for `mov rax, rdi` followed by a call or 
    jump instruction to objc_retainAutoreleasedReturnValue or 
    objc_unsafeClaimAutoreleasedReturnValue. 
  i386:  Callee looks for a magic nop `movl %ebp, %ebp` (frame pointer register)
  armv7: Callee looks for a magic nop `mov r7, r7` (frame pointer register). 
  arm64: Callee looks for a magic nop `mov x29, x29` (frame pointer register). 

static ALWAYS_INLINE bool 
callerAcceptsOptimizedReturn(const void * const ra0)
{
    const uint8_t *ra1 = (const uint8_t *)ra0;
    const unaligned_uint16_t *ra2;
    const unaligned_uint32_t *ra4 = (const unaligned_uint32_t *)ra1;
    const void **sym;

#define PREFER_GOTPCREL 0
#if PREFER_GOTPCREL
    // 48 89 c7    movq  %rax,%rdi
    // ff 15       callq *symbol@GOTPCREL(%rip)
    if (*ra4 != 0xffc78948) {
        return false;
    }
    if (ra1[4] != 0x15) {
        return false;
    }
    ra1 += 3;
#else
    // 48 89 c7    movq  %rax,%rdi
    // e8          callq symbol
    if (*ra4 != 0xe8c78948) {
        return false;
    }
    ra1 += (long)*(const unaligned_int32_t *)(ra1 + 4) + 8l;
    ra2 = (const unaligned_uint16_t *)ra1;
    // ff 25       jmpq *symbol@DYLDMAGIC(%rip)
    if (*ra2 != 0x25ff) {
        return false;
    }
#endif
    ra1 += 6l + (long)*(const unaligned_int32_t *)(ra1 + 2);
    sym = (const void **)ra1;
    // 这里检验了主调方在返回值之后是否紧接着调用了以下2个方法去持有返回的对象,如果有则说明可以去优化,不需要被调用方去autorelease,也不需要调用方去retain返回的对象了,省去了开销
    if (*sym != objc_retainAutoreleasedReturnValue  &&  
        *sym != objc_unsafeClaimAutoreleasedReturnValue) 
    {
        return false;
    }

    return true;
}

里面主要的部分是这个:

if (*sym != objc_retainAutoreleasedReturnValue  &&  *sym != objc_unsafeClaimAutoreleasedReturnValue) {
  return false;  
}

也就是说如果 objc_retainAutoreleasedReturnValue 为YES,或者objc_unsafeClaimAutoreleasedReturnValue 是YES,那么久可以优化。

// Accept a value returned through a +0 autoreleasing convention for use at +1.
id
objc_retainAutoreleasedReturnValue(id obj)
{
    if (acceptOptimizedReturn() == ReturnAtPlus1) return obj; // 调用方去TLS中查找刚好标记位为true,那么就直接返回该对象了。省去了retain的操作

    return objc_retain(obj);
}

// Accept a value returned through a +0 autoreleasing convention for use at +0.
id
objc_unsafeClaimAutoreleasedReturnValue(id obj)
{
    if (acceptOptimizedReturn() == ReturnAtPlus0) return obj;

    return objc_releaseAndReturn(obj); // release(obj) and return obj
}

这俩方法里面都调用了acceptOptimizedReturn

enum ReturnDisposition : bool {
    ReturnAtPlus0 = false, ReturnAtPlus1 = true
};

static ALWAYS_INLINE ReturnDisposition 
acceptOptimizedReturn()
{
    ReturnDisposition disposition = getReturnDisposition(); // 从tsl中读取对应的值
    setReturnDisposition(ReturnAtPlus0);  // reset to the unoptimized state 将数据之为0
    return disposition;
}

static ALWAYS_INLINE ReturnDisposition getReturnDisposition() {
    return (ReturnDisposition)(uintptr_t)tls_get_direct(RETURN_DISPOSITION_KEY);
}
static ALWAYS_INLINE void setReturnDisposition(ReturnDisposition disposition) {
    tls_set_direct(RETURN_DISPOSITION_KEY, (void*)(uintptr_t)disposition);
}

其实木有特别看明白这个getReturnDisposition到底是个啥含义,感觉就是一个bool值的存储,反正上面这一段的意思就是以下两种状况会做返回值优化:

  1. 假如编译器知道调用方是强持有返回的值(ref + 1),那么就没必要返回的时候autorelease一下,然后调用方再将返回值retain一下,直接返回对象就好了
  2. 假如调用方不强持有返回的值,那么返回值不加入autoreleasepool中的话,就需要objc_releaseAndReturnrelease该对象了,省去了加入autoreleasepool的操作

这个就可以用于解释之前refer的那个文章里面的很多case啦,因为如果return value被强持有,返回值会优化为非autorelease的对象哈~

那么为啥要这么优化呢:省去了加入自动释放池的时间消耗、避免对象对自动释放池的内存占用。

但其实这里的结论和之前的有些实验还是冲突的,所以优化这个事儿不知道是因为arch的区别也会有区别,还是因为别的一些判断,感觉有的时候即使是返回值被强指针引用也没有做优化。anyway其实重点是这种优化的思想比较好啦,太过细节的东西可能面试才会用到,日常没啥用处。如果遇到需要优化一些操作的时候,可以想一下如何让后面的指令可以和前面的相抵消。

refer to:
//www.greatytc.com/p/8ad0f1c4889a
https://www.debugger.wiki/article/html/1570775013637318
https://blog.csdn.net/junjun150013652/article/details/53149145
//www.greatytc.com/p/dc6b89de4215
http://seanchense.github.io/2019/10/15/optimized-return-autorelease/

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

推荐阅读更多精彩内容