iOS內(nèi)存泄漏問題及解決方案

內(nèi)存泄漏

內(nèi)存泄漏指的是程序中已動態(tài)分配的堆內(nèi)存由于某些原因未能釋放或無法釋放苦始,造成系統(tǒng)內(nèi)存的浪費(fèi)婚惫,導(dǎo)致程序運(yùn)行速度變慢甚至系統(tǒng)崩潰氧骤。

在 iOS 開發(fā)中會遇到的內(nèi)存泄漏場景可以分為幾類:

循環(huán)引用

當(dāng)對象 A 強(qiáng)引用對象 B奈泪,而對象 B 又強(qiáng)引用對象 A疾棵,或者多個對象互相強(qiáng)引用形成一個閉環(huán),就是循環(huán)引用馆蠕。

Block

Block 會對其內(nèi)部的對象強(qiáng)引用期升,因此使用的時候需要確保不會形成循環(huán)引用。

舉個例子荆几,看下面這段代碼:

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。要解決這個循環(huán)引用的問題祖秒,有兩種思路诞吱。

使用 Weak-Strong Dance
先用 __weak 將 self 置為弱引用,打破“循環(huán)”關(guān)系竭缝,但是 weakSelf 在 block 中可能被提前釋放房维,因此還需要在 block 內(nèi)部,用 __strong 對 weakSelf 進(jìn)行強(qiáng)引用抬纸,這樣可以確保 strongSelf 在 block 結(jié)束后才會被釋放咙俩。

__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è)置一個指針 vc 指向 self,重新形成一個 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 對象是采用 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)建方式呜呐,需要手動將 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 就不會執(zhí)行蘑辑,timer 就無法在 dealloc 中銷毀,self 始終被強(qiáng)引用坠宴,永遠(yuǎn)得不到釋放洋魂,循環(huán)矛盾,最終造成內(nèi)存泄漏啄踊。

解決方式:

在合適的時機(jī)銷毀 NSTimer
當(dāng) NSTimer 初始化之后忧设,加入 runloop 會導(dǎo)致被當(dāng)前的頁面強(qiáng)引用,因此不會執(zhí)行 dealloc颠通。所以需要在合適的時機(jī)銷毀 _timer址晕,斷開 _timer、runloop 和當(dāng)前頁面之間的強(qiáng)引用關(guān)系顿锰。

使用 GCD 的定時器
GCD 不基于 runloop谨垃,可以用 GCD 的計時器代替 NSTimer 實(shí)現(xiàn)計時任務(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)生大量的臨時對象纷责,直至循環(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再膳,及時釋放占用內(nèi)存大的臨時變量,減少內(nèi)存占用峰值曲横。

for (int i = 0; i < 100000; i++) {
    @autoreleasepool {
        NSString *str = @"Abc";
        str = [str lowercaseString];
        str = [str stringByAppendingString:@"xyz"];
        NSLog(@"%@", str);
    }
}
野指針與僵尸對象

指針指向的對象已經(jīng)被釋放/回收喂柒,這個指針就叫做野指針。這個被釋放的對象就是僵尸對象禾嫉。

如果用野指針去訪問僵尸對象灾杰,或者說向野指針發(fā)送消息,會發(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 就會崩潰
    }
    return 0;
}

解決方案:當(dāng)對象釋放后讲竿,應(yīng)該將其置為 nil泥兰。

常用的內(nèi)存檢查工具
Instruments

Instruments 是 Xcode 自帶的工具集合,為開發(fā)者提供強(qiáng)大的程序性能分析和測試能力题禀。

它打開方式為:Xcode → Open Developer Tool → Instruments鞋诗。其中的 Allocations、Leaks 和 Zombies 功能可以協(xié)助我們進(jìn)行內(nèi)存泄漏檢查迈嘹。

? Leaks:動態(tài)檢查泄漏的內(nèi)存削彬,如果檢查過程時出現(xiàn)了紅色叉叉,就說明存在內(nèi)存泄漏秀仲,可以定位到泄漏的位置融痛,去解決問題。此外神僵,Xcode 中還提供靜態(tài)監(jiān)測方法 Analyze雁刷,可以直接通過 Product → Analyze 打開,如果出現(xiàn)泄漏保礼,會出現(xiàn)“藍(lán)色分支圖標(biāo)”提示沛励。

? Allocations:用來檢查內(nèi)存使用/分配情況。比如出現(xiàn)“循環(huán)加載引起內(nèi)存峰值”的情況炮障,就可以通過這個工具檢查出來目派。

? Zombies:檢查是否訪問了僵尸對象。

Instruments 的使用相對來說比較復(fù)雜胁赢,你也可以通過在工程中引入一些第三方框架進(jìn)行檢測企蹭。

MLeaksFinder

MLeaksFinder 是 WeRead 團(tuán)隊(duì)開源的 iOS 內(nèi)存泄漏檢測工具。
它的使用非常簡單智末,只要在工程引入框架练对,就可以在 App 運(yùn)行過程中監(jiān)測到內(nèi)存泄漏的對象并立即提醒。MLeaksFinder 也不具備侵入性吹害,使用時無需在 release 版本移除,因?yàn)樗粫?debug 版本生效虚青。
不過 MLeaksFinder 的只能定位到內(nèi)存泄漏的對象它呀,如果你想要檢查該對象是否存在循環(huán)引用。就結(jié)合 FBRetainCycleDetector 一起使用棒厘。

FBRetainCycleDetector

FBRetainCycleDetector 是 Facebook 開源的一個循環(huán)引用檢測工具纵穿。它會遞歸遍歷傳入內(nèi)存的 OC 對象的所有強(qiáng)引用的對象,檢測以該對象為根結(jié)點(diǎn)的強(qiáng)引用樹有沒有出現(xiàn)循環(huán)引用奢人。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谓媒,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子何乎,更是在濱河造成了極大的恐慌句惯,老刑警劉巖土辩,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異抢野,居然都是意外死亡拷淘,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門指孤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來启涯,“玉大人,你說我怎么就攤上這事恃轩〗嵬荩” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵叉跛,是天一觀的道長松忍。 經(jīng)常有香客問我,道長昧互,這世上最難降的妖魔是什么挽铁? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任坷备,我火速辦了婚禮鳄橘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘道偷。我一直安慰自己玖雁,他們只是感情好更扁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著赫冬,像睡著了一般浓镜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上劲厌,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天膛薛,我揣著相機(jī)與錄音,去河邊找鬼补鼻。 笑死哄啄,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的风范。 我是一名探鬼主播咨跌,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼硼婿!你這毒婦竟也來了锌半?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤寇漫,失蹤者是張志新(化名)和其女友劉穎刊殉,沒想到半個月后殉摔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡冗澈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年钦勘,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片亚亲。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡彻采,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出捌归,到底是詐尸還是另有隱情肛响,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布惜索,位于F島的核電站特笋,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏巾兆。R本人自食惡果不足惜猎物,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望角塑。 院中可真熱鬧蔫磨,春花似錦、人聲如沸圃伶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽窒朋。三九已至搀罢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間侥猩,已是汗流浹背榔至。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留欺劳,地道東北人洛退。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像杰标,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子彩匕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355