今天收到一个反馈, 说有个按钮突然不显示, 排除了网络配置等因素的影响后, 查看文件修改记录, 发现最近一年都没有动过相关代码, 一时有点尬住了. 按钮的明明有 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; } return _weakButton; }
|
其实一眼应该就能看出这个代码有问题, 在 getter 方法中, 将临时变量赋值给 _weakButton 属性这一步肯定是有问题的, 但是诡异的是, 这段代码在 iOS 15/16 运行没问题, 能正常显示, 在 iOS 17 才有问题, 这也是为什么需求上线的时候没人发现, 但是随着 iOS 17 版本的覆盖, 问题就暴露了.
这么明显的错误, 为什么在 iOS15/16 没问题呢?
想知道 OC 的对象为什么没被销毁, 直接打印一下引用计数就好
1 2 3
| po CFGetRetainCount((__bridge CFTypeRef)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]; btn.frame = CGRectMake(100, 100, 200, 50); [btn setBackgroundColor:UIColor.redColor]; [btn setTitle:@"Test" forState:UIControlStateNormal]; btn.titleLabel.font = [UIFont systemFontOfSize:12]; _weakButton = btn; } 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]; btn.frame = CGRectMake(100, 100, 200, 50); [btn setBackgroundColor:UIColor.redColor]; [btn setTitle:@"Test" forState:UIControlStateNormal]; btn.titleLabel.font = [UIFont systemFontOfSize:12]; _weakButton = btn; } 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
|
static ALWAYS_INLINE bool callerAcceptsOptimizedReturn(const void *ra) { if (*(uint32_t *)ra == 0xaa1d03fd) { return true; } return false; }
|
iOS 17 的实现改为用指针记录下一条语句的位置, 比较指针
