1. 程式人生 > >App 記憶體洩漏二三事

App 記憶體洩漏二三事

AFHTTP 記憶體洩漏問題

這是 AFHTTP 框架的通病。這個問題很常見,也最好解決,網上也有不少的解決方案。主流的解決方案就是使用單例。定義一個單例物件 SessionManager:

@interface SessionManager : NSObject
@property(nonatomic,strong)AFHTTPSessionManager *manager;
+(SessionManager *)share;
@end

@implementation SessionManager


+(SessionManager *)share{
    static SessionManager *shareObj = nil;
    static
dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ shareObj = [[SessionManager alloc] init]; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; /** 設定超時*/ [manager.requestSerializer willChangeValueForKey:@"timeoutInterval"]; manager.requestSerializer.timeoutInterval = 10
; [manager.requestSerializer didChangeValueForKey:@"timeoutInterval"]; shareObj.manager = manager; }); return shareObj; } @end

然後這樣使用它:

AFHTTPSessionManager* manager = [SessionManager share].manager;
manager.requestSerializer = [AFHTTPRequestSerializer new];
……

環信 UI 框架中的記憶體洩漏問題

環信框架中,有一個對 UIViewController 的擴充套件(Category) :UIViewController+HUD,它對 MBHUD 進行了二次封裝,通過它可以使你的 MBHUD 的呼叫變得更簡單,比如顯示一個 HUD 你可以這樣:

[self showHudInView:self.view hint:@""];

但是這個方法中有一個嚴重的記憶體洩漏問題。當你在一個 View Controller 中多次顯示 HUD 之後(比如反覆下拉重新整理表格),用檢視偵錯程式檢視 UIView,你會發現檢視樹中顯示了多個 HUD 物件。也就是說每次 showHudInView 之後都會重新生成一個新的 HUD,而原來的 HUD 雖然被隱藏了,但它們在記憶體中仍然是持續存在的。每次 showHudInView 呼叫大概會導致 400-500 k 的記憶體洩漏。如果你反覆重新整理表格(比如 5 分鐘或更長)直到記憶體撐爆,app 崩潰。

解決的方法很簡單,在 showHudInView 方法中加入一句:

HUD.removeFromSuperViewOnHide = YES;

這樣,被隱藏的 HUD 會自動從 subviews 中移除,不再佔用記憶體。

O-C 塊中對 self 強引用導致的記憶體洩漏問題

在 View Controller 類的 O-C 塊中,如果你直接引用了 self,則會導致 View Controller 被強引用(因為塊的引數都是以 copy 引用的,會導致 retained count 加 1)。這樣,當 View Controller 被 pop 出導航控制器棧後不會被釋放,導致記憶體洩漏。這個洩漏就比較嚴重了,少則幾百 K,多則幾兆。

一個比較明顯的例子就是 MJRefresh。在 View Controller 中,如果我們想支援下拉重新整理,通常會這樣使用 MJRefresh:

self.tableView.mj_header=[MJRefreshNormalHeader headerWithRefreshingBlock:^{
        [self.tableView.mj_header endRefreshing];
        currentPage = 1;
        [self loadNoticeList:1 success:^(NSArray<CampusNoticeModel *> *data) {
            [self.models removeAllObjects];
            [self.models addObjectsFromArray:data];
            [self.tableView reloadData];
        } failure:^(NSString *msg) {
        }];        
    }];

注意,O-C 塊中對 View Controller 進行了強引用,比如:self.tableView 和 self.models。

原則上,當我們在 O-C 塊中引用 self 時,應當使用弱引用,比如上面的程式碼應當改為:

__weak __typeof(self) weakSelf=self;
    self.tableView.mj_header=[MJRefreshNormalHeader headerWithRefreshingBlock:^{
        [weakSelf.tableView.mj_header endRefreshing];
        currentPage = 1;
        [weakSelf loadNoticeList:1 success:^(NSArray<CampusNoticeModel *> *data) {
            [weakSelf.models removeAllObjects];
            [weakSelf.models addObjectsFromArray:data];
            [weakSelf.tableView reloadData];
        } failure:^(NSString *msg) {
        }];
    }];

也就是將 O-C 塊中所有的 self 改成 weakSelf。這裡有一個例外,如果引用的是例項變數而不是屬性,原則上是不需要 weakSelf 的。比如 currentPage 在 View Controller 中是以例項變數形式定義的(也就是說沒有用 @property 進行宣告),那麼我們不需要通過 weakSelf 來進行引用。

但是,如果你在專案中使用 MLeaksFinder 來檢測記憶體洩漏時,MLeaksFinder 仍然會認為 O-C 塊中對 currentPage 的引用存在問題。因此,為了讓 MLeaksFinder 徹底“閉上嘴”,我們最好也將 currentPage 修改為屬性(使用 @property 宣告),然後將 O-C 塊中的引用方式修改為:weakSelf.currentPage。

在使用 CADisplayLink 時,如果不釋放 CADisplayLink,很容易出現記憶體洩漏。以自定義 UIView 為例,我們會使用定時器進行某些自定義的繪圖和動畫操作。這時我們會用到 CADisplayLink :

displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(animateDashboard:)];

當我們需要開啟定時器時,可以將它新增到 runloop:

[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

但是 displayLink 會持有 UIView 物件,導致 UIView 永遠不會被釋放。因此我們需要在一個適當的時機釋放 displayLink,比如在 CADisplayLink 的 action 方法中根據一定的條件來 invalidate 它:

-(void)animateDashboard:(CADisplayLink *)sender{
    if( endValue <= self.value){// 到達終點值,停止動畫
        ......
        [displayLink invalidate];
        ......
    }else{
        ......
    }
}

另外,CADisplayLink 最好不要複用。也就是說,每次啟動 CADisplayLink 時都重新初始化並將它新增到 runloop,而每次停止動畫時都 invalidate:

-(void)animating{
    if(_stopped == YES){
        displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(blink:)];
        [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

        _stopped = NO;
        [self setNeedsDisplay];
    }
}
-(void)stopAnimating{
    if(_stopped == NO){
        [displayLink invalidate];
        _stopped = YES;
    }
}

reloadRowsAtIndexPaths 導致的記憶體洩漏

UITableView 的 reloadRowsAtIndexPaths 的行為非常奇怪,在重新整理 cell 時,它並不會重用原有的 cell,而是重新建立新的 cell 覆蓋在原來的 cell 上,這會導致額外的記憶體開銷。當重複多次呼叫 reloadRowsAtIndexPaths 之後,你可以在檢視偵錯程式中看到這樣的效果:

無論你用不用 beginUpdates/endUpdates,結果都是一樣。
解決的辦法目前只有一個,不要用 reloadRowsAtIndexPaths,而是使用 reloadData,當然會有一點效能上的代價,但也是沒有辦法的事情。

定時器導致的記憶體洩漏問題

有時候 NSTimer (尤其是 repeated 為 YES 時)會導致記憶體洩漏問題。因為定時器是在另外一個執行緒中執行的,當介面消失後,定時器仍然還在執行,如果在定時器任務中引用了 UI 元素,則這些檢視都會被強引用,從而導致介面消失後 view 無法釋放,導致記憶體洩漏。
因此,如果在你的 UIViewController 中使用了定時器,一定要記得在 viewWillDisappear 方法中 invalidate 它。

addScriptMessageHandler 導致的記憶體洩漏

WKUserContentController 的 addScriptMessageHandler 方法會導致一個對 handler 物件的強引用,從而導致 handler (通常是 webView 所在的 ViewController)不會被釋放,於是記憶體洩漏。
解決的辦法是 removeScriptMessageHandlerForName。根據官方文件,當你 addScriptMessageHandler 之後,需要在不再需要 handler 時,需要呼叫 removeScriptMessageHandlerForName 解除 handler 的強引用。
問題在於,“當你不在需要它的時候”到底是什麼時候?我們一般會在 viewDidLoad 中 addScriptMessageHandler,按道理應該在 dealloc 中 removeScriptMessageHandlerForName。但由於記憶體都已經洩漏了,ViewContoller 的 dealloc 根本不會呼叫,這個方法是無效的。
解決的辦法有兩個,一個是將 addScriptMessageHandler 放到 viewDidAppear 中執行,那麼我們就可以在 viewDidDisappler 中 removeScriptMessageHandlerForName 了。
另一個方法是將 handler 弱引用。這需要新建一個類,建立一個弱引用的屬性,用這個屬性來包裝 handler 物件:

@interface WeakScriptMessageDelegate : NSObject<WKScriptMessageHandler>
// 1
@property (nonatomic, weak) id<WKScriptMessageHandler> scriptDelegate;

- (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate;

@end

@implementation WeakScriptMessageDelegate

- (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate
{
    self = [super init];
    if (self) {
        _scriptDelegate = scriptDelegate;
    }
    return self;
}
// 2
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message];
}

@end
  1. 這個屬性就是用來弱引用 handler 的屬性,它儲存了一個對 handler 的弱引用。型別是 id,因為 addScriptMessageHandler 方法需要一個 WKScriptMessageHandler 物件作為引數。
  2. 這個物件對 WKScriptMessageHandler 進行了封裝,它同樣實現了 WKScriptMessageHandler 協議,這個協議中有一個唯一的方法需要實現,即 userContentController 方法。在方法內部,我們可以直接呼叫 handler 的同名方法實現(因為二者的行為是一致的)。

然後這樣使用它:

// 將 handler 轉換成一個若引用的 handler,從而避免記憶體洩漏
WeakScriptMessageDelegate* weakHandler = [[WeakScriptMessageDelegate alloc] initWithDelegate:handler];

[webView.configuration.userContentController addScriptMessageHandler:weakHandler name:methodName];

這種辦法也可以用來解決許多強引用導致的記憶體洩漏。

MLeaksFinder

記憶體洩漏問題多種多樣,它們經常以出乎人意料的形式存在,我們無法以一種固定的模式來判斷 app 中存在的記憶體洩漏問題。我們常常需要使用多個工具和手段來檢查 app 中的記憶體問題,比如可以用 Xcode 的 Analyze 工具對程式碼進行靜態語法分析,用 Instrument 的 Leaks/Allocations 工具進行動態記憶體檢查分析,用檢視偵錯程式檢視 UI 問題等等。

但我們還可以用許多第三方記憶體洩漏檢測框架,比如:MLeaksFinder 和 HeapInspector-for-iOS,尤其是前者(後者目前會導致 App “凍死”的問題,作者還在解決這個問題)。

MLeaksFinder 是一個專門用於檢測 UI 類記憶體洩漏的工具,我們可以利用它來檢測 UIViewController 和 UIView 中未 dealloc 的 subview。

它的使用非常簡單,直接 pod MLeaksFinder,然後找到 MLeaksFinder.h 標頭檔案,將其中的 MEMORY_LEAKS_FINDER_ENABLED 巨集和 MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED 巨集開啟(設定為 1)就可以了。

編譯執行 app,測試各種操作,切換到不同的 view controller,當 MLeaksFinder 發現記憶體洩漏會彈出一個 alert(同時控制檯會有輸出),告訴你哪個類和 UIView 中存在記憶體洩漏(以及迴圈持有)。

相關推薦

App 記憶體洩漏

AFHTTP 記憶體洩漏問題 這是 AFHTTP 框架的通病。這個問題很常見,也最好解決,網上也有不少的解決方案。主流的解決方案就是使用單例。定義一個單例物件 SessionManager: @interface SessionManager : NSO

關於vs開發windows程式過程中記憶體檢查

做為一個C/C++程式設計師,面對資源管理是必不可少的。今天,我對我這些年的經驗的一些總結。 每一個程式在執行時都佔用一塊可用的記憶體空間,用於存放動態分配的物件,此記憶體空間稱為程式的自由儲存區或堆。 C 語言程式使用一對標準庫函式 malloc 和 free 在自由儲存區

Android:Handler )由記憶體洩漏所想到的(垃圾回收機制)

主要內容解決Handler記憶體洩漏以及延伸(垃圾回收、引用等)解決Handler記憶體洩漏及延伸為什麼Handler會引起記憶體洩漏?這是一段使用Handler的程式碼public class Lea

記憶體: Xcode 記憶體圖、Instruments 視覺化檢測迴圈引用

小結下,記憶體管理的語義: 需要該物件的時候,他就得在。不需要他的時候,他最好被釋放了。 合理的利用資源。 需要該物件的時候,他不在,釋放早了。 野指標問題,用殭屍物件除錯 給他發訊息,程式會崩,EXC_BAD_INSTRUCTION 不需要該物件的時候,他還在。記憶體可

Linux使用者程序記憶體分配及二級頁表PTE的

我們在用偵錯程式看Linux使用者程序程式碼時,發現了一件很有意思的事情,在一段記憶體空間中,有一整頁(4K)都是data abort,如下:第一頁4011c000資料正常... ...4011cfec [0xe28dd014]   add      r13,r13,#0x144011cff0 [0xe8bd

記憶體,part1

1.簡介 早期的計算機較現在更簡單。計算機系統的元件例如CPU,記憶體,硬碟,網路介面同時發展起來,結果就是他們在效能上很平衡。例如在資料傳輸速度上,記憶體網絡卡cpu相差不多。 但是,當計算機系統的整個結構確定後,每個子部件的發展速度就各不相同了。例如 C

後臺性能測試不可不知的

報告 空間 bold 返回 定義 需求 加鎖 交互 posit 某月黑風高之夜,某打車平臺上線了一大波(G+)優惠活動,眾人紛紛下單。於是乎,該打車平臺使用的智能提示服務扛不住直接趴窩了(如下圖)。事後,負責智能提示服務開發和運維的有關部門開會後決定:必須對智能提示服務進行

MySQL 死鎖與日誌

mysql索引 open 靜態變量 ... 硬盤 永久 state stack 應該 最近線上 MySQL 接連發生了幾起數據異常,都是在淩晨爆發,由於業務場景屬於典型的數據倉庫型應用,白天壓力較小無法復現。甚至有些異常還比較詭異,最後 root cause 分析頗費周

web項目從Myeclipse遷移到idea的

知識 ima 說著 art 項目結構 玩耍 錯誤 unit 學習  今天新接手了一個myeclipse項目,想把這個項目從myeclipse遷移到idea,花了點時間,也遇到一些新的問題,打算記錄下來。  這是myeclipse的項目結構      我整理一下      整

移動端】:移動端觸摸事件點透及多種解決方案。

優化 提前 sta 屬性 lis 剛才 觸摸事件 功能 觸發 大家都知道的少說,多分享一些幹貨。 一、首先說移動端的三大主要事件: 1.手指按下: ontouchstart2.手指移動:ontouchmove3.手指擡起 ontouchend *使用移動端事件時,為盡

移動端【四】:陀螺儀(重力感應器)實現手機位置、加速度感應以及常見應用。

效果 防止 size tro 通過 select 代碼 陀螺儀 prime 首先說明一下:陀螺儀感應需在真機環境下進行調試,PC端無效果。 1.獲取感應器 需在window上監聽devicemotion事件,再通過事件對象獲取accelerationIncludingG

【58沈劍架構系列】緩存架構設計細節

得到 數據 余額 優點 提高 得出 商品 命中率 計算 本文主要討論這麽幾個問題: (1)“緩存與數據庫”需求緣起 (2)“淘汰緩存”還是“更新緩存” (3)緩存和數據庫的操作時序 (4)緩存和

【Unity遊戲開發】AssetBundle雜記--AssetBundle的

比較 streaming 指定 但是 chunk 加載 公司 prefab 方法 一、簡介   馬三在公司大部分時間做的都是遊戲業務邏輯和編輯器工具等相關工作,因此對Unity AssetBundle這塊的知識點並不是很熟悉,自己也是有打算想了解並熟悉一下AssetBun

基層管理者項目管理

strong 結合 att 基本 基礎 不知道 data- 素質 早就   上一篇基層管理開篇中,闡述了關於基層管理的一些理解,包括重要性,方法論等。純理論的文字不免會讓讀者感到一絲絲苦澀,因而在這篇文章中我會結合一些實際案例來講解,基層技術管理者如何做好團隊和項目的管理。

部署node api的

更換 www docke 工程師 工程 issues 解決 分別是 res   當接到node開發node api的時候,我就想用docker來部署,眾所周知,node的版本更新叠代很快。很多以前需要babel後才能采用的方法正在不斷被node 原生的支持。如果隨便更換生產

消息隊列

qps ons 自定義 工作 設計模式的 ESS 監聽器 top 通過      最近在看kafka的代碼,就免不了想看看消息隊列的一些要點:服務質量(QOS)、性能、擴展性等等,下面一一探索這些概念,並談談在特定的消息隊列如kafka或者mosquito中是如何具體實

阿里資料庫十年變遷,那些你不知道的

第十個雙11即將來臨之際,阿里技術推出《十年牧碼記》系列,邀請參與歷年雙11備戰的核心技術大牛,一起回顧阿里技術的變遷。 今天,阿里資料庫事業部研究員張瑞,將為你講述雙11資料庫技術不為人知的故事。在零點交易數字一次次提升的背後,既是資料庫技術的一次次突破,也見證了阿里技術人永不言敗的精神,每一次化“不可能

Android的

1.什麼是 Activity? a. 四大元件之一,是使用者互動的介面。一般的來說一個使用者互動介面對應一個activity。 b.activity 是 Context 的子類,同時實現了 window.callback 和 keyevent.callback, 可以處理與窗體使用

關於Hasp SRM

以前說到對付hasp srm狗,大家想到的就是模擬,用的最多的就是MultiKey(http://testprotect.com/download) 這是俄國人寫的,不過到了V20.0.0就不再更新了,因為新的Hasp驅動出現--目前到了V7.32(http://www.safenet-inc.

PAT 童年生活 (遞推) 詳細題解

按理說這就是一道水題, 可我一開始竟然沒想出來要用遞推, 反而糾結在組合數學和搜尋上面了 水題就要多多找找規律, 把前幾個答案都列出來, 馬上就可以發現就是斐波那契數列了 //童年生活二三事 #include <cstdio> #include <iostream&g