面试题答案
一键面试编译期插入的内存管理代码
- 对象创建与持有:在ARC模式下,编译器会在对象创建时自动插入合适的内存管理代码。例如,当使用
[[NSObject alloc] init]
创建对象时,编译器会确保在对象创建后,该对象的引用计数被正确设置,通常会设置为1。这意味着该对象被创建它的代码所持有。 - 对象释放:编译器会在对象不再被使用时插入释放代码。当对象超出其作用域,或者其强引用计数变为0时,编译器会插入释放对象内存的代码。这一过程是自动化的,开发者无需手动调用
release
方法。例如,在一个局部变量作用域结束时,编译器会插入相应的释放代码来处理该变量所指向的对象。 - 所有权修饰符:ARC引入了
__strong
、__weak
、__unsafe_unretained
等所有权修饰符。编译器会根据这些修饰符来生成不同的内存管理代码。__strong
表示强引用,持有对象;__weak
表示弱引用,不持有对象且当对象被释放时会自动置为nil
;__unsafe_unretained
也不持有对象,但当对象被释放后,指向它的指针不会自动置为nil
,可能导致野指针。编译器会根据这些修饰符来决定何时增加或减少对象的引用计数。
runtime如何追踪对象引用计数
- 对象头:在Objective-C对象的内存布局中,对象头(
objc_object
结构体中的一部分)包含了与引用计数相关的信息。在64位系统下,对象头的一部分位用于存储引用计数。由于64位系统中对象指针通常为64位,苹果采用了一种优化策略,将引用计数存储在对象指针的一部分位中,这样可以减少额外的内存开销。 - Side Tables:当对象的引用计数超出对象头所能存储的范围时(例如,引用计数频繁增加和减少导致溢出),runtime会使用Side Tables来存储引用计数。Side Tables本质上是一个哈希表,键为对象的地址,值为包含引用计数等信息的结构体。通过这种方式,runtime能够灵活地管理对象的引用计数,即使引用计数的值非常大也能有效处理。
- 引用计数操作:runtime提供了一系列函数来操作对象的引用计数,如
objc_retain
用于增加引用计数,objc_release
用于减少引用计数。当对象的引用计数变为0时,runtime会调用objc_destructInstance
函数来释放对象的实例变量,并最终调用free
函数释放对象的内存。
ARC在多线程环境下的内存管理机制
- 线程安全的引用计数操作:runtime在实现引用计数操作时,使用了自旋锁(spinlock)来保证在多线程环境下引用计数操作的原子性。例如,在调用
objc_retain
和objc_release
时,自旋锁会防止多个线程同时修改对象的引用计数,从而避免数据竞争问题。自旋锁在短时间内获取锁的效率较高,因为它不会使线程进入睡眠状态,而是在循环中尝试获取锁,直到成功为止。 - 弱引用的线程安全性:
__weak
修饰的弱引用在多线程环境下也是线程安全的。runtime通过维护一个全局的弱引用表(weak table)来管理所有的弱引用。当对象被释放时,runtime会遍历弱引用表,将所有指向该对象的弱引用指针置为nil
。在多线程环境下,对弱引用表的操作也通过锁机制来保证线程安全,确保不会出现一个线程正在释放对象并修改弱引用表,而另一个线程同时读取或修改弱引用表的情况。
实际优化案例
- 避免循环引用:在一个常见的视图控制器与视图之间的关系中,如果视图控制器持有一个视图,而视图又通过代理(delegate)反向持有视图控制器,就可能会出现循环引用。在ARC之前,开发者需要手动打破循环引用,例如在合适的时机将代理设置为
nil
。而在ARC环境下,通过将代理属性声明为__weak
(对于代理方不应该持有被代理方的情况),编译器会自动处理循环引用问题。例如:
// 视图类
@interface MyView : UIView
@property (nonatomic, weak) id<MyViewDelegate> delegate;
@end
// 视图控制器类
@interface MyViewController : UIViewController <MyViewDelegate>
@property (nonatomic, strong) MyView *myView;
@end
@implementation MyViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.myView = [[MyView alloc] initWithFrame:self.view.bounds];
self.myView.delegate = self;
[self.view addSubview:self.myView];
}
@end
在这个例子中,MyView
的delegate
属性声明为__weak
,避免了视图和视图控制器之间的循环引用,确保在不需要时对象能够被正确释放。
2. 优化内存使用:在一个包含大量图片的滚动视图(如UITableView
或UICollectionView
)中,使用ARC可以有效管理图片对象的内存。由于单元格在滚动过程中会被复用,ARC会自动处理单元格中图片对象的引用计数。当单元格被移出屏幕不再显示时,其强引用计数会减少,当计数变为0时,图片对象的内存会被自动释放。这相比于手动管理内存,大大减少了内存泄漏的风险,同时也提高了内存使用效率。例如:
// 自定义表格视图单元格类
@interface MyTableViewCell : UITableViewCell
@property (nonatomic, strong) UIImageView *imageView;
@end
@implementation MyTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[self.contentView addSubview:self.imageView];
}
return self;
}
@end
// 表格视图数据源方法
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *identifier = @"MyCell";
MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier forIndexPath:indexPath];
cell.imageView.image = [UIImage imageNamed:@"someImage"];
return cell;
}
在这个例子中,当单元格被复用,新的图片对象被设置到imageView
时,旧的图片对象(如果存在)的引用计数会减少,最终被释放,有效管理了内存。