MST

星途 面试题库

面试题:Objective-C中Block在复杂场景下的内存管理与性能优化

假设你正在开发一个大型的iOS应用,其中有多个模块频繁使用Block进行异步任务处理、数据传递等操作。在某些情况下,应用出现了内存泄漏和性能问题。请从Block的内存管理角度出发,详细分析可能导致这些问题的原因,并给出一套完整的优化方案,包括但不限于如何正确使用Block的拷贝、如何避免不必要的内存持有以及如何监控和排查潜在的内存问题。
21.9万 热度难度
编程语言Objective-C

知识考点

AI 面试

面试题答案

一键面试

可能导致内存泄漏和性能问题的原因

  1. 循环引用
    • 当Block捕获了对象,而该对象又强引用了Block,就会形成循环引用,导致对象无法释放,造成内存泄漏。例如,在ViewController中定义一个Block,Block中访问了ViewController的属性,而ViewController又强引用了这个Block,就会出现这种情况。
    • 代码示例:
@interface ViewController ()
@property (nonatomic, copy) void(^myBlock)(void);
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.myBlock = ^{
        NSLog(@"%@", self.title);
    };
    self.myBlock();
}
@end
  • 在上述代码中,self.myBlock捕获了self,而self又强引用了myBlock,形成了循环引用。
  1. 不必要的Block拷贝
    • 频繁地对Block进行拷贝操作,可能会增加内存开销。比如在循环中每次都进行Block的拷贝,而实际上不需要这样做。
    • 例如:
for (int i = 0; i < 1000; i++) {
    void(^block)(void) = ^{
        NSLog(@"%d", i);
    };
    dispatch_async(dispatch_get_main_queue(), [block copy]);
}
  • 这里在循环中每次都对Block进行拷贝,增加了不必要的开销。
  1. Block中持有大量数据
    • 如果Block捕获了大量的数据,而这些数据在Block执行完毕后仍然被持有,就会导致内存浪费,影响性能。比如捕获了一个大的数组或字典等。
    • 示例:
NSMutableArray *largeArray = [NSMutableArray arrayWithCapacity:10000];
// 填充大量数据
for (int i = 0; i < 10000; i++) {
    [largeArray addObject:@(i)];
}
void(^block)(void) = ^{
    NSLog(@"%@", largeArray);
};
block();
  • 这里block捕获了largeArray,即使block执行完毕,largeArray可能仍然因为block的持有而无法释放。

优化方案

  1. 解决循环引用
    • 使用__weak__unsafe_unretained关键字来打破循环引用。推荐使用__weak,因为__unsafe_unretained可能会导致野指针问题。
    • 修正前面ViewController的代码:
@interface ViewController ()
@property (nonatomic, copy) void(^myBlock)(void);
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    self.myBlock = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf) {
            NSLog(@"%@", strongSelf.title);
        }
    };
    self.myBlock();
}
@end
  • 这里先使用__weak修饰符创建一个弱引用weakSelf,在Block内部再创建一个强引用strongSelf,并进行判空操作,这样既避免了循环引用,又能安全地访问self的属性。
  1. 避免不必要的Block拷贝
    • 确定是否真的需要对Block进行拷贝。如果只是在当前栈内使用Block,不需要拷贝。只有在需要将Block传递到其他线程或在对象生命周期外使用时,才进行拷贝。
    • 对于前面循环中不必要的拷贝示例,可以改为:
for (int i = 0; i < 1000; i++) {
    void(^block)(void) = ^{
        NSLog(@"%d", i);
    };
    dispatch_async(dispatch_get_main_queue(), block);
}
  • 这里去掉了不必要的[block copy]操作。
  1. 减少Block中持有数据
    • 如果Block只需要部分数据,尽量只捕获需要的部分,而不是整个大的数据结构。
    • 对于前面捕获大数组的示例,如果只需要数组的某一个元素,可以这样修改:
NSMutableArray *largeArray = [NSMutableArray arrayWithCapacity:10000];
// 填充大量数据
for (int i = 0; i < 10000; i++) {
    [largeArray addObject:@(i)];
}
NSNumber *number = largeArray[0];
void(^block)(void) = ^{
    NSLog(@"%@", number);
};
block();
  • 这里只捕获了largeArray中的第一个元素number,而不是整个largeArray,减少了内存占用。

监控和排查潜在的内存问题

  1. 使用 Instruments
    • Leaks工具:在Xcode中,可以使用Instruments的Leaks工具来检测内存泄漏。运行应用并在Leaks工具中查看是否有对象被泄漏。如果有泄漏,Leaks工具会显示泄漏对象的相关信息,帮助定位问题代码。
    • Allocations工具:Allocations工具可以监控应用的内存分配情况。可以查看对象的创建和销毁时间,分析内存增长趋势,找出可能存在内存问题的地方。例如,如果某个对象一直没有被销毁,可能存在内存泄漏。
  2. 添加日志
    • 在关键代码处添加日志,记录Block的创建、执行和释放情况。例如:
void(^block)(void) = ^{
    NSLog(@"Block执行");
};
NSLog(@"Block创建");
block();
NSLog(@"Block执行完毕");
block = nil;
NSLog(@"Block释放");
  • 通过分析日志,可以了解Block的生命周期,判断是否存在异常情况。
  1. 使用WeakProxy
    • 可以创建一个WeakProxy类,用于包装对象,在Block中使用WeakProxy来避免直接捕获对象,从而减少循环引用的风险。WeakProxy类的实现可以参考以下代码:
@interface WeakProxy : NSProxy
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation WeakProxy
+ (instancetype)proxyWithTarget:(id)target {
    WeakProxy *proxy = [WeakProxy alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    SEL sel = invocation.selector;
    if ([self.target respondsToSelector:sel]) {
        [invocation invokeWithTarget:self.target];
    }
}
@end
  • 使用时:
@interface ViewController ()
@property (nonatomic, copy) void(^myBlock)(void);
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    WeakProxy *proxy = [WeakProxy proxyWithTarget:self];
    self.myBlock = ^{
        NSLog(@"%@", proxy.target.title);
    };
    self.myBlock();
}
@end
  • 这样在Block中通过WeakProxy间接访问self,降低了循环引用的可能性。