Objective-C 下常見(jiàn)內(nèi)存泄漏(leak)和循環(huán)引用 / 保留環(huán)(retain cycle)

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ù)方式:

  • deallocviewWillDisappear: 中調(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ù):

  • deallocremoveObserver: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ù):

  • 使用 NSHashTableNSMapTable 支持弱引用的集合(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)置)

  1. 運(yùn)行并復(fù)現(xiàn)內(nèi)存問(wèn)題(例如打開(kāi)頁(yè)面多次)。
  2. 在 Xcode 的 debug bar 點(diǎn)擊左側(cè)的 Memory Graph(或運(yùn)行時(shí)底欄的 Debug Memory Graph)。
  3. 點(diǎn)擊 Capture Memory Graph(快照),在圖譜中搜索未釋放的對(duì)象(例如你的 VC 類(lèi)名)。
  4. 查看引用路徑(Retain/Strong references)——它會(huì)顯示哪條引用鏈阻止對(duì)象釋放(很適合找 block / timer / strong property 引用)。
  5. 根據(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)

  1. 找不到 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。
  2. 大量 CF 對(duì)象(CG、CV、CF)增長(zhǎng)?

    • 檢查:是否有未釋放的 CGPathRef/CGImageRef/CGColorRef/CFStringRef/CVPixelBufferRef。
    • 修復(fù):在合適位置 CFRelease 或使用 __bridge_transfer。
  3. 單例或緩存不斷增長(zhǎng)?

    • 檢查:緩存策略、是否 remove 對(duì)象、是否把臨時(shí)對(duì)象誤放到單例數(shù)組。
    • 修復(fù):用 NSCache(自動(dòng)釋放)、添加最大容量邏輯、在 memoryWarning 時(shí)清理。
  4. 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 管理生命周期。
  5. KVO/Notification 崩潰或泄漏?

    • 檢查是否在 deallocremoveObserver: 或是否使用 NSKeyValueObservation token。
    • 修復(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)步驟)

  1. 復(fù)現(xiàn):按用戶(hù)報(bào)問(wèn)題的場(chǎng)景反復(fù)觸發(fā)(例如 push/pop 頁(yè)面多次)。
  2. NSLog(@"dealloc: %@", self) 或 break on dealloc 檢查對(duì)象是否調(diào)用了 dealloc
  3. 捕快照:Xcode Memory Graph → Capture snapshot → 搜索類(lèi)名 → 查看引用路徑。
  4. Instruments Leaks/Allocations:分析持續(xù)增長(zhǎng)對(duì)象與分配棧信息。
  5. 根據(jù)引用路徑定位(通常第一步就能指向問(wèn)題:timer、block、notification、delegate、CF 等)。
  6. 修復(fù)(弱化引用 / invalidate / removeObserver / CFRelease)。
  7. 回歸:再次運(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é)合上下文判斷。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容