iOS項(xiàng)目內(nèi)存泄漏探究-使用Runtime檢測內(nèi)存泄漏

平安喜樂

前言

iOS自從引入ARC機(jī)制后,一般的內(nèi)存管理就可以不用我們來負(fù)責(zé)了,但是一些操作如果不注意,還是會引起內(nèi)存泄漏。

造成內(nèi)存泄露的主要原因就是當(dāng)我們不需要引用這個(gè)對象的時(shí)候,這個(gè)對象的引用計(jì)數(shù)器不為0。

一 、APP內(nèi)存泄露排查:

背景: APP(直播類)上線后,測試使用wetest工具進(jìn)行檢測發(fā)現(xiàn):

  1. 在房間內(nèi)長時(shí)間進(jìn)行掛機(jī),iOS客戶端內(nèi)存會隨著時(shí)間的增加一直增長;
  2. 當(dāng)反復(fù)進(jìn)出房間后內(nèi)存也只是有一小部分進(jìn)行了回收釋放,還是有很大一部分內(nèi)存沒有釋放。

上面的現(xiàn)象會造成在一些低端機(jī)型上長時(shí)間觀看直播或者頻繁進(jìn)出房間客戶端閃退;高端機(jī)型上雖然不會閃退,也會有卡頓的現(xiàn)象出現(xiàn)。這嚴(yán)重影響了用戶體驗(yàn),需要盡快找出內(nèi)存泄露的點(diǎn)進(jìn)行修復(fù)。

從內(nèi)存泄露表現(xiàn)上進(jìn)行分析,兩種情況下可能的原因:

  • 內(nèi)存隨著時(shí)間一直增長:房間內(nèi)會隨著時(shí)間會一直變化的就是視頻流、送禮信息、聊天信息這三個(gè),第一時(shí)間考慮到應(yīng)該就是這三個(gè)中一 個(gè)或多個(gè)造成的內(nèi)存泄露。
  • 進(jìn)出房間后內(nèi)存沒有回到原始值:房間控制器roomViewController沒有釋放或者包含的一些子模塊沒有釋放,導(dǎo)致退出房間后對象沒有釋 放還在內(nèi)存中。

1)房間內(nèi)一直增長的問題

思路:先采用三種情況分別單一進(jìn)行測試,大概確認(rèn)是哪部分有內(nèi)存泄露,再使用工具分析是哪個(gè)具體點(diǎn)造成的。

既然房間內(nèi)視頻流、送禮、聊天都有可能引起這個(gè)現(xiàn)象,那么就用三個(gè)用例進(jìn)行測試:1、只加載視頻流,不送禮聊天 2、只送禮 3、只聊天 使用上面的用例分別在房間內(nèi)測試了一個(gè)小時(shí)以后得出結(jié)論:

  • 只加載視頻流、不送禮和聊天內(nèi)存基本沒有變化,也就是說視頻流沒有內(nèi)存泄露。
  • 只聊天一小時(shí)內(nèi)存增長80M左右。
  • 只送禮一小時(shí)內(nèi)存增長110M左右。

視頻流沒有內(nèi)存泄露,和我們剛開始猜想差不多。視頻流使用的其他的框架,應(yīng)該是比較完善的。

聊天和送禮都有內(nèi)存增長且送禮增長的較快,應(yīng)該是聊天區(qū)、送禮底版都有內(nèi)存泄露。送禮增加快猜測是因?yàn)樗投Y信息會同時(shí)展示聊天區(qū)和送
禮底版引起。

instrument

下面要具體排查出是哪些代碼造成了內(nèi)存泄露,再用上面的方法效率就很低了。這時(shí)候就要用到xcode自帶的性能檢測工具instrument。

注意:instrument在使用前需要把工程build settings中debug infomation Format設(shè)置為DWARF,不然不能根據(jù)內(nèi)存片段找到對應(yīng)代碼。

使用instrument的Allocation和Leaks進(jìn)行內(nèi)存分析后發(fā)現(xiàn):

  • 聊天區(qū)的圖片資源占用內(nèi)存一直在增加
  • 送禮底版每飄一次,展示送禮底版的GiftBottomView對象就會增加一個(gè)
1.1)聊天區(qū)的圖片資源占用

查看聊天區(qū)代碼,圖片引用的方式并不會造成內(nèi)存泄露。重寫相關(guān)類的deinit方法,發(fā)現(xiàn)當(dāng)對象從屏幕上移除的時(shí)候系統(tǒng)都進(jìn)行的回收。

這就奇怪了,該釋放的對象都進(jìn)行了釋放還為什么會出現(xiàn)這種問題呢?

考慮到是圖片占用內(nèi)存一直增加,把聊天區(qū)的圖片邏輯先注釋掉進(jìn)行測試。測試后發(fā)現(xiàn)不顯示圖片后內(nèi)存是比較平穩(wěn)。

聊天區(qū)圖片資源并沒有單獨(dú)引用,單條聊天信息對象ChatRecordCell釋放的時(shí)候內(nèi)部的圖片資源應(yīng)該是一起釋放的呀?

經(jīng)過本地的各種嘗試后發(fā)現(xiàn):

ChatRecordCell顯示聊天信息時(shí)使用的是富文本的形式。當(dāng)聊天區(qū)有圖片時(shí),圖片資源也是轉(zhuǎn)換成一個(gè)NSTextAttachment形式的富文本。

每次根據(jù)圖片資源生成一個(gè)NSTextAttachment對象后,內(nèi)存就會增長一個(gè)圖片的大小,同一張圖片也是。

NSTextAttachment每次會生成一個(gè)新的對象,因此沒有進(jìn)行復(fù)用之前圖片資源,在內(nèi)存緊張時(shí)圖片資源系統(tǒng)默認(rèn)是不進(jìn)行釋放的。

考慮到聊天區(qū)的圖片都是固定的一些等級圖片,當(dāng)對應(yīng)圖片的NSTextAttachment創(chuàng)建以后,根據(jù)圖片名稱作為key把NSTextAttachment對象存在 一個(gè)字典中,當(dāng)下次再需要顯示這個(gè)圖片資源時(shí)直接到字典中獲取上次存放的對象。

成效:加入NSTextAttachment緩存后使用機(jī)器人進(jìn)行聊天房間內(nèi)內(nèi)存相對平穩(wěn),一小時(shí)的內(nèi)存波動(dòng)基本在10M以內(nèi)。

1.2)送禮底版問題

重寫送禮相關(guān)類的deinit方法,發(fā)現(xiàn)當(dāng)對象移除的時(shí)候GiftBottomView的deinit方法沒走。說明GiftBottomView存在強(qiáng)引用導(dǎo)致移除屏幕后系
統(tǒng)沒有進(jìn)行內(nèi)存回收。

排查代碼發(fā)現(xiàn)是控制GiftBottomView中顯示時(shí)間、特效播放的timer沒有主動(dòng)銷毀導(dǎo)致。

因?yàn)镚iftBottomView中mTimer變量對timer對象進(jìn)行強(qiáng)引用,而timer在創(chuàng)建時(shí)添加target時(shí)對GiftBottomView也進(jìn)行了強(qiáng)引用。

導(dǎo)致了mTimer和GiftBottomView兩個(gè)對象之間互相進(jìn)行了強(qiáng)引用,造成了循環(huán)應(yīng)用。當(dāng)從屏幕中移除后mTimer和GiftBottomView引用計(jì)數(shù)器都 不為0,因此也都不會進(jìn)行內(nèi)存回收。

結(jié)論:當(dāng)一個(gè)類中包含有timer時(shí),在銷毀對象的時(shí)候需要手動(dòng)的調(diào)用timer的銷毀方法進(jìn)行銷毀,不然當(dāng)前類是不會進(jìn)行銷毀的。

成效:進(jìn)行對應(yīng)的修改后再次使用wetest進(jìn)行測試,在房間內(nèi)掛機(jī)一小時(shí)內(nèi)存增長在10M左右。

2)退出房間內(nèi)存沒有回到原始值

沒有回到原始值要考慮兩個(gè)方面:

  1. 系統(tǒng)緩存
  2. 最定義對象沒有釋放

系統(tǒng)緩存:當(dāng)圖片資源被第一次后,雖然有時(shí)候?qū)D片的引用已經(jīng)移除但系統(tǒng)不會立即進(jìn)行回收;在內(nèi)存不緊張的情況下會存在內(nèi)存中一段時(shí)間,如果再用到這個(gè)圖片資源后會直接從內(nèi)存中進(jìn)行獲取。

因?yàn)檫@塊并不是真正的泄露泄露,所以我們要把這部分進(jìn)行排除。

自定義對象沒有釋放:這部分是我們重點(diǎn)研究的部分。 再次使用instrument的Allocation和Leaks進(jìn)行內(nèi)存分析發(fā)現(xiàn):房間控制器roomViewController沒有釋放。

造成roomViewController的原因主要分為兩種:roomViewController內(nèi)有循環(huán)引用;roomViewController有對象沒有釋放都會造成 roomViewController不釋放。

當(dāng)退出房間后分析內(nèi)存片段發(fā)現(xiàn): PlayerListCollectionView在線列表、ContributionRankViewController貢獻(xiàn)榜等會導(dǎo)致roomViewController沒有進(jìn)行釋放。

在線列表排查代碼發(fā)現(xiàn)和GiftBottomView的原因是一樣的,定時(shí)刷新在線列表時(shí)起了一個(gè)定時(shí)器,在退出房間時(shí)沒有進(jìn)行手動(dòng)釋放造成了循環(huán) 引用。
ContributionRankViewController是因?yàn)閞oomViewController中有個(gè)變量對其進(jìn)行了強(qiáng)引用,ContributionRankViewController又對 roomViewController進(jìn)行了強(qiáng)應(yīng)用造成了循環(huán)引用。

解決:抽取退出房間方法leaveRoomAndClearUI,清除一些需要手動(dòng)進(jìn)行釋放的類。

成效:解決房間的內(nèi)存泄露后,退出roomViewController時(shí)調(diào)用了deinit方法進(jìn)行了控件的銷毀,當(dāng)非第一次進(jìn)出房間內(nèi)存變化在2M以內(nèi)。

3)思考

經(jīng)過上面的優(yōu)化,使用wetest進(jìn)行性能測試時(shí)沒有發(fā)現(xiàn)很明顯的內(nèi)存泄露。

后續(xù)每個(gè)月功能開發(fā)提測后都安排對應(yīng)的人進(jìn)行內(nèi)存檢測:第一發(fā)現(xiàn)新增功能是否有新增的內(nèi)存泄露,第二排查歷史功能是否有未發(fā)現(xiàn)的內(nèi)存泄露。

這樣每個(gè)月都需要對功能進(jìn)行內(nèi)存泄露檢測。通過instrument進(jìn)行運(yùn)行、截取片段、分析內(nèi)存、找到對應(yīng)代碼。

雖然這樣也可以找到內(nèi)存泄露的點(diǎn),但是第一影響較小的內(nèi)存泄露也不容易被發(fā)現(xiàn);第二工作效率非常低,每個(gè)版本都需要花費(fèi)不少的人力進(jìn)行分析。

實(shí)在受不了這種低效率、繁瑣的工作。那么有沒有一勞永逸或者高效的辦法呢?

二 、內(nèi)存泄露自動(dòng)檢測:

1)調(diào)研

上網(wǎng)查詢資料發(fā)現(xiàn),有很多人利用runtime的method swzzling技術(shù)可以一定程度的進(jìn)行自動(dòng)化的內(nèi)存檢測,開發(fā)過程中也可以實(shí)時(shí)的在進(jìn)行檢 測。

主要思路:

  • 通過method swzzlinghook的方式,監(jiān)聽整個(gè)類的創(chuàng)建與銷毀,在銷毀的時(shí)候進(jìn)行一定的處理,判斷是否進(jìn)行了釋放。
  • 使用method swzzlinghook采用無侵入的方式,解決常見的內(nèi)存泄露。

2)method swzzling

iOSRuntime:

Objective-C 是一個(gè)動(dòng)態(tài)語言,這意味著它不僅需要一個(gè)編譯器,也需要一個(gè)運(yùn)行時(shí)系統(tǒng)(runtime system)來動(dòng)態(tài)得創(chuàng)建類和對象、進(jìn)行消息 傳遞和轉(zhuǎn)發(fā)。

平時(shí)編寫的OC代碼,在程序運(yùn)行過程中,其實(shí)最終會轉(zhuǎn)換成Runtime的C語言代碼,Runtime是Object-C的幕后工作者。這就是 Objective-C Runtime 系統(tǒng)存在的意義,它是整個(gè)Objc運(yùn)行框架的一塊基石。

Objective-C對象收到消息之后,會調(diào)用objc_msdSend方法需要在運(yùn)行期才能解析出來。

發(fā)給某對象的全部消息都要有“動(dòng)態(tài)消息派發(fā)系統(tǒng)”來處理,該系統(tǒng)會查出對應(yīng)的方法,并執(zhí)行其代碼。

還有就是與給定的選擇子名稱相對應(yīng)的方法是可以在運(yùn)行期改變的。

可以采用此特性,發(fā)揮出巨大優(yōu)勢,因?yàn)槲覀儙撞恍枰创a,也不需要通過集成子類來覆寫方法就能改變這個(gè)類本身的功能。

這樣新功能將在類的所有實(shí)例中生效,而不僅限于覆寫了相應(yīng)方法的那些子類實(shí)例。此方案經(jīng)常稱為“方法調(diào)配”(method swzzling)

類的方法列表會把選擇子的名稱映射到相應(yīng)的方法實(shí)現(xiàn)之上,使得“動(dòng)態(tài)消息派發(fā)系統(tǒng)”能夠據(jù)此找到應(yīng)該調(diào)用的方法。這些方法均已函數(shù)指 針的形式來表示,這種指針叫做IMP。

從最常用的NSString類來解釋:NSString類可以相應(yīng)lowercaseString、uppercaseString、capitalizedString等選擇子。

下面映射表中的每個(gè)選擇子都映射到了不同的IMP上


image.png

Objective-C運(yùn)行期系統(tǒng)提供的方法能夠用來操作這張表。我們可以向其中新增選擇子,也可以改變某選擇子所對應(yīng)的方法實(shí)現(xiàn),還可以交換兩個(gè)選擇子所映射的指針。

上面圖可以經(jīng)過操作變成下圖的樣子


image.png

在上圖新的映射表中,多了一個(gè)名為newSelector的選擇子,lowercaseString、uppercaseString的實(shí)現(xiàn)則互換了。上面你的修改都無須編寫子類,只要修改了“方法表”的布局,就會反映到程序中所有的NSString的實(shí)例之上,這就是次特性的強(qiáng)大之處。

3)UIViewController自動(dòng)檢測的實(shí)現(xiàn)

先找一個(gè)開發(fā)中最常用的類UIViewController進(jìn)行實(shí)現(xiàn)。

UIViewController是一個(gè)界面的基礎(chǔ);一般情況下一個(gè)界面就是一個(gè)UIViewController;當(dāng)從一個(gè)界面返回到上一級界面時(shí),則這個(gè)界面應(yīng)該是被釋放的。

也就是當(dāng)一個(gè) UIViewController被dismiss 后,該 UIViewController 包括它的 view、view 的 subviews 等等將很快被釋放。

于是,我們只需在一個(gè) ViewController 被 dismiss 一小段時(shí)間后,看看該 UIViewController、它的 view、view 的 subviews 等等是否還存在,就可以判斷出當(dāng)前UIViewController有沒有被釋放,如果沒有被釋放那就很大可能是有內(nèi)存泄露。

具體的方法是:

為ViewController 添加一個(gè)方法 -willDealloc 方法。

該方法的作用是,先用一個(gè)弱指針指向 self,并延遲一小段時(shí)間(3秒)后,通過這個(gè)弱指針調(diào)用一個(gè)方法-alertNotDealloc,而 -alertNotDealloc 主要作用是提供程序當(dāng)前類有內(nèi)存泄露。

這樣,當(dāng)我們認(rèn)為某個(gè)對象應(yīng)該要被釋放了,在釋放前調(diào)用這個(gè)方法,如果3秒后它被釋放成功,weakSelf 就指向 nil,不會調(diào)用到 -alertNotDealloc 方法,也就沒有任何響應(yīng);

如果它沒被釋放(則泄露了)則-alertNotDealloc 就會被調(diào)用進(jìn)行彈框。

- (BOOL)willDealloc {
    // 弱引用的self
    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong id strongSelf = weakSelf;
        // 對當(dāng)前類進(jìn)行一個(gè)weak的引用,如果沒有其他引用的時(shí)候,會直接事nil
        [strongSelf alerttNotDealloc];
    });
    return YES;
}
- (void)alerttNotDealloc {
    NSString *className = NSStringFromClass([self class]);
    // 打印當(dāng)前類的名稱等信息
    NSLog(@"Possibly Memory Leak.\nIn case that %@ should not be dealloced, override -willDealloc in %@ by returning NO.", className, className);
}

什么時(shí)候去調(diào)用willdealloc方法呢?也就是一個(gè)ViewController什么時(shí)候應(yīng)該被銷毀:當(dāng)dismiss被調(diào)用的時(shí)候。

但是現(xiàn)有的接口沒有辦法在每個(gè)實(shí)例中dismiss被調(diào)用時(shí)同時(shí)調(diào)用willDealloc。

這時(shí)候就要用到runtime的“方法調(diào)配”技術(shù)對系統(tǒng)方法和自定義方法的實(shí)現(xiàn)進(jìn)行交換。

重寫ViewController的load方法(load方法只有當(dāng)前類在項(xiàng)目中第一次被加載時(shí)調(diào)用,也就是只調(diào)用一次且比實(shí)例中任何方法調(diào)用時(shí)機(jī)都早),在load方法中把自定的swizzled_dismiss方法和系統(tǒng)的實(shí)現(xiàn)IMP進(jìn)行交換。

在swizzled_dismiss方法中獲取到退出界面的viewController,調(diào)用willDealloc方法。

這樣如果3秒后alertNotDealloc方法被調(diào)用了則當(dāng)前viewController有內(nèi)存泄露,需要進(jìn)行進(jìn)一步分析具體泄露點(diǎn)。


// ViewController在load進(jìn)行方法交換
@implementation UIViewController (MemoryLeak)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleSEL:@selector(viewDidDisappear:) withSEL:@selector(swizzled_viewDidDisappear:)];
        [self swizzleSEL:@selector(viewWillAppear:) withSEL:@selector(swizzled_viewWillAppear:)];
        [self swizzleSEL:@selector(dismissViewControllerAnimated:completion:) withSEL:@selector(swizzled_dismissViewControllerAnimated:completion:)];
    });
}
- (void)swizzled_dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
    // 當(dāng)前控制器被移除堆棧,也就是控制器被銷毀了
    [self swizzled_dismissViewControllerAnimated:flag completion:completion];
    // 調(diào)用應(yīng)該被釋放的控制器willDealloc方法
    [self.presentedViewController willDealloc];
}
@end

4)常見類自動(dòng)檢測拓展

經(jīng)過上面的處理以后,UIViewControoler內(nèi)有內(nèi)存泄露時(shí)可以及時(shí)的進(jìn)行提醒。需要把自動(dòng)檢測的功能拓展到我們常用的一些類UINavigationController、UITouch等。

考慮到常用的類都是繼承自NSObject,給NSObjet添加一個(gè)分類增加willDealloc方法,這樣在UIViewControoler、UINavigationController等中都可以直接調(diào)用willDealloc。

新建需要拓展類的分類,在對應(yīng)分類的load方法中使用method swzzling重寫被移除方法(就是當(dāng)前對象應(yīng)該被釋放了)。
UIApplication:sendAction:to:from:forEvent:

UITouch-setView:
UINavigationController:popViewControllerAnimated:

在自定義的方法中調(diào)用移除對象的willDealloc方法,檢測當(dāng)前對象是否被正常進(jìn)行了釋放。

5)method swzzling的問題

ViewController

完成上面的實(shí)現(xiàn)以后確實(shí)可以自動(dòng)進(jìn)行ViewController的內(nèi)存檢測,但是當(dāng)ViewController被UINavigationController包裹以后時(shí)不生效。

發(fā)現(xiàn)被UINavigationController包裹以后ViewController被銷毀前不走dismiss方法,而是走UINavigationController的pop方法,因此也就沒有調(diào)用willDealloc。

這時(shí)候能不能通過監(jiān)聽ViewController視圖隱藏的時(shí)間進(jìn)行調(diào)用呢?但是視圖隱藏不代表是被銷毀,也可能是跳轉(zhuǎn)到了其他的界面。

這時(shí)候需要使用runtime的另外一個(gè)技術(shù)"關(guān)聯(lián)對象(AssociatedObject)",使用AssociatedObject動(dòng)態(tài)的給當(dāng)前ViewController添加一個(gè)屬性kPoppedKey:Bool。

再通過method swzzling監(jiān)聽ViewController的視圖出現(xiàn)willAppear、視圖隱藏willDisappear方法和UINavigationController的pop方法。

在willAppear把kPoppedKey設(shè)置為false,在UINavigationController的pop方法中把kPoppedKey設(shè)置為true。

在willDisappear中判斷kPoppedKey為true時(shí)則調(diào)用willDealloc方法進(jìn)行內(nèi)存檢測。

// ViewController在load進(jìn)行方法交換
const void *const kHasBeenPoppedKey = &kHasBeenPoppedKey;
@implementation UIViewController (MemoryLeak)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleSEL:@selector(viewDidDisappear:) withSEL:@selector(swizzled_viewDidDisappear:)];
        [self swizzleSEL:@selector(viewWillAppear:) withSEL:@selector(swizzled_viewWillAppear:)];
    });
}
- (void)swizzled_viewDidDisappear:(BOOL)animated {
    [self swizzled_viewDidDisappear:animated];
    // 當(dāng)前視圖被隱藏,有兩種情況:1、跳轉(zhuǎn)到其他控制器(此時(shí)不處理)2、被包裹的NAV控制器pop出了堆棧,此時(shí)也需要進(jìn)行銷毀處理
    if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
        [self willDealloc];
    }
}
- (void)swizzled_viewWillAppear:(BOOL)animated {
    [self swizzled_viewWillAppear:animated];
    // 設(shè)置關(guān)聯(lián)對象,判斷隱藏的時(shí)候是否是進(jìn)行銷毀,還是跳轉(zhuǎn)到其他控制器
    objc_setAssociatedObject(self, kHasBeenPoppedKey, @(NO), OBJC_ASSOCIATION_RETAIN);
}
@end
 
 
// UINavigationController在load進(jìn)行方法交換
@implementation UINavigationController (MemoryLeak)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleSEL:@selector(popViewControllerAnimated:) withSEL:@selector(swizzled_popViewControllerAnimated:)];
    });
}
- (UIViewController *)swizzled_popViewControllerAnimated:(BOOL)animated {
    UIViewController *poppedViewController = [self swizzled_popViewControllerAnimated:animated];
    if (!poppedViewController) {
        return nil;
    }
    extern const void *const kHasBeenPoppedKey;
    // 設(shè)置關(guān)聯(lián)對象,當(dāng)前控制器要被銷毀了
    objc_setAssociatedObject(poppedViewController, kHasBeenPoppedKey, @(YES), OBJC_ASSOCIATION_RETAIN);
    return poppedViewController;
}
@end

通過上面的操作可以動(dòng)態(tài)監(jiān)聽UIViewController的創(chuàng)建和銷毀,當(dāng)應(yīng)該被銷毀時(shí)如果延遲3秒后對象還沒有被銷毀,則通過彈框通知程序,最好可以把堆棧信息進(jìn)行打印。

通過類似的操作給UIView、UITouch、UITabBarController、UIPageViewController等添加監(jiān)聽,這樣就可以把一些常用的類都進(jìn)行內(nèi)存監(jiān)聽,可以在開發(fā)過程中發(fā)現(xiàn)問題

單例、全局變量

項(xiàng)目中有些類是全局變量或者是以單例的形式存在的,這時(shí)候開發(fā)過程中只要使用到這樣類時(shí)就會進(jìn)行提示,有點(diǎn)影響正常功能開發(fā)了。

這時(shí)候就需要增加一個(gè)白名單機(jī)制,當(dāng)發(fā)現(xiàn)當(dāng)前對象是白名單中的時(shí)候就不進(jìn)行下一步的內(nèi)存泄露檢測。

使用一個(gè)全局變量NoCheckMemoryArray記錄不需要進(jìn)行檢測的一些類,在willDealloc中進(jìn)行判斷是否在NoCheckMemoryArray,當(dāng)不在NoCheckMemoryArray再進(jìn)行內(nèi)存檢測。

6)method swzzling動(dòng)態(tài)檢測優(yōu)勢

使用method swzzling的方法可以在實(shí)時(shí)的進(jìn)行檢測,開發(fā)過程中如果代碼有問題會直接進(jìn)行報(bào)告,這樣可以在提測前就把內(nèi)存泄露的風(fēng)險(xiǎn)降到最低。

method swzzling動(dòng)態(tài)檢測內(nèi)存泄露的好處:

  • 使用簡單,不侵入業(yè)務(wù)邏輯代碼,因?yàn)槊總€(gè)類在第一次加載的時(shí)候直接就把對應(yīng)的方法動(dòng)態(tài)添加成功了,而且可以設(shè)置只在debug環(huán)境下才進(jìn)行添加
  • 不需要額外的操作,只需開發(fā)自己的業(yè)務(wù)邏輯,在運(yùn)行調(diào)試時(shí)就能幫你檢測
  • 內(nèi)存泄露發(fā)現(xiàn)及時(shí),更改完代碼后一運(yùn)行即能發(fā)現(xiàn)(這點(diǎn)很重要,可以馬上就能意識到哪里寫錯(cuò)了)
  • 精準(zhǔn),能準(zhǔn)確地告訴你哪個(gè)對象沒被釋放
  • 可以添加白名單機(jī)制,有些類可能是單例或者就是會有引用,這時(shí)候可以通過設(shè)置白名單進(jìn)行屏蔽

三 、APP內(nèi)存泄露示例:

通過instrument和method swzzling動(dòng)態(tài)檢測發(fā)現(xiàn)了項(xiàng)目中的一些內(nèi)存泄露。

按照產(chǎn)生的原因大概有一下幾種類型:

1)ViewController中存在NSTimer


image.png

2)ViewController中的代理delegate


image.png

3)ViewController中Block


image.png

4)CoreFoundation對象沒有釋放


image.png

5)WKWebview設(shè)置配置沒有移除


image.png

6)當(dāng)前控制器強(qiáng)引用目標(biāo)控制器,退出目標(biāo)控制器時(shí),目標(biāo)控制器沒有銷毀

7)當(dāng)前控制器內(nèi)view強(qiáng)引用當(dāng)前控制器,在移除view時(shí)要把引用當(dāng)前控制器這個(gè)引用消除

四、思考與總結(jié):

雖然現(xiàn)在iOS使用了ARC的進(jìn)行內(nèi)存管理,但是如果不注意的情況還是會引起內(nèi)存泄露造成嚴(yán)重后果。

內(nèi)存泄露不止會影響用戶體驗(yàn),個(gè)別情況下還會影響功能的正常使用。在平時(shí)開發(fā)中要熟記引起內(nèi)存泄露的常見形式,開發(fā)時(shí)直接進(jìn)行避免。

ARC模式下內(nèi)存泄露的情況只有上面列舉的一些情況,主要大家寫代碼的時(shí)候都注意點(diǎn)一般情況下不會出現(xiàn)內(nèi)存泄露的。

Runtime技術(shù)提供的便利是iOS中其他方式無法給與的。檢測檢測內(nèi)存泄露只是其中的一個(gè)應(yīng)用,具體的其他應(yīng)用還有很多,這里暫時(shí)就不進(jìn)行列舉了。

檢測內(nèi)存泄露使用Runtime進(jìn)行檢測,相交與系統(tǒng)提供的的instrument進(jìn)行檢測效率提升了很多。

instrument需要每次版本的時(shí)候把本地修改、新增的功能都進(jìn)行一定時(shí)間的檢測,然后根據(jù)檢測的結(jié)果分析、整理是否有內(nèi)存泄露。發(fā)現(xiàn)有內(nèi)存泄露后還需要具體的堆棧信息,分析多個(gè)時(shí)間點(diǎn)內(nèi)存中對象的數(shù)量、對象銷毀是否正常,然后在找到對應(yīng)有問題的代碼進(jìn)行修改再進(jìn)行檢測。這種辦法和上面提供的自動(dòng)檢測內(nèi)存的辦法比較耗時(shí)耗力,還可能存在遺漏的地方。

還有最重要的一點(diǎn)在runtime實(shí)際應(yīng)用中大部分都是采用無侵入的方式進(jìn)行的,不會和業(yè)務(wù)代碼有任何的耦合更不會影響業(yè)務(wù)代碼的執(zhí)行。

五、參考:

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

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

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