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而没有持有它,但仍然存在潜在风险:
- Block捕获manager: Block内部访问
manager.someProperty时会捕获manager的强引用 - 管理器可能持有Block: 如果将来修改代码持有Block,就会形成循环引用
- 隐式引用: 即使没有显式持有,某些情况下也可能形成隐式引用
循环引用链
如果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应用。