这个事儿我大概四五月之前想写来着,拖了这么久也是醉了。。我觉得我最近脑子基本是废了
※ 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值的存储,反正上面这一段的意思就是以下两种状况会做返回值优化:
- 假如编译器知道调用方是强持有返回的值(ref + 1),那么就没必要返回的时候autorelease一下,然后调用方再将返回值retain一下,直接返回对象就好了
- 假如调用方不强持有返回的值,那么返回值不加入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/