Objective-C 下常见内存泄漏(leak)和循环引用 / 保留环(retain cycle)的类型逐项归类、解释成因、给出最小可复现 Objective-C 示例(含问题代码与修复代码),并列出定位与避免的实战方法(Xcode Memory Graph、Instruments、常用调试技巧与注意点)。
概览(先看要点)
- 内存泄漏(leak):对象分配后永远没有被释放(或 CoreFoundation 类型没有被 CFRelease),内存持续增长。常见于忘记释放 CF 对象、malloc/ calloc 未 free、单例/静态缓存、未移除 observer/timer/runloop source 等。
- 循环引用 / 保留环(retain cycle):两个或多个对象通过强引用链互相持有,导致 ARC 无法回收。最常见的是:对象 ↔ block(block 捕获 self),对象 ↔ 对象(两个对象都持有 strong),集合持有对象(strong)且对象反向持有集合(strong),Delegate 非 weak 等。
- 定位工具:Xcode Memory Graph(可视化引用关系)、Instruments(Leaks、Allocations)、static analyzer、Malloc/Guard Malloc、Address Sanitizer(不同目的)。
下面分门别类地讲。
1. 最常见的“循环引用”类型(带示例与修复)
1.1 对象 — block(block 捕获 self)
原因:block 在堆上会强引用它捕获的对象,若对象持有该 block(例如把 block 存为属性),就形成循环引用:self -> block -> self。
示例(有问题):
@interface MyController : UIViewController
@property (nonatomic, copy) void (^myBlock)(void);
@end
@implementation MyController
- (void)viewDidLoad {
[super viewDidLoad];
self.myBlock = ^{
// 捕获 self —— 强引用
NSLog(@"%@", self.view);
};
}
@end
修复(用 __weak / __typeof / strongify):
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
NSLog(@"%@", strongSelf.view);
};
或者将 block 属性设为 assign(不推荐)或将 block 作为局部变量不用属性持有,或把属性放到另一个不被 self 强持有的 owner。
1.2 NSTimer / CADisplayLink / RunLoop Source 没释放
原因:系统对象(NSTimer、CADisplayLink)默认由 RunLoop 保持强引用,timer 的 target 通常是 self;若 timer 未 invalidated,会保留 target。
示例(有问题):
@interface MyController : UIViewController
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation MyController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(tick:)
userInfo:nil
repeats:YES];
}
- (void)tick:(NSTimer *)t {}
@end
// 若没有在 dealloc 或 viewWillDisappear 时 [self.timer invalidate], timer 会保留 self。
修复方式:
- 在
dealloc或viewWillDisappear:中调用[timer invalidate]并且self.timer = nil;。 - 使用
NSTimer的 block API(iOS 10+)并弱化 self。 - 使用
CADisplayLink同理:[displayLink invalidate]。 - 使用
GCD定时器(dispatch_source_t)并且取消来源。
示例(proxy 解法避免强引用):
@interface WeakProxy : NSProxy
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
// 实现简单代理,timer target 用 WeakProxy,这样 proxy 不强持有 target。
1.3 对象 ↔ 对象 双向 strong
原因:两个对象互相持有 strong(例如 parent strong 持有 child,child strong 持有 parent)。
示例(有问题):
@interface Parent : NSObject
@property (nonatomic, strong) Child *child;
@end
@interface Child : NSObject
@property (nonatomic, strong) Parent *parent;
@end
修复:通常把一方改成 weak(通常 child 对 parent 用 weak):
@property (nonatomic, weak) Parent *parent;
若需要非零弱引用(weak 会被置 nil),可以用 __unsafe_unretained(极少数场景)或使用 assign(非对象)。
1.4 Delegate 未用 weak(或协议未指定 weak)
Delegate 模式若 delegate 属性为 strong,会很容易产生环。约定 delegate 应为 weak(或者 assign 在非 ARC 时或当 delegate 非 ObjC 对象)。
@property (nonatomic, weak) id<MyDelegate> delegate;
注意:weak 只对 ObjC 对象有效,Cocoa Touch 的 weak 是零化引用(nil-on-dealloc)。
1.5 KVO(Key-Value Observing)未移除 observer
原因:早期 KVO(手动 addObserver:forKeyPath:)会让 observed 对 observer 有强引用(或系统内部未释放),如果不移除 observer,会导致对象无法释放或崩溃(iOS 11+ 部分改进,但仍建议手动移除)。
修复:
- 在
dealloc中removeObserver:forKeyPath:(或使用 block-based KVO,或 iOS 11+ 的-observeValueForKeyPath:的自动移除机制,但不要依赖)。 - 使用
NSKeyValueObservation(iOS 11+)并保留 observation 对象;当 observation 释放时会自动移除。
1.6 NSNotificationCenter 未移除 observer
原因:如果使用 [[NSNotificationCenter defaultCenter] addObserver:selector:name:object:],NSNotificationCenter 会保留 observer(在某些 iOS 版本下);忘记移除会导致泄漏或消息发到已释放对象。
修复:
- 在
dealloc中[[NSNotificationCenter defaultCenter] removeObserver:self]; - 或使用 block API
addObserverForName:object:queue:usingBlock:并保持返回的 observer token,然后移除;iOS 9+ 有NSNotificationCenter的对象已改进,仍建议手动移除。
1.7 CoreFoundation / Bridging 错误(CF 类型未释放)
原因:使用 CF/低层 API(CGPathCreate..., CFBridgingRetain, CFBridgingRelease, CGColorCreate 等)需要手动 CFRelease,不正确的桥接会造成泄漏或野指针。
示例(错):
CGColorRef color = CGColorCreateGenericRGB(1,0,0,1);
// 忘记 CGColorRelease(color);
ARC 下 bridging 错误示例:
CFStringRef cfStr = CFStringCreateWithCString(...); // +1
NSString *ns = (__bridge_transfer NSString *)cfStr; // correct: transfers ownership
// 如果写成 (__bridge NSString *) 则会 leak
修复:
- 使用
__bridge_transfer/CFBridgingRelease来把 CF 转移给 ARC 管理。 - 或者在合适的地方手动
CFRelease。
1.8 GCD(dispatch_*)捕获导致的循环引用
原因:block 被 dispatch_async 复制到队列,会捕获 self 并保留。若 self 又持有某个长期存在的队列/源,会形成环。
示例(会造成延长生命周期):
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self doWork]; // 捕获 self
});
修复同 block:__weak typeof(self) weakSelf = self; 然后在 block 内转 strong。
注意:dispatch_after 等会保留 block 直到执行完成。
1.9 Collection(NSArray/NSDictionary)与对象互相持有
情形:对象放入集合(集合强持有元素),若元素之间形成互相持有,可能无法释放。或集合(例如缓存)为静态单例,导致内容一直存在。
修复:
- 使用
NSHashTable或NSMapTable支持弱引用的集合(weakObjectsHashTable、weakToStrongObjectsMapTable)。 - 控制缓存策略(LRU、清理时机)。
2. 其他类型内存泄漏(非循环引用,也会导致内存持续增长)
2.1 malloc / 手动分配内存未 free
C 函数 malloc/calloc/strdup、posix_memalign 等配对缺失会导致 leak。用 Instruments 的 Allocations / Leaks 可定位。
修复:确保 free() 在合适地方调用,或用 ARC 下转换为 NSData/NSString 等托管对象。
2.2 大对象被缓存(单例/静态缓存/全局数组)
单例或静态容器无限制添加对象会增长内存。审查缓存策略,限制缓存大小。
2.3 图像、CGImage、CIImage、纹理资源未释放
CGImageRef、CGContext、CVPixelBufferRef 等需 CFRelease。UIImage.imageNamed: 会缓存图片,注意是否合适(大图慎用)。
2.4 RunLoop Mode/Observation 导致持有
定制的 RunLoop source 或 CFRunLoopObserver 未移除也会保留对象。
3. 如何用工具定位(实战步骤)
3.1 Xcode Memory Graph(内置)
- 运行并复现内存问题(例如打开页面多次)。
- 在 Xcode 的 debug bar 点击左侧的 Memory Graph(或运行时底栏的 Debug Memory Graph)。
- 点击 Capture Memory Graph(快照),在图谱中搜索未释放的对象(例如你的 VC 类名)。
- 查看引用路径(Retain/Strong references)——它会显示哪条引用链阻止对象释放(很适合找 block / timer / strong property 引用)。
- 根据路径修改代码(将某处改为 weak、invalidate timer、remove observer 等),再复测。
Memory Graph 特别适合“对象名明确、引用路径短”的问题。
3.2 Instruments — Leaks / Allocations
- 打开 Xcode → Product → Profile(或直接打开 Instruments)。
- 选择 Leaks 采样模板(同时看 Allocations 来看内存使用趋势)。
- 运行常用流程,观察 Leaks timeline 或 Persistent Allocations。
- 点击某个泄露或持续分配项,查看 backtrace / responsible caller(可以定位是哪个 API 分配的)。
- 对 Objective-C 对象可以启用 “Record reference counts” 来分析 retain/release 调用(有性能损耗)。
3.3 Address Sanitizer / Zombies / Guard Malloc
- Zombies:帮助发现对已释放对象的消息调用(不是找 leak,但常用于内存错误)。
- Address Sanitizer:检测越界、use-after-free 等低层错误(不同于 leak)。
- Guard Malloc:检测内存越界,严重影响性能,只做短期测试。
3.4 手工方法(日志、断点)
- 在
-dealloc打断点或写NSLog(@"dealloc: %@", self)来确认对象是否释放。 - 对可疑属性改为
weak看是否会被置 nil(帮你判断是不是强引用保留)。 - 使用
malloc_size()/vm_read等低层 API 非常规方法。
4. 常见场景与对应的“修复清单” (排查 checklist)
-
找不到
dealloc被调用的 VC/Manager?- 检查:block 属性 / NSTimer / GCD block / display link / KVO / Notification / delegate / strong child reference。
- 修复:weakify block;invalidate timer;remove observer;delegate 用 weak。
-
大量 CF 对象(CG、CV、CF)增长?
- 检查:是否有未释放的
CGPathRef/CGImageRef/CGColorRef/CFStringRef/CVPixelBufferRef。 - 修复:在合适位置
CFRelease或使用__bridge_transfer。
- 检查:是否有未释放的
-
单例或缓存不断增长?
- 检查:缓存策略、是否 remove 对象、是否把临时对象误放到单例数组。
- 修复:用 NSCache(自动释放)、添加最大容量逻辑、在 memoryWarning 时清理。
-
Block capture 导致 VC 无法释放?
- 检查 block 是否作为 property 存在(例如网络回调、动画 completion)。
- 修复:
__weak/__strong组合;把 block 放到单独 handler 对象,由 handler 管理生命周期。
-
KVO/Notification 崩溃或泄漏?
- 检查是否在
dealloc中removeObserver:或是否使用NSKeyValueObservationtoken。 - 修复:使用 token 自动移除,或在
dealloc中移除。
- 检查是否在
5. 代码示例集(问题 -> 修复)——更完整的例子
5.1 block 捕获 self(网络回调存为 property)
// 问题:self -> completionBlock -> self
@property (nonatomic, copy) void (^completionBlock)(void);
- (void)setup {
self.completionBlock = ^{
[self doSomething]; // 捕获 self
};
}
修复:
__weak typeof(self) weakSelf = self;
self.completionBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
[strongSelf doSomething];
};
5.2 NSTimer 的典型问题与 Proxy 解决方案
问题:scheduledTimerWithTimeInterval:target:selector:repeats: 会让 RunLoop 持有 timer,而 timer 强持有 target(self)。
修复:使用 WeakProxy 或 block timer(iOS 10+)。
// block timer (iOS 10+)
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
__strong typeof(self) strongSelf = weakSelf;
if (!strongSelf) {
[timer invalidate];
return;
}
[strongSelf tick:timer];
}];
5.3 KVO 未移除 — 导致崩溃或泄露
修复(iOS 11+ 推荐):
@property (nonatomic, strong) NSKeyValueObservation *obs;
self.obs = [someObj observe:@"property" options:NSKeyValueObservingOptionNew handler:^(id obj, NSDictionary *change) {
// handle
}];
// 当 self.obs 释放时会自动移除
或手动 removeObserver: 在 dealloc 中。
5.4 CoreFoundation 桥接错误
CFStringRef cf = CFStringCreateWithCString(NULL, "abc", kCFStringEncodingUTF8);
// ARC 管理不到 cf,必须释放
NSString *s = (__bridge_transfer NSString *)cf; // 转移所有权给 ARC —— 自动释放
// 或 CFRelease(cf) 当不再使用时
6. ARC 下的关键关键字(简明)
-
strong:默认。持有引用,增加 retain count。 -
weak:零化弱引用(对象被释放时自动变 nil)。适用于 delegate、parent 指向 child 的反向引用等。 -
assign:非对象类型或非 ARC 对象(危险)。 -
copy:copy 一份(常用于 NSString、block)。 -
__weak/__unsafe_unretained(unsafe 不会置 nil,已释放后变成野指针,很危险)。 -
__bridge/__bridge_retained/__bridge_transfer:CF 与 ObjC 桥接时管理所有权的工具,使用错误会泄漏或过早释放。
7. 避免与最佳实践(建议清单)
- delegate 一律
weak(除非有非常特殊的生命周期需求)。 - block 捕获 self 时总是
weakSelf+strongSelf模式(防止在 block 执行过程中对象被释放导致崩溃)。 - Timer / CADisplayLink / RunLoopSource:创建后在
dealloc或适当生命周期方法中 invalidate。 - KVO / Notification:成对使用 add/remove,prefer token-based APIs。
- 使用
NSCache或带清理策略的缓存,不要把大图放到永不清除的数组里。 - 对 CF 类型使用
__bridge_transfer或手动 CFRelease。 - 经常运行 Memory Graph 来捕捉“VC 未释放”的情况。
- 对长期异步任务使用弱引用:dispatch_async、dispatch_after、网络库回调等。
8. 进阶:复杂保留环与工具技巧
- 多对象图:retain cycle 不一定是两个对象,有时是链:A -> B -> C -> A。Memory Graph 能显示路径,但复杂图需要耐心定位。
-
弱集合:
[NSHashTable weakObjectsHashTable]保存弱引用集合(不增加元素引用计数)。 - NSProxy / 中间层:用 proxy 做中介(例如 timer 的 target),避免 timer 直接持有 VC。
- 自动检测脚本:CI 中运行 static analyzer(Xcode 的 Analyze)可以捕获一些潜在问题,但不能替代动态检测。
9. 快速排查模板(实战步骤)
- 复现:按用户报问题的场景反复触发(例如 push/pop 页面多次)。
- 加
NSLog(@"dealloc: %@", self)或 break on dealloc 检查对象是否调用了dealloc。 - 捕快照:Xcode Memory Graph → Capture snapshot → 搜索类名 → 查看引用路径。
- Instruments Leaks/Allocations:分析持续增长对象与分配栈信息。
- 根据引用路径定位(通常第一步就能指向问题:timer、block、notification、delegate、CF 等)。
- 修复(弱化引用 / invalidate / removeObserver / CFRelease)。
- 回归:再次运行测试场景确保释放、内存稳定。
10. 常见误区(提醒)
-
__block在 ARC 下并不能替代__weak来断开环(__block会导致被捕获对象变为可变,ARC 下 __block 对象通常是强引用,除非加上 __weak)。 -
weak不是万能的:如果被 weak 的对象在短时间内变 nil,后续逻辑需要判断 nil。 - 不要滥用
__unsafe_unretained—— 只有在性能极其敏感并且你能保证生命周期的人用。 - memory graph 显示的“referenced by” 不一定是问题根源(例如系统缓存等),需要结合上下文判断。