iOS17 Autorelease 底层改变导致不规范代码失效

今天收到一个反馈, 说有个按钮突然不显示, 排除了网络配置等因素的影响后, 查看文件修改记录, 发现最近一年都没有动过相关代码, 一时有点尬住了. 按钮的明明有 getter 方法, 但是加到 UI 上的时候就变成 nil 了, 排查代码也没有逻辑对按钮的 property 置空. 所以直接看 property 定义如下:

1
@property (nonatomic, weak) UIButton *emptyButton;

居然是个 weak 属性.

简化一下代码, 大致如下:

问题代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

@property (nonatomic, weak) UIButton *weakButton;

- (void)viewDidLoad
{
[super viewDidLoad];

[self.view addSubview:[self fetchButton]];
}


- (UIView *)fetchButton
{
return self.weakButton;
}


- (UIButton *)weakButton
{
if (!_weakButton)
{
UIButton *btn = [UIButton new];
btn.frame = CGRectMake(100, 100, 200, 50);
[btn setBackgroundColor:UIColor.redColor];
[btn setTitle:@"Test" forState:UIControlStateNormal];
btn.titleLabel.font = [UIFont systemFontOfSize:12];
_weakButton = btn; // 复制给 weak property, 在代码块结束 btn 就被释放了
}
return _weakButton;
}

其实一眼应该就能看出这个代码有问题, 在 getter 方法中, 将临时变量赋值给 _weakButton 属性这一步肯定是有问题的, 但是诡异的是, 这段代码在 iOS 15/16 运行没问题, 能正常显示, 在 iOS 17 才有问题, 这也是为什么需求上线的时候没人发现, 但是随着 iOS 17 版本的覆盖, 问题就暴露了.

这么明显的错误, 为什么在 iOS15/16 没问题呢?

想知道 OC 的对象为什么没被销毁, 直接打印一下引用计数就好

1
2
3
po CFGetRetainCount((__bridge CFTypeRef)btn)
// 或者直接打 btn 的地址
po CFGetRetainCount((__bridge CFTypeRef)0x10321cf10)

在 iOS 15 上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (UIButton *)weakButton 
{
if (!_weakButton)
{
UIButton *btn = [UIButton new];
// 引用计数 1
btn.frame = CGRectMake(100, 100, 200, 50);
[btn setBackgroundColor:UIColor.redColor];
[btn setTitle:@"Test" forState:UIControlStateNormal];
btn.titleLabel.font = [UIFont systemFontOfSize:12];
// 引用计数 2
_weakButton = btn; // 复制给 weak property, 在代码块结束 btn 就被释放了
}
// 引用计数 1
return _weakButton;
}

在 iOS 17 上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (UIButton *)weakButton 
{
if (!_weakButton)
{
UIButton *btn = [UIButton new];
// 引用计数 1
btn.frame = CGRectMake(100, 100, 200, 50);
[btn setBackgroundColor:UIColor.redColor];
[btn setTitle:@"Test" forState:UIControlStateNormal];
btn.titleLabel.font = [UIFont systemFontOfSize:12];
// 引用计数 1
_weakButton = btn; // 复制给 weak property, 在代码块结束 btn 就被释放了
}
// 引用计数 0
return _weakButton;
}

所以问题出在 btn.titleLabel.font = [UIFont systemFontOfSize:12]; 这句话上, 其实再简化下, 问题实际是出在 titleLabel 的 getter 方法中,
使用 Hopper 看下 [UIButton titleLabel]的源码:

的确有对 btn 本身做 autorelease 的操作, 在 iOS 17 上这部分源码也没有改变, 所以问题应该出在objc_retainAutoreleasedReturnValue 的实现中, 恰好 iOS 17 的确修改了里面一个关键方法的实现, 将 autorelease 的汇编语句从 3 条指令降低到 2 条, 能减少可执行文件 2% 左右的大小, 参考文章”ARC 与 AutoReleasePool”的章节:Autorelease 自动省略.

修改的函数是callerAcceptsOptimizedReturn
iOS 15 的实现是直接比较下一条语句的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
// __arm__


static ALWAYS_INLINE bool
callerAcceptsOptimizedReturn(const void *ra)
{
// fd 03 1d aa mov fp, fp
// arm64 instructions are well-aligned
if (*(uint32_t *)ra == 0xaa1d03fd) {
return true;
}
return false;
}

iOS 17 的实现改为用指针记录下一条语句的位置, 比较指针

-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道