MST
星途 面试题库

面试题:Objective-C代码块(Block)与循环引用及解决方案深入分析

详细说明在Objective-C中使用代码块(Block)时,如何产生循环引用问题。请以一个复杂的视图控制器场景为例,展示在视图层级、属性设置及代码块使用中循环引用是怎样形成的,并给出至少两种有效的解决方案,分析每种方案的优缺点。
14.0万 热度难度
编程语言Objective-C

知识考点

AI 面试

面试题答案

一键面试

循环引用的形成

  1. 场景设定:假设有一个复杂的视图控制器 ViewController,其中包含一个自定义视图 CustomViewViewController 持有 CustomView 的实例作为属性,CustomView 中有一个代码块属性 completionBlock,并且 CustomView 在某个操作完成后会调用这个代码块。
  2. 代码示例
#import <UIKit/UIKit.h>

@interface CustomView : UIView
@property (nonatomic, copy) void (^completionBlock)();
@end

@implementation CustomView
- (void)doSomeWork {
    // 模拟工作完成后调用代码块
    if (self.completionBlock) {
        self.completionBlock();
    }
}
@end

@interface ViewController : UIViewController
@property (nonatomic, strong) CustomView *customView;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.customView = [[CustomView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:self.customView];
    
    __weak typeof(self) weakSelf = self;
    self.customView.completionBlock = ^{
        // 这里如果使用 `self` 而不是 `weakSelf`,就会形成循环引用
        [weakSelf.customView doSomeWork];
    };
}
@end
  1. 形成原理ViewController 持有 CustomView,而 CustomView 的代码块 completionBlock 又持有 ViewController(如果在代码块中直接使用 self)。这样就形成了一个循环引用,ViewController -> CustomView -> completionBlock -> ViewController,导致内存无法释放。

解决方案及优缺点分析

  1. 使用 __weak 修饰符
    • 方案:如上述代码示例中,在代码块外使用 __weak typeof(self) weakSelf = self;,然后在代码块内使用 weakSelf。这样代码块不会强引用 ViewController,从而打破循环引用。
    • 优点:简单直接,在大多数情况下能有效解决循环引用问题,不会引入过多复杂逻辑。
    • 缺点:如果在代码块执行期间 ViewController 被释放,weakSelf 会变为 nil,可能导致空指针访问问题,需要在代码块中注意检查 weakSelf 是否为 nil
  2. 使用 __block 修饰符(配合手动释放)
    • 方案
#import <UIKit/UIKit.h>

@interface CustomView : UIView
@property (nonatomic, copy) void (^completionBlock)();
@end

@implementation CustomView
- (void)doSomeWork {
    if (self.completionBlock) {
        self.completionBlock();
    }
}
@end

@interface ViewController : UIViewController
@property (nonatomic, strong) CustomView *customView;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.customView = [[CustomView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:self.customView];
    
    __block typeof(self) blockSelf = self;
    self.customView.completionBlock = ^{
        [blockSelf.customView doSomeWork];
        blockSelf = nil; // 手动释放对 self 的引用
    };
}
@end
- **优点**:能解决循环引用问题,并且在代码块内可以直接使用类似 `self` 的变量,相对简洁。
- **缺点**:需要手动在代码块结束时将 `blockSelf` 置为 `nil`,如果忘记这一步,仍然可能存在循环引用。而且在ARC环境下,对 `__block` 变量的内存管理相对复杂一些。

3. 在合适时机手动断开引用: - 方案:在 ViewControllerdealloc 方法中,将 CustomViewcompletionBlock 置为 nil

#import <UIKit/UIKit.h>

@interface CustomView : UIView
@property (nonatomic, copy) void (^completionBlock)();
@end

@implementation CustomView
- (void)doSomeWork {
    if (self.completionBlock) {
        self.completionBlock();
    }
}
@end

@interface ViewController : UIViewController
@property (nonatomic, strong) CustomView *customView;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.customView = [[CustomView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:self.customView];
    
    self.customView.completionBlock = ^{
        [self.customView doSomeWork];
    };
}

- (void)dealloc {
    self.customView.completionBlock = nil;
}
@end
- **优点**:逻辑清晰,明确在 `ViewController` 释放时断开循环引用。
- **缺点**:需要在每个可能产生循环引用的地方手动处理,容易遗漏。如果有多个地方持有循环引用,维护成本较高。