Objective-C 下常见内存泄漏(leak)和循环引用 / 保留环(retain cycle)

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。

修复方式:

  • deallocviewWillDisappear: 中调用 [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+ 部分改进,但仍建议手动移除)。

修复:

  • deallocremoveObserver: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)与对象互相持有

情形:对象放入集合(集合强持有元素),若元素之间形成互相持有,可能无法释放。或集合(例如缓存)为静态单例,导致内容一直存在。

修复:

  • 使用 NSHashTableNSMapTable 支持弱引用的集合(weakObjectsHashTableweakToStrongObjectsMapTable)。
  • 控制缓存策略(LRU、清理时机)。

2. 其他类型内存泄漏(非循环引用,也会导致内存持续增长)

2.1 malloc / 手动分配内存未 free

C 函数 malloc/calloc/strdupposix_memalign 等配对缺失会导致 leak。用 Instruments 的 Allocations / Leaks 可定位。

修复:确保 free() 在合适地方调用,或用 ARC 下转换为 NSData/NSString 等托管对象。

2.2 大对象被缓存(单例/静态缓存/全局数组)

单例或静态容器无限制添加对象会增长内存。审查缓存策略,限制缓存大小。

2.3 图像、CGImage、CIImage、纹理资源未释放

CGImageRefCGContextCVPixelBufferRef 等需 CFRelease。UIImage.imageNamed: 会缓存图片,注意是否合适(大图慎用)。

2.4 RunLoop Mode/Observation 导致持有

定制的 RunLoop source 或 CFRunLoopObserver 未移除也会保留对象。


3. 如何用工具定位(实战步骤)

3.1 Xcode Memory Graph(内置)

  1. 运行并复现内存问题(例如打开页面多次)。
  2. 在 Xcode 的 debug bar 点击左侧的 Memory Graph(或运行时底栏的 Debug Memory Graph)。
  3. 点击 Capture Memory Graph(快照),在图谱中搜索未释放的对象(例如你的 VC 类名)。
  4. 查看引用路径(Retain/Strong references)——它会显示哪条引用链阻止对象释放(很适合找 block / timer / strong property 引用)。
  5. 根据路径修改代码(将某处改为 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)

  1. 找不到 dealloc 被调用的 VC/Manager?

    • 检查:block 属性 / NSTimer / GCD block / display link / KVO / Notification / delegate / strong child reference。
    • 修复:weakify block;invalidate timer;remove observer;delegate 用 weak。
  2. 大量 CF 对象(CG、CV、CF)增长?

    • 检查:是否有未释放的 CGPathRef/CGImageRef/CGColorRef/CFStringRef/CVPixelBufferRef
    • 修复:在合适位置 CFRelease 或使用 __bridge_transfer
  3. 单例或缓存不断增长?

    • 检查:缓存策略、是否 remove 对象、是否把临时对象误放到单例数组。
    • 修复:用 NSCache(自动释放)、添加最大容量逻辑、在 memoryWarning 时清理。
  4. Block capture 导致 VC 无法释放?

    • 检查 block 是否作为 property 存在(例如网络回调、动画 completion)。
    • 修复:__weak/__strong 组合;把 block 放到单独 handler 对象,由 handler 管理生命周期。
  5. KVO/Notification 崩溃或泄漏?

    • 检查是否在 deallocremoveObserver: 或是否使用 NSKeyValueObservation token。
    • 修复:使用 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. 快速排查模板(实战步骤)

  1. 复现:按用户报问题的场景反复触发(例如 push/pop 页面多次)。
  2. NSLog(@"dealloc: %@", self) 或 break on dealloc 检查对象是否调用了 dealloc
  3. 捕快照:Xcode Memory Graph → Capture snapshot → 搜索类名 → 查看引用路径。
  4. Instruments Leaks/Allocations:分析持续增长对象与分配栈信息。
  5. 根据引用路径定位(通常第一步就能指向问题:timer、block、notification、delegate、CF 等)。
  6. 修复(弱化引用 / invalidate / removeObserver / CFRelease)。
  7. 回归:再次运行测试场景确保释放、内存稳定。

10. 常见误区(提醒)

  • __block 在 ARC 下并不能替代 __weak 来断开环(__block 会导致被捕获对象变为可变,ARC 下 __block 对象通常是强引用,除非加上 __weak)。
  • weak 不是万能的:如果被 weak 的对象在短时间内变 nil,后续逻辑需要判断 nil。
  • 不要滥用 __unsafe_unretained —— 只有在性能极其敏感并且你能保证生命周期的人用。
  • memory graph 显示的“referenced by” 不一定是问题根源(例如系统缓存等),需要结合上下文判断。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容