[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 大家应该都很熟悉了(如果不是很清楚它是啥可以参考:https://www.jianshu.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:
https://www.jianshu.com/p/8ad0f1c4889a
https://www.debugger.wiki/article/html/1570775013637318
https://blog.csdn.net/junjun150013652/article/details/53149145
https://www.jianshu.com/p/dc6b89de4215
http://seanchense.github.io/2019/10/15/optimized-return-autorelease/

你可能感兴趣的:([iOS] __autorelease的碎碎念&疑惑)