iOS内存管理之循环引用检测与预防

背景

在iOS开发中,循环引用(Retain Cycle)是导致内存泄漏的主要原因之一。尽管ARC(自动引用计数)大大简化了内存管理,但开发者仍然需要警惕循环引用的风险。如果不及时处理,循环引用会导致对象无法被正常释放,最终造成内存泄漏和应用崩溃。本文将通过实际案例分析循环引用的成因、检测方法和预防策略。

循环引用基础概念

什么是循环引用

循环引用是指两个或多个对象之间互相持有强引用,导致引用计数永远无法归零,从而无法被系统释放的情况。

常见的循环引用场景

1. 对象间的双向强引用

@interface Person : NSObject
@property (nonatomic, strong) Dog *dog;
@end

@interface Dog : NSObject
@property (nonatomic, strong) Person *owner;
@end

// 循环引用示例
Person *person = [[Person alloc] init];
Dog *dog = [[Dog alloc] init];
person.dog = dog;
dog.owner = person; // 造成循环引用

2. Block中的循环引用

@interface ViewController : UIViewController
@property (nonatomic, copy) void (^completionBlock)(void);
@end

@implementation ViewController

- (void)setupBlock {
    self.completionBlock = ^{
        [self doSomething]; // 循环引用:Block -> self -> Block
    };
}

- (void)doSomething {
    // 业务逻辑
}

@end

实际案例分析

案例:业务管理器与Block循环引用

让我们通过一个实际的业务场景来分析循环引用问题:

问题代码示例

@interface SomeBussinessManager : NSObject
- (void)doSomeThingWithBlock:(void (^)(void))blockName;
@property (nonatomic, strong) NSArray *someProperty;
@end

@interface SomeBussinessManager ()
@property (nonatomic, copy) void (^retaindBlock)(void);
@end

@implementation SomeBussinessManager
- (void)dealloc {
    NSLog(@"%s", __FUNCTION__); // 检测对象是否正常释放
}

- (void)doSomeThingWithBlock:(void (^)(void))blockName {
    // 注释掉的代码会造成循环引用
    // self.retaindBlock = blockName;

    // 直接执行Block,避免持有
    blockName();
}
@end

调用代码分析

@interface RetainCycleTestViewController : UIViewController
@end

@implementation RetainCycleTestViewController

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    SomeBussinessManager *manager = [[SomeBussinessManager alloc] init];
    manager.someProperty = @[@"test"];

    // 潜在的循环引用风险
    [manager doSomeThingWithBlock:^{
        NSArray *someProperty = manager.someProperty; // 捕获了manager的强引用
        NSLog(@"访问属性: %@", someProperty);
    }];
}

@end

循环引用分析

当前代码的风险

虽然当前代码中doSomeThingWithBlock方法直接执行了Block而没有持有它,但仍然存在潜在风险:

  1. Block捕获manager: Block内部访问manager.someProperty时会捕获manager的强引用
  2. 管理器可能持有Block: 如果将来修改代码持有Block,就会形成循环引用
  3. 隐式引用: 即使没有显式持有,某些情况下也可能形成隐式引用

循环引用链

如果retaindBlock赋值被取消注释,会形成如下循环引用:

ViewController -> manager (strong)
manager -> retaindBlock (copy, strong)
retaindBlock -> manager (captured, strong)

循环引用的解决方案

1. 使用弱引用(Weak Reference)

解决方案一:__weak修饰符

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    SomeBussinessManager *manager = [[SomeBussinessManager alloc] init];
    manager.someProperty = @[@"test"];

    // 使用弱引用避免循环引用
    __weak typeof(manager) weakManager = manager;
    [manager doSomeThingWithBlock:^{
        __strong typeof(weakManager) strongManager = weakManager;
        if (strongManager) {
            NSArray *someProperty = strongManager.someProperty;
            NSLog(@"访问属性: %@", someProperty);
        }
    }];
}

解决方案二:在管理器中使用弱引用

@interface SomeBussinessManager ()
@property (nonatomic, copy) void (^retaindBlock)(void);
@property (nonatomic, weak) id weakTarget; // 弱引用目标对象
@end

@implementation SomeBussinessManager

- (void)doSomeThingWithBlock:(void (^)(void))blockName {
    self.weakTarget = [blockName target]; // 假设可以获取target
    self.retaindBlock = [blockName copy]; // 持有Block但使用弱引用target

    blockName();
}

@end

2. 使用弱引用表(Weak Reference Table)

@interface WeakReferenceManager : NSObject
+ (instancetype)shared;
- (void)setWeakReference:(id)object forKey:(NSString *)key;
- (id)getWeakReferenceForKey:(NSString *)key;
@end

@implementation WeakReferenceManager

static NSMutableDictionary *_weakRefs = nil;

+ (void)initialize {
    if (self == [WeakReferenceManager class]) {
        _weakRefs = [NSMutableDictionary dictionary];
    }
}

- (void)setWeakReference:(id)object forKey:(NSString *)key {
    NSValue *weakValue = [NSValue valueWithNonretainedObject:object];
    _weakRefs[key] = weakValue;
}

- (id)getWeakReferenceForKey:(NSString *)key {
    NSValue *weakValue = _weakRefs[key];
    return [weakValue nonretainedObjectValue];
}

@end

3. 优化业务管理器设计

@interface ImprovedBussinessManager : NSObject
@property (nonatomic, strong) NSArray *someProperty;

// 避免Block的方案
- (void)doSomeThingWithDelegate:(id<BusinessDelegate>)delegate;
- (void)doSomeThingWithTarget:(id)target selector:(SEL)selector;
- (NSString *)getResultFromProperty;
@end

@protocol BusinessDelegate <NSObject>
- (void)businessDidCompleteWithResult:(NSString *)result;
@end

@implementation ImprovedBussinessManager

- (void)doSomeThingWithDelegate:(id<BusinessDelegate>)delegate {
    // 执行业务逻辑
    NSString *result = [self processBusiness];

    // 通过代理回调,避免循环引用
    if ([delegate respondsToSelector:@selector(businessDidCompleteWithResult:)]) {
        [delegate businessDidCompleteWithResult:result];
    }
}

- (void)doSomeThingWithTarget:(id)target selector:(SEL)selector {
    NSString *result = [self processBusiness];

    if (target && selector && [target respondsToSelector:selector]) {
        // 使用performSelector避免直接引用
        [target performSelector:selector withObject:result];
    }
}

- (NSString *)getResultFromProperty {
    return [self.someProperty componentsJoinedByString:@", "];
}

- (NSString *)processBusiness {
    // 模拟业务处理
    return [self getResultFromProperty];
}

@end

循环引用检测工具

1. 内存泄漏检测工具

使用Xcode Instruments

// 在测试代码中添加标记
- (void)createPotentialRetainCycle {
    // 内存标记点
    NSLog(@"创建潜在的循环引用对象");

    SomeBussinessManager *manager = [[SomeBussinessManager alloc] init];
    manager.someProperty = @[@"test"];

    // 故意创建循环引用进行测试
    __weak typeof(manager) weakManager = manager;
    [manager doSomeThingWithBlock:^{
        __strong typeof(weakManager) strongManager = weakManager;
        NSLog(@"Block执行: %@", strongManager.someProperty);
    }];

    // 标记对象应该被释放的位置
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"2秒后,manager应该已经被释放");
    });
}

自定义检测工具

@interface RetainCycleDetector : NSObject
+ (instancetype)shared;
- (void)startDetection;
- (void)markObject:(id)object;
- (void)checkForObject:(id)object;
@end

@implementation RetainCycleDetector

static NSMutableSet *_monitoredObjects = nil;
static NSMutableSet *_releasedObjects = nil;

+ (void)initialize {
    if (self == [RetainCycleDetector class]) {
        _monitoredObjects = [NSMutableSet set];
        _releasedObjects = [NSMutableSet set];
    }
}

- (void)markObject:(id)object {
    if (!object) return;

    NSString *objectAddress = [NSString stringWithFormat:@"%p", object];
    [_monitoredObjects addObject:objectAddress];

    // 监控对象的释放
    [self swizzleDeallocForClass:[object class]];
}

- (void)checkForObject:(id)object {
    NSString *objectAddress = [NSString stringWithFormat:@"%p", object];
    if ([_monitoredObjects containsObject:objectAddress]) {
        NSLog(@"⚠️ 检测到潜在循环引用: %@", object);
    }
}

- (void)swizzleDeallocForClass:(Class)class {
    // 实现Method Swizzling来监控dealloc
    SEL originalSelector = @selector(dealloc);
    SEL swizzledSelector = @selector(detector_dealloc);

    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

    if (originalMethod && swizzledMethod) {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

@end

2. 运行时检测

@interface RuntimeRetainCycleDetector : NSObject
+ (void)detectRetainCyclesInViewController:(UIViewController *)viewController;
+ (NSArray *)findStrongReferencesToObject:(id)object;
@end

@implementation RuntimeRetainCycleDetector

+ (void)detectRetainCyclesInViewController:(UIViewController *)viewController {
    NSLog(@"开始检测ViewController中的循环引用");

    // 检查属性
    unsigned int propertyCount;
    objc_property_t *properties = class_copyPropertyList([viewController class], &propertyCount);

    for (unsigned int i = 0; i < propertyCount; i++) {
        objc_property_t property = properties[i];
        NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];

        id value = [viewController valueForKey:propertyName];
        if (value) {
            [self checkForRetainCycleBetweenObject:viewController andObject:value propertyName:propertyName];
        }
    }

    free(properties);
}

+ (void)checkForRetainCycleBetweenObject:(id)sourceObject andObject:(id)targetObject propertyName:(NSString *)propertyName {
    // 检查目标对象是否强引用回源对象
    if ([targetObject isKindOfClass:[NSArray class]]) {
        NSArray *array = (NSArray *)targetObject;
        for (id item in array) {
            if (item == sourceObject) {
                NSLog(@"🚨 发现循环引用: %@.%@ -> 数组 -> %@", NSStringFromClass([sourceObject class]), propertyName, NSStringFromClass([sourceObject class]));
            }
        }
    }
    // 可以添加更多类型的检查...
}

@end

最佳实践建议

1. Block使用规范

正确的Block使用方式

// ✅ 推荐方式
__weak typeof(self) weakSelf = self;
self.completionBlock = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        [strongSelf updateUI];
    }
};

// ✅ 对于简单的操作,可以直接使用weakSelf
self.completionBlock = ^{
    [weakSelf updateUI]; // 如果不访问weakSelf的属性,可以直接使用
};

// ✅ 避免在Block中直接使用self
// self.completionBlock = ^{
//     [self updateUI]; // ❌ 错误:循环引用
// };

Block参数设计

// ✅ 将Block作为参数传递,而不是存储
- (void)performAsyncOperationWithCompletion:(void(^)(BOOL success, NSError *error))completion {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        BOOL success = [self doHeavyWork];
        NSError *error = success ? nil : [NSError errorWithDomain:@"ErrorDomain" code:1001 userInfo:nil];

        dispatch_async(dispatch_get_main_queue(), ^{
            if (completion) {
                completion(success, error);
            }
        });
    });
}

// ❌ 避免持有Block
@property (nonatomic, copy) void (^completionBlock)(BOOL success, NSError *error);

2. 代理模式优化

// ✅ 使用弱引用代理
@protocol MyProtocol <NSObject>
@optional
- (void)didCompleteOperation:(id)result;
@end

@interface MyClass : NSObject
@property (nonatomic, weak) id<MyProtocol> delegate; // 使用weak引用
@end

// ✅ 及时清理代理
- (void)dealloc {
    self.delegate = nil;
}

3. 定时器管理

// ✅ 正确的定时器使用方式
@interface TimerManager : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation TimerManager

- (void)startTimerWithTarget:(id)target selector:(SEL)selector interval:(NSTimeInterval)interval {
    self.target = target;
    self.selector = selector;

    self.timer = [NSTimer scheduledTimerWithTimeInterval:interval
                                                  target:self
                                                selector:@selector(timerFired:)
                                                userInfo:nil
                                                 repeats:YES];
}

- (void)timerFired:(NSTimer *)timer {
    if (self.target && [self.target respondsToSelector:self.selector]) {
        [self.target performSelector:self.selector withObject:nil];
    }
}

- (void)dealloc {
    [self.timer invalidate];
    self.timer = nil;
}

@end

4. 通知观察者管理

// ✅ 及时移除通知观察者
@interface NotificationManager : NSObject
@end

@implementation NotificationManager

- (void)setupNotifications {
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(handleNotification:)
                                                 name:@"SomeNotification"
                                               object:nil];
}

- (void)handleNotification:(NSNotification *)notification {
    // 处理通知
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

@end

内存管理监控工具

1. 开发时监控

@interface MemoryMonitor : NSObject
+ (void)enableMemoryLeakDetection;
+ (void)logMemoryUsage;
+ (void)trackViewControllerLifecycle:(UIViewController *)viewController;
@end

@implementation MemoryMonitor

+ (void)trackViewControllerLifecycle:(UIViewController *)viewController {
    NSString *className = NSStringFromClass([viewController class]);
    NSLog(@"📱 ViewController创建: %@", className);

    // 在适当的时机检查是否释放
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 检查对象是否还存在(需要其他配合机制)
    });
}

+ (void)logMemoryUsage {
    struct mach_task_basic_info info;
    mach_msg_type_number_t size = MACH_TASK_BASIC_INFO_COUNT;
    kern_return_t kerr = task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &size);

    if (kerr == KERN_SUCCESS) {
        CGFloat memoryUsage = info.resident_size / (1024.0 * 1024.0);
        NSLog(@"💾 当前内存使用: %.2f MB", memoryUsage);
    }
}

@end

2. 生产环境监控

@interface ProductionMemoryMonitor : NSObject
+ (void)setupProductionMonitoring;
@end

@implementation ProductionMemoryMonitor

+ (void)setupProductionMonitoring {
    // 监控内存压力
    [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification
                                                      object:nil
                                                       queue:[NSOperationQueue mainQueue]
                                                  usingBlock:^(NSNotification *note) {
        NSLog(@"⚠️ 收到内存警告");
        [self performMemoryCleanup];
    }];

    // 定期检查内存使用情况
    [NSTimer scheduledTimerWithTimeInterval:30.0
                                     target:self
                                   selector:@selector(checkMemoryUsage)
                                   userInfo:nil
                                    repeats:YES];
}

+ (void)performMemoryCleanup {
    // 清理缓存
    // 清理不必要的强引用
    // 释放临时资源
}

+ (void)checkMemoryUsage {
    // 检查内存使用情况,如果过高则采取相应措施
}

@end

总结

循环引用是iOS开发中常见但容易忽视的问题。通过本文的分析和解决方案,我们可以:

1. 识别循环引用

  • Block循环引用: 最常见的形式,需要特别注意
  • 代理循环引用: 使用弱引用代理模式
  • 定时器循环引用: 及时清理定时器引用
  • 通知循环引用: 及时移除通知观察者

2. 预防策略

  • 使用弱引用: 在Block中使用__weak修饰符
  • 设计模式优化: 使用代理模式替代Block存储
  • 及时清理: 在适当时机清理强引用
  • 工具检测: 使用Instruments和自定义工具检测泄漏

3. 最佳实践

  • 代码审查: 在代码审查时重点关注循环引用
  • 单元测试: 编写测试验证对象正确释放
  • 监控工具: 在开发和生产环境中部署监控工具
  • 文档规范: 制定团队内存管理规范

通过合理的内存管理策略和工具支持,我们可以有效避免循环引用导致的内存泄漏,构建更加稳定和高效的iOS应用。