內(nèi)存泄漏
內(nèi)存泄漏指的是程序中已動(dòng)態(tài)分配的堆內(nèi)存由于某些原因未能釋放或無法釋放,造成系統(tǒng)內(nèi)存的浪費(fèi),導(dǎo)致程序運(yùn)行速度變慢甚至系統(tǒng)崩潰。
在 iOS 開發(fā)中會(huì)遇到的內(nèi)存泄漏場(chǎng)景可以分為幾類:
循環(huán)引用
當(dāng)對(duì)象 A 強(qiáng)引用對(duì)象 B,而對(duì)象 B 又強(qiáng)引用對(duì)象 A,或者多個(gè)對(duì)象互相強(qiáng)引用形成一個(gè)閉環(huán),就是循環(huán)引用。
Block
Block 會(huì)對(duì)其內(nèi)部的對(duì)象強(qiáng)引用,因此使用的時(shí)候需要確保不會(huì)形成循環(huán)引用。
舉個(gè)例子,看下面這段代碼:
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", self.name);
});
};
self.block();
block 是 self 的屬性,因此 self 強(qiáng)引用了 block,而 block 內(nèi)部又調(diào)用了 self,因此 block 也強(qiáng)引用了 self。要解決這個(gè)循環(huán)引用的問題,有兩種思路。
使用 Weak-Strong Dance
先用 __weak 將 self 置為弱引用,打破“循環(huán)”關(guān)系,但是 weakSelf 在 block 中可能被提前釋放,因此還需要在 block 內(nèi)部,用 __strong 對(duì) weakSelf 進(jìn)行強(qiáng)引用,這樣可以確保 strongSelf 在 block 結(jié)束后才會(huì)被釋放。
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", strongSelf.name);
});
};
self.block();
斷開持有關(guān)系
使用 __block 關(guān)鍵字設(shè)置一個(gè)指針 vc 指向 self,重新形成一個(gè) self → block → vc → self 的循環(huán)持有鏈。在調(diào)用結(jié)束后,將 vc 置為 nil,就能斷開循環(huán)持有鏈,從而令 self 正常釋放。
__block UIViewController *vc = self;
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", vc.name);
vc = nil;
});
};
self.block();
NSTimer
NSTimer 對(duì)象是采用 target-action 方式創(chuàng)建的,通常 target 就是類本身,為了方便又常把 NSTimer 聲明為屬性:
// 第一種創(chuàng)建方式,timer 默認(rèn)添加進(jìn) runloop
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timeFire) userInfo:nil repeats:YES];
// 第二種創(chuàng)建方式,需要手動(dòng)將 timer 添加進(jìn) runloop
self.timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(timeFire) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
這就形成了 self → timer → self(target) 的循環(huán)持有鏈。只要 self 不釋放,dealloc 就不會(huì)執(zhí)行,timer 就無法在 dealloc 中銷毀,self 始終被強(qiáng)引用,永遠(yuǎn)得不到釋放,循環(huán)矛盾,最終造成內(nèi)存泄漏。
解決方式:
在合適的時(shí)機(jī)銷毀 NSTimer
當(dāng) NSTimer 初始化之后,加入 runloop 會(huì)導(dǎo)致被當(dāng)前的頁面強(qiáng)引用,因此不會(huì)執(zhí)行 dealloc。所以需要在合適的時(shí)機(jī)銷毀 _timer,斷開 _timer、runloop 和當(dāng)前頁面之間的強(qiáng)引用關(guān)系。
使用 GCD 的定時(shí)器
GCD 不基于 runloop,可以用 GCD 的計(jì)時(shí)器代替 NSTimer 實(shí)現(xiàn)計(jì)時(shí)任務(wù)。
使用帶 block 的 timer
iOS 10 之后,Apple 提供了一種 block 的方式來解決循環(huán)引用的問題。
######1.NSTimer
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
為了兼容 iOS 10 之前的方法,可以寫成 NSTimer 分類的形式,將 block 作為 SEL 傳入初始化方法中,統(tǒng)一以 block 的形式處理回調(diào)。
// NSTimer+WeakTimer.m
#import "NSTimer+WeakTimer.h"
@implementation NSTimer (WeakTimer)
+ (NSTimer *)ht_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
repeats:(BOOL)repeats
block:(void(^)(void))block {
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(ht_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}
+ (void)ht_blockInvoke:(NSTimer *)timer {
void (^block)(void) = timer.userInfo;
if(block) {
block();
}
}
@end
然后在需要的類中創(chuàng)建 timer。
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer ht_scheduledTimerWithTimeInterval:1.0f repeats:YES block:^{
[weakSelf timeFire];
}];
循環(huán)加載引起內(nèi)存峰值
因?yàn)檠h(huán)內(nèi)產(chǎn)生大量的臨時(shí)對(duì)象,直至循環(huán)結(jié)束才釋放,可能導(dǎo)致內(nèi)存泄漏。
for (int i = 0; i < 1000000; i++) {
NSString *str = @"Abc";
str = [str lowercaseString];
str = [str stringByAppendingString:@"xyz"];
NSLog(@"%@", str);
}
解決方案:在循環(huán)中創(chuàng)建自己的 autoreleasepool,及時(shí)釋放占用內(nèi)存大的臨時(shí)變量,減少內(nèi)存占用峰值。
for (int i = 0; i < 100000; i++) {
@autoreleasepool {
NSString *str = @"Abc";
str = [str lowercaseString];
str = [str stringByAppendingString:@"xyz"];
NSLog(@"%@", str);
}
}
野指針與僵尸對(duì)象
指針指向的對(duì)象已經(jīng)被釋放/回收,這個(gè)指針就叫做野指針。這個(gè)被釋放的對(duì)象就是僵尸對(duì)象。
如果用野指針去訪問僵尸對(duì)象,或者說向野指針發(fā)送消息,會(huì)發(fā)生 EXC_BAD_ACCESS 崩潰,出現(xiàn)內(nèi)存泄漏。
// MRC 下
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
[stu setAge:18];
[stu release]; // stu 在 release 之后,內(nèi)存空間被釋放并回收,stu 變成野指針
// [stu setAge:20]; // set 再調(diào)用 setAge 就會(huì)崩潰
}
return 0;
}
解決方案:當(dāng)對(duì)象釋放后,應(yīng)該將其置為 nil。
常用的內(nèi)存檢查工具
Instruments
Instruments 是 Xcode 自帶的工具集合,為開發(fā)者提供強(qiáng)大的程序性能分析和測(cè)試能力。
它打開方式為:Xcode → Open Developer Tool → Instruments。其中的 Allocations、Leaks 和 Zombies 功能可以協(xié)助我們進(jìn)行內(nèi)存泄漏檢查。
? Leaks:動(dòng)態(tài)檢查泄漏的內(nèi)存,如果檢查過程時(shí)出現(xiàn)了紅色叉叉,就說明存在內(nèi)存泄漏,可以定位到泄漏的位置,去解決問題。此外,Xcode 中還提供靜態(tài)監(jiān)測(cè)方法 Analyze,可以直接通過 Product → Analyze 打開,如果出現(xiàn)泄漏,會(huì)出現(xiàn)“藍(lán)色分支圖標(biāo)”提示。
? Allocations:用來檢查內(nèi)存使用/分配情況。比如出現(xiàn)“循環(huán)加載引起內(nèi)存峰值”的情況,就可以通過這個(gè)工具檢查出來。
? Zombies:檢查是否訪問了僵尸對(duì)象。
Instruments 的使用相對(duì)來說比較復(fù)雜,你也可以通過在工程中引入一些第三方框架進(jìn)行檢測(cè)。
MLeaksFinder
MLeaksFinder 是 WeRead 團(tuán)隊(duì)開源的 iOS 內(nèi)存泄漏檢測(cè)工具。
它的使用非常簡(jiǎn)單,只要在工程引入框架,就可以在 App 運(yùn)行過程中監(jiān)測(cè)到內(nèi)存泄漏的對(duì)象并立即提醒。MLeaksFinder 也不具備侵入性,使用時(shí)無需在 release 版本移除,因?yàn)樗粫?huì)在 debug 版本生效。
不過 MLeaksFinder 的只能定位到內(nèi)存泄漏的對(duì)象,如果你想要檢查該對(duì)象是否存在循環(huán)引用。就結(jié)合 FBRetainCycleDetector 一起使用。
FBRetainCycleDetector
FBRetainCycleDetector 是 Facebook 開源的一個(gè)循環(huán)引用檢測(cè)工具。它會(huì)遞歸遍歷傳入內(nèi)存的 OC 對(duì)象的所有強(qiáng)引用的對(duì)象,檢測(cè)以該對(duì)象為根結(jié)點(diǎn)的強(qiáng)引用樹有沒有出現(xiàn)循環(huán)引用。