Objective-C 下常見(jiàn)內(nèi)存泄漏(leak)和循環(huán)引用 / 保留環(huán)(retain cycle)的類(lèi)型逐項(xiàng)歸類(lèi)、解釋成因、給出最小可復(fù)現(xiàn) Objective-C 示例(含問(wèn)題代碼與修復(fù)代碼),并列出定位與避免的實(shí)戰(zhàn)方法(Xcode Memory Graph、Instruments、常用調(diào)試技巧與注意點(diǎn))。
概覽(先看要點(diǎn))
- 內(nèi)存泄漏(leak):對(duì)象分配后永遠(yuǎn)沒(méi)有被釋放(或 CoreFoundation 類(lèi)型沒(méi)有被 CFRelease),內(nèi)存持續(xù)增長(zhǎng)。常見(jiàn)于忘記釋放 CF 對(duì)象、malloc/ calloc 未 free、單例/靜態(tài)緩存、未移除 observer/timer/runloop source 等。
- 循環(huán)引用 / 保留環(huán)(retain cycle):兩個(gè)或多個(gè)對(duì)象通過(guò)強(qiáng)引用鏈互相持有,導(dǎo)致 ARC 無(wú)法回收。最常見(jiàn)的是:對(duì)象 ? block(block 捕獲 self),對(duì)象 ? 對(duì)象(兩個(gè)對(duì)象都持有 strong),集合持有對(duì)象(strong)且對(duì)象反向持有集合(strong),Delegate 非 weak 等。
- 定位工具:Xcode Memory Graph(可視化引用關(guān)系)、Instruments(Leaks、Allocations)、static analyzer、Malloc/Guard Malloc、Address Sanitizer(不同目的)。
下面分門(mén)別類(lèi)地講。
1. 最常見(jiàn)的“循環(huán)引用”類(lèi)型(帶示例與修復(fù))
1.1 對(duì)象 — block(block 捕獲 self)
原因:block 在堆上會(huì)強(qiáng)引用它捕獲的對(duì)象,若對(duì)象持有該 block(例如把 block 存為屬性),就形成循環(huán)引用:self -> block -> self。
示例(有問(wèn)題):
@interface MyController : UIViewController
@property (nonatomic, copy) void (^myBlock)(void);
@end
@implementation MyController
- (void)viewDidLoad {
[super viewDidLoad];
self.myBlock = ^{
// 捕獲 self —— 強(qiáng)引用
NSLog(@"%@", self.view);
};
}
@end
修復(fù)(用 __weak / __typeof / strongify):
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
NSLog(@"%@", strongSelf.view);
};
或者將 block 屬性設(shè)為 assign(不推薦)或?qū)?block 作為局部變量不用屬性持有,或把屬性放到另一個(gè)不被 self 強(qiáng)持有的 owner。
1.2 NSTimer / CADisplayLink / RunLoop Source 沒(méi)釋放
原因:系統(tǒng)對(duì)象(NSTimer、CADisplayLink)默認(rèn)由 RunLoop 保持強(qiáng)引用,timer 的 target 通常是 self;若 timer 未 invalidated,會(huì)保留 target。
示例(有問(wèn)題):
@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
// 若沒(méi)有在 dealloc 或 viewWillDisappear 時(shí) [self.timer invalidate], timer 會(huì)保留 self。
修復(fù)方式:
- 在
dealloc或viewWillDisappear:中調(diào)用[timer invalidate]并且self.timer = nil;。 - 使用
NSTimer的 block API(iOS 10+)并弱化 self。 - 使用
CADisplayLink同理:[displayLink invalidate]。 - 使用
GCD定時(shí)器(dispatch_source_t)并且取消來(lái)源。
示例(proxy 解法避免強(qiáng)引用):
@interface WeakProxy : NSProxy
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
// 實(shí)現(xiàn)簡(jiǎn)單代理,timer target 用 WeakProxy,這樣 proxy 不強(qiáng)持有 target。
1.3 對(duì)象 ? 對(duì)象 雙向 strong
原因:兩個(gè)對(duì)象互相持有 strong(例如 parent strong 持有 child,child strong 持有 parent)。
示例(有問(wèn)題):
@interface Parent : NSObject
@property (nonatomic, strong) Child *child;
@end
@interface Child : NSObject
@property (nonatomic, strong) Parent *parent;
@end
修復(fù):通常把一方改成 weak(通常 child 對(duì) parent 用 weak):
@property (nonatomic, weak) Parent *parent;
若需要非零弱引用(weak 會(huì)被置 nil),可以用 __unsafe_unretained(極少數(shù)場(chǎng)景)或使用 assign(非對(duì)象)。
1.4 Delegate 未用 weak(或協(xié)議未指定 weak)
Delegate 模式若 delegate 屬性為 strong,會(huì)很容易產(chǎn)生環(huán)。約定 delegate 應(yīng)為 weak(或者 assign 在非 ARC 時(shí)或當(dāng) delegate 非 ObjC 對(duì)象)。
@property (nonatomic, weak) id<MyDelegate> delegate;
注意:weak 只對(duì) ObjC 對(duì)象有效,Cocoa Touch 的 weak 是零化引用(nil-on-dealloc)。
1.5 KVO(Key-Value Observing)未移除 observer
原因:早期 KVO(手動(dòng) addObserver:forKeyPath:)會(huì)讓 observed 對(duì) observer 有強(qiáng)引用(或系統(tǒng)內(nèi)部未釋放),如果不移除 observer,會(huì)導(dǎo)致對(duì)象無(wú)法釋放或崩潰(iOS 11+ 部分改進(jìn),但仍建議手動(dòng)移除)。
修復(fù):
- 在
dealloc中removeObserver:forKeyPath:(或使用 block-based KVO,或 iOS 11+ 的-observeValueForKeyPath:的自動(dòng)移除機(jī)制,但不要依賴(lài))。 - 使用
NSKeyValueObservation(iOS 11+)并保留 observation 對(duì)象;當(dāng) observation 釋放時(shí)會(huì)自動(dòng)移除。
1.6 NSNotificationCenter 未移除 observer
原因:如果使用 [[NSNotificationCenter defaultCenter] addObserver:selector:name:object:],NSNotificationCenter 會(huì)保留 observer(在某些 iOS 版本下);忘記移除會(huì)導(dǎo)致泄漏或消息發(fā)到已釋放對(duì)象。
修復(fù):
- 在
dealloc中[[NSNotificationCenter defaultCenter] removeObserver:self]; - 或使用 block API
addObserverForName:object:queue:usingBlock:并保持返回的 observer token,然后移除;iOS 9+ 有NSNotificationCenter的對(duì)象已改進(jìn),仍建議手動(dòng)移除。
1.7 CoreFoundation / Bridging 錯(cuò)誤(CF 類(lèi)型未釋放)
原因:使用 CF/低層 API(CGPathCreate..., CFBridgingRetain, CFBridgingRelease, CGColorCreate 等)需要手動(dòng) CFRelease,不正確的橋接會(huì)造成泄漏或野指針。
示例(錯(cuò)):
CGColorRef color = CGColorCreateGenericRGB(1,0,0,1);
// 忘記 CGColorRelease(color);
ARC 下 bridging 錯(cuò)誤示例:
CFStringRef cfStr = CFStringCreateWithCString(...); // +1
NSString *ns = (__bridge_transfer NSString *)cfStr; // correct: transfers ownership
// 如果寫(xiě)成 (__bridge NSString *) 則會(huì) leak
修復(fù):
- 使用
__bridge_transfer/CFBridgingRelease來(lái)把 CF 轉(zhuǎn)移給 ARC 管理。 - 或者在合適的地方手動(dòng)
CFRelease。
1.8 GCD(dispatch_*)捕獲導(dǎo)致的循環(huán)引用
原因:block 被 dispatch_async 復(fù)制到隊(duì)列,會(huì)捕獲 self 并保留。若 self 又持有某個(gè)長(zhǎng)期存在的隊(duì)列/源,會(huì)形成環(huán)。
示例(會(huì)造成延長(zhǎng)生命周期):
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self doWork]; // 捕獲 self
});
修復(fù)同 block:__weak typeof(self) weakSelf = self; 然后在 block 內(nèi)轉(zhuǎn) strong。
注意:dispatch_after 等會(huì)保留 block 直到執(zhí)行完成。
1.9 Collection(NSArray/NSDictionary)與對(duì)象互相持有
情形:對(duì)象放入集合(集合強(qiáng)持有元素),若元素之間形成互相持有,可能無(wú)法釋放?;蚣希ɡ缇彺妫殪o態(tài)單例,導(dǎo)致內(nèi)容一直存在。
修復(fù):
- 使用
NSHashTable或NSMapTable支持弱引用的集合(weakObjectsHashTable、weakToStrongObjectsMapTable)。 - 控制緩存策略(LRU、清理時(shí)機(jī))。
2. 其他類(lèi)型內(nèi)存泄漏(非循環(huán)引用,也會(huì)導(dǎo)致內(nèi)存持續(xù)增長(zhǎng))
2.1 malloc / 手動(dòng)分配內(nèi)存未 free
C 函數(shù) malloc/calloc/strdup、posix_memalign 等配對(duì)缺失會(huì)導(dǎo)致 leak。用 Instruments 的 Allocations / Leaks 可定位。
修復(fù):確保 free() 在合適地方調(diào)用,或用 ARC 下轉(zhuǎn)換為 NSData/NSString 等托管對(duì)象。
2.2 大對(duì)象被緩存(單例/靜態(tài)緩存/全局?jǐn)?shù)組)
單例或靜態(tài)容器無(wú)限制添加對(duì)象會(huì)增長(zhǎng)內(nèi)存。審查緩存策略,限制緩存大小。
2.3 圖像、CGImage、CIImage、紋理資源未釋放
CGImageRef、CGContext、CVPixelBufferRef 等需 CFRelease。UIImage.imageNamed: 會(huì)緩存圖片,注意是否合適(大圖慎用)。
2.4 RunLoop Mode/Observation 導(dǎo)致持有
定制的 RunLoop source 或 CFRunLoopObserver 未移除也會(huì)保留對(duì)象。
3. 如何用工具定位(實(shí)戰(zhàn)步驟)
3.1 Xcode Memory Graph(內(nèi)置)
- 運(yùn)行并復(fù)現(xiàn)內(nèi)存問(wèn)題(例如打開(kāi)頁(yè)面多次)。
- 在 Xcode 的 debug bar 點(diǎn)擊左側(cè)的 Memory Graph(或運(yùn)行時(shí)底欄的 Debug Memory Graph)。
- 點(diǎn)擊 Capture Memory Graph(快照),在圖譜中搜索未釋放的對(duì)象(例如你的 VC 類(lèi)名)。
- 查看引用路徑(Retain/Strong references)——它會(huì)顯示哪條引用鏈阻止對(duì)象釋放(很適合找 block / timer / strong property 引用)。
- 根據(jù)路徑修改代碼(將某處改為 weak、invalidate timer、remove observer 等),再?gòu)?fù)測(cè)。
Memory Graph 特別適合“對(duì)象名明確、引用路徑短”的問(wèn)題。
3.2 Instruments — Leaks / Allocations
- 打開(kāi) Xcode → Product → Profile(或直接打開(kāi) Instruments)。
- 選擇 Leaks 采樣模板(同時(shí)看 Allocations 來(lái)看內(nèi)存使用趨勢(shì))。
- 運(yùn)行常用流程,觀察 Leaks timeline 或 Persistent Allocations。
- 點(diǎn)擊某個(gè)泄露或持續(xù)分配項(xiàng),查看 backtrace / responsible caller(可以定位是哪個(gè) API 分配的)。
- 對(duì) Objective-C 對(duì)象可以啟用 “Record reference counts” 來(lái)分析 retain/release 調(diào)用(有性能損耗)。
3.3 Address Sanitizer / Zombies / Guard Malloc
- Zombies:幫助發(fā)現(xiàn)對(duì)已釋放對(duì)象的消息調(diào)用(不是找 leak,但常用于內(nèi)存錯(cuò)誤)。
- Address Sanitizer:檢測(cè)越界、use-after-free 等低層錯(cuò)誤(不同于 leak)。
- Guard Malloc:檢測(cè)內(nèi)存越界,嚴(yán)重影響性能,只做短期測(cè)試。
3.4 手工方法(日志、斷點(diǎn))
- 在
-dealloc打斷點(diǎn)或?qū)?NSLog(@"dealloc: %@", self)來(lái)確認(rèn)對(duì)象是否釋放。 - 對(duì)可疑屬性改為
weak看是否會(huì)被置 nil(幫你判斷是不是強(qiáng)引用保留)。 - 使用
malloc_size()/vm_read等低層 API 非常規(guī)方法。
4. 常見(jiàn)場(chǎng)景與對(duì)應(yīng)的“修復(fù)清單” (排查 checklist)
-
找不到
dealloc被調(diào)用的 VC/Manager?- 檢查:block 屬性 / NSTimer / GCD block / display link / KVO / Notification / delegate / strong child reference。
- 修復(fù):weakify block;invalidate timer;remove observer;delegate 用 weak。
-
大量 CF 對(duì)象(CG、CV、CF)增長(zhǎng)?
- 檢查:是否有未釋放的
CGPathRef/CGImageRef/CGColorRef/CFStringRef/CVPixelBufferRef。 - 修復(fù):在合適位置
CFRelease或使用__bridge_transfer。
- 檢查:是否有未釋放的
-
單例或緩存不斷增長(zhǎng)?
- 檢查:緩存策略、是否 remove 對(duì)象、是否把臨時(shí)對(duì)象誤放到單例數(shù)組。
- 修復(fù):用 NSCache(自動(dòng)釋放)、添加最大容量邏輯、在 memoryWarning 時(shí)清理。
-
Block capture 導(dǎo)致 VC 無(wú)法釋放?
- 檢查 block 是否作為 property 存在(例如網(wǎng)絡(luò)回調(diào)、動(dòng)畫(huà) completion)。
- 修復(fù):
__weak/__strong組合;把 block 放到單獨(dú) handler 對(duì)象,由 handler 管理生命周期。
-
KVO/Notification 崩潰或泄漏?
- 檢查是否在
dealloc中removeObserver:或是否使用NSKeyValueObservationtoken。 - 修復(fù):使用 token 自動(dòng)移除,或在
dealloc中移除。
- 檢查是否在
5. 代碼示例集(問(wèn)題 -> 修復(fù))——更完整的例子
5.1 block 捕獲 self(網(wǎng)絡(luò)回調(diào)存為 property)
// 問(wèn)題:self -> completionBlock -> self
@property (nonatomic, copy) void (^completionBlock)(void);
- (void)setup {
self.completionBlock = ^{
[self doSomething]; // 捕獲 self
};
}
修復(fù):
__weak typeof(self) weakSelf = self;
self.completionBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
[strongSelf doSomething];
};
5.2 NSTimer 的典型問(wèn)題與 Proxy 解決方案
問(wèn)題:scheduledTimerWithTimeInterval:target:selector:repeats: 會(huì)讓 RunLoop 持有 timer,而 timer 強(qiáng)持有 target(self)。
修復(fù):使用 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 未移除 — 導(dǎo)致崩潰或泄露
修復(fù)(iOS 11+ 推薦):
@property (nonatomic, strong) NSKeyValueObservation *obs;
self.obs = [someObj observe:@"property" options:NSKeyValueObservingOptionNew handler:^(id obj, NSDictionary *change) {
// handle
}];
// 當(dāng) self.obs 釋放時(shí)會(huì)自動(dòng)移除
或手動(dòng) removeObserver: 在 dealloc 中。
5.4 CoreFoundation 橋接錯(cuò)誤
CFStringRef cf = CFStringCreateWithCString(NULL, "abc", kCFStringEncodingUTF8);
// ARC 管理不到 cf,必須釋放
NSString *s = (__bridge_transfer NSString *)cf; // 轉(zhuǎn)移所有權(quán)給 ARC —— 自動(dòng)釋放
// 或 CFRelease(cf) 當(dāng)不再使用時(shí)
6. ARC 下的關(guān)鍵關(guān)鍵字(簡(jiǎn)明)
-
strong:默認(rèn)。持有引用,增加 retain count。 -
weak:零化弱引用(對(duì)象被釋放時(shí)自動(dòng)變 nil)。適用于 delegate、parent 指向 child 的反向引用等。 -
assign:非對(duì)象類(lèi)型或非 ARC 對(duì)象(危險(xiǎn))。 -
copy:copy 一份(常用于 NSString、block)。 -
__weak/__unsafe_unretained(unsafe 不會(huì)置 nil,已釋放后變成野指針,很危險(xiǎn))。 -
__bridge/__bridge_retained/__bridge_transfer:CF 與 ObjC 橋接時(shí)管理所有權(quán)的工具,使用錯(cuò)誤會(huì)泄漏或過(guò)早釋放。
7. 避免與最佳實(shí)踐(建議清單)
- delegate 一律
weak(除非有非常特殊的生命周期需求)。 - block 捕獲 self 時(shí)總是
weakSelf+strongSelf模式(防止在 block 執(zhí)行過(guò)程中對(duì)象被釋放導(dǎo)致崩潰)。 - Timer / CADisplayLink / RunLoopSource:創(chuàng)建后在
dealloc或適當(dāng)生命周期方法中 invalidate。 - KVO / Notification:成對(duì)使用 add/remove,prefer token-based APIs。
- 使用
NSCache或帶清理策略的緩存,不要把大圖放到永不清除的數(shù)組里。 - 對(duì) CF 類(lèi)型使用
__bridge_transfer或手動(dòng) CFRelease。 - 經(jīng)常運(yùn)行 Memory Graph 來(lái)捕捉“VC 未釋放”的情況。
- 對(duì)長(zhǎng)期異步任務(wù)使用弱引用:dispatch_async、dispatch_after、網(wǎng)絡(luò)庫(kù)回調(diào)等。
8. 進(jìn)階:復(fù)雜保留環(huán)與工具技巧
- 多對(duì)象圖:retain cycle 不一定是兩個(gè)對(duì)象,有時(shí)是鏈:A -> B -> C -> A。Memory Graph 能顯示路徑,但復(fù)雜圖需要耐心定位。
-
弱集合:
[NSHashTable weakObjectsHashTable]保存弱引用集合(不增加元素引用計(jì)數(shù))。 - NSProxy / 中間層:用 proxy 做中介(例如 timer 的 target),避免 timer 直接持有 VC。
- 自動(dòng)檢測(cè)腳本:CI 中運(yùn)行 static analyzer(Xcode 的 Analyze)可以捕獲一些潛在問(wèn)題,但不能替代動(dòng)態(tài)檢測(cè)。
9. 快速排查模板(實(shí)戰(zhàn)步驟)
- 復(fù)現(xiàn):按用戶(hù)報(bào)問(wèn)題的場(chǎng)景反復(fù)觸發(fā)(例如 push/pop 頁(yè)面多次)。
- 加
NSLog(@"dealloc: %@", self)或 break on dealloc 檢查對(duì)象是否調(diào)用了dealloc。 - 捕快照:Xcode Memory Graph → Capture snapshot → 搜索類(lèi)名 → 查看引用路徑。
- Instruments Leaks/Allocations:分析持續(xù)增長(zhǎng)對(duì)象與分配棧信息。
- 根據(jù)引用路徑定位(通常第一步就能指向問(wèn)題:timer、block、notification、delegate、CF 等)。
- 修復(fù)(弱化引用 / invalidate / removeObserver / CFRelease)。
- 回歸:再次運(yùn)行測(cè)試場(chǎng)景確保釋放、內(nèi)存穩(wěn)定。
10. 常見(jiàn)誤區(qū)(提醒)
-
__block在 ARC 下并不能替代__weak來(lái)斷開(kāi)環(huán)(__block會(huì)導(dǎo)致被捕獲對(duì)象變?yōu)榭勺?,ARC 下 __block 對(duì)象通常是強(qiáng)引用,除非加上 __weak)。 -
weak不是萬(wàn)能的:如果被 weak 的對(duì)象在短時(shí)間內(nèi)變 nil,后續(xù)邏輯需要判斷 nil。 - 不要濫用
__unsafe_unretained—— 只有在性能極其敏感并且你能保證生命周期的人用。 - memory graph 顯示的“referenced by” 不一定是問(wèn)題根源(例如系統(tǒng)緩存等),需要結(jié)合上下文判斷。