淺談iOS Crash(2)

發表於2017-07-14

一、殭屍物件(Zombie Objects)

1、概述
  • 殭屍物件:已經被釋放掉的物件。一般來說,訪問已經釋放的物件或向它發訊息會引起錯誤。因為指標指向的記憶體塊認為你無權訪問或它無法執行該訊息,這時候核心會丟擲一個異常( EXC ),表明你不能訪問該儲存區域(BAD ACCESS)。(EXC_BAD_ACCESS型別錯誤)
  • 除錯解決該類問題一般採用NSZombieEnabled(開啟殭屍模式)。
2、使用NSZombieEnabled
  • Xcode提供的NSZombieEnabled,通過生成殭屍物件來替換dealloc的實現,當物件引用計數為0的時候,將需要dealloc的物件轉化為殭屍物件。如果之後再給這個殭屍物件發訊息,則丟擲異常。先選中Product -> Scheme -> Edit Scheme -> Diagnostics -> 勾選Zombie Objects 項,顯示如下:
201701-14251b2f3765e441
設定NSZombieEnabled.png
  • 然後在Product -> Scheme -> Edit Scheme -> Arguments設定NSZombieEnabled、MallocStackLoggingNoCompact兩個變數,且值均為YES。顯示如下:
201701-14251b2f3765e441
設定NSZombieEnabled和MallocStackLoggingNoCompact.png
  • 僅設定Zombie Objects的話,如果Crash發生在當前呼叫棧,系統可以把崩潰原因定位到具體程式碼中;但是如果Crash不是發生在當前呼叫棧,系統僅僅告知崩潰地址,所以我們需要新增變數MallocStackLoggingNoCompact,讓Xcode記錄每個地址alloc的歷史,然後通過命令將地址還原出來。
  • Xcode 6之前還可以使用gdb,可以使用info malloc-history address命令來將發生崩潰的地址還原成具體的程式碼行,Xcode 7之後只能使用lldb,使用命令bt來列印呼叫堆疊。下面是某Crash通過殭屍模式除錯,使用bt檢視的效果。
201701-14251b2f3765e441
bt效果.png

說明:發版前要將殭屍物件檢測這些設定都去掉,否則每次通過指標訪問物件時,都去檢查指標指向的物件是否為殭屍物件,這就影響效率了。

3、程式碼中的注意事項

在ARC時代,避免訪問釋放掉的記憶體,程式碼需要注意的地方有:

  • 檢查程式碼1 :不能使用assgin或 unsafe_unretained修飾指向OC物件的指標

    assgin和unsafe_unretained表示不持物件,是弱引用。如果指標指向的物件被釋放了,它們就變成了野指標,很有可能發生Crash。

    建議1: assign僅用於修飾NSInteger等OC基礎型別,以及short、int、double、結構體等C資料型別,不修飾物件指標;

    建議2: OC物件屬性一般使用strong關鍵字(預設)修飾。

    建議3: 如果需要弱引用OC物件,建議使用weak關鍵字,因為被weak指標所引用的物件被回收後,weak指標會被賦為nil(空指標),給nil發任何訊息都不會出問題。使用weak修飾代理物件屬性就是很好的例子

  • 檢查程式碼2 :Core Foundation等底層操作

    Core Foundation等底層操作它們不支援ARC,還需要手動記憶體管理。

    建議: 注意CF物件的建立和釋放。

    二、野指標(Wild pointer)

1、概述
  • 野指標是指向一個已刪除的物件 或 未申請訪問受限記憶體區域的指標。而這裡的野指標主要是物件釋放後,指標未置空導致的野指標。該類Crash發生比較隨機,找出來比較費勁,比較常見的做法是,在開發階段,提高這類Crash的復現率,儘可能得將其發現並解決。
  • 向OC物件發出release訊息,只是標記物件佔用的那塊記憶體可以被釋放,系統並沒有立即收回記憶體;如果此時還向該物件傳送其他訊息,可能會發生Crash,也可能沒有問題。下圖是 訪問野指標(指向已刪除物件的指標)可能發生的情況。
201701-14251b2f3765e441
訪問野指標可能發生的情況圖.png
  • 從上圖可以知道,野指標造成的Crash的隨機性比較大,但是被隨機填入的資料是不可訪問的情況下,Crash是必現的。我們的思路是:想辦法給 野指標指向的記憶體填寫不可訪問的資料,讓隨機的Crash變成必現的Crash。
2、設定Malloc Scribble
  • Xcode提供的Malloc Scribble,可以將物件釋放後在記憶體上填上不可訪問的資料,將隨機發生變成不隨機發生的事情,選中Product->Scheme->Edit Scheme ->Diagnostics – >勾選 Malloc Scribble項,結果如下:
201701-14251b2f3765e441
設定Malloc Scribble.png
  • 設定了Enable Scribble,在物件申請記憶體後在申請的記憶體上填0xaa,記憶體釋放後在釋放的記憶體上填0x55;如果記憶體未被初始化就被訪問,或者釋放後被訪問,Crash必現。

說明:該方法必須連線Xcode執行程式碼才發現,不適合測試人員使用。可以基於fishhook ,選擇hook物件釋放的介面(C的free函式),達到和設定Enable Scribble一樣的效果。詳情參考如何定位Obj-C野指標隨機Crash(一):先提高野指標Crash率如何定位Obj-C野指標隨機Crash(二):讓非必現Crash變成必現如何定位Obj-C野指標隨機Crash(三):加點黑科技讓Crash自報家門

3、程式碼中的注意事項

檢查使用assgin或 unsafe_unretained 修飾指向OC物件的指標 和 Core Foundation等底層操作。

三、記憶體洩漏(Memory Leak)

1、概述
  • 記憶體洩漏是指沒有釋放掉不再引用物件的記憶體。即便ARC幫我們解決很多麻煩,但是記憶體洩漏問題依然比較多;一般開發結束後,都要做一些基本的記憶體洩漏排查工作。
  • 記憶體洩漏排查,一般採用Analyzer(靜態分析) + Leaks + MLeaksFinder (第三方工具)
2-1、排查之靜態分析(Analyzer)
  • Xcode提供的 Analyzer可以在程式沒執行的時候,通過分析程式碼上下文的語法結構和記憶體情況,找出程式碼中潛在錯誤,如記憶體洩露、未使用函式和變數等。選中Product->Analyze(快捷鍵command+shift+B)可以使用了。
  • Analyzer主要分析四種問題:

    1) 邏輯錯誤:訪問空指標或未初始化的變數等;
    2) 記憶體管理錯誤:如記憶體洩漏等;Core Foundation不支援ARC
    3) 宣告錯誤:從未使用過的變數;
    4) API呼叫錯誤:未包含使用的庫和框架。

  • Analyzer執行後,常見的警告型別有:

    1)記憶體警告(Memory)

    eg

    分析:Analyzer檢查出來記憶體洩漏,比較常見的就是CG、CF開頭的記憶體洩漏,記憶體申請,忘記釋放了。還有一種是,C申請的記憶體,沒有配對使用new delete, malloc free。

    2)無效資料警告(Dead store)

    eg

    分析: dataArray已經被初始化分配了記憶體,然後被另一個可變陣列賦值,導致一個資料來源卻申請了兩塊記憶體,造成了記憶體洩露。

    3)邏輯錯誤監測(Logic error)

    eg

    分析: NSMutableArray是可變資料型別,應該用strong來修飾其物件。

說明: Analyzer由於是編譯器根據程式碼進行的判斷, 做出的判斷不一定會準確, 因此如果遇到提示, 應該去結合程式碼上文檢查一下;還有某些造成記憶體洩漏的迴圈引用通過Analyzer分析不出來。

2-2、排查之記憶體洩漏工具(Leaks)
  • Xcode提供的Leak可以幫助發現執行著的程式記憶體洩漏的地方。通過選中Product-> Profile(快捷鍵command+i,喚起Instrument工具介面) -> Leaks。切換到Call Tree模式,底部選中Separate by Thread(按執行緒分開做分析)、Invert Call Tree(反向輸出呼叫樹)、Hide System Libraries(隱藏系統庫檔案)。最後點選紅色按鈕開始“錄製”,效果如下圖:
201701-14251b2f3765e441
Leaks除錯介面.png
  • Leaks除錯介面上,1是Allocations 模板,顯示記憶體分配情況;2是 Leaks 模板,這裡可以檢視記憶體洩露情況。如果紅X出現, 表示有記憶體洩露;主框體區域則會顯示洩露的物件。Call Tree選項介紹如下:
CALL TREE 中選項 說明
Separate by Category 按型別分類,展開All Heap Allocations這一套顯示的就是不同方法裡堆記憶體的分配情況
Separate by Thread 按執行緒分開做分析,這樣更容易揪出那些吃資源的問題執行緒。特別是對於主執行緒,它要處理和渲染所有的介面資料,一旦受到阻塞,程式必然卡頓或停止響應。
Invert Call Tree 反向輸出呼叫樹。把呼叫層級最深的方法顯示在最上面,更容易找到最耗時的操作。
Hide System Libraries 隱藏系統庫檔案。過濾掉各種系統呼叫,只顯示自己的程式碼呼叫。
Flattern Recursion 拼合遞迴。將同一遞迴函式產生的多條堆疊(因為遞迴函式會呼叫自己)合併為一條
2-3、排查之MLeaksFinder(強烈推薦)

MLeaksFinder是微信閱讀團隊為了簡化記憶體洩漏排查工作,推出的第三方工具,也是我們當前專案中記憶體洩漏的工具之一。

  • 特點:整合簡單,主要檢查UI方面(UIView 和 UIViewController)的洩漏。
  • 原理:不入侵開發程式碼,通過hook 掉 UIViewController 和 UINavigationController 的 pop 跟 dismiss 方法,檢查ViewController物件被 pop 或 dismiss 一小段時間後,看看該ViewController物件的 view,view 的 subviews 等等是否還存在。
  • 實現:為基類 NSObject 新增一個方法 -willDealloc 方法,利用weak指標指向自己,並在一小段時間(3秒)後,再次檢測該weak指標是否有效,有效則記憶體洩漏。
  • 整合:通過Cocoapods引入或直接把程式碼拖進專案,很方便。發生記憶體洩漏,會彈出警告框,提示發生記憶體洩漏的位置。

說明:詳細內容請參考:MLeaksFinder:精準 iOS 記憶體洩露檢測工具MLeaksFinder 新特性

3、程式碼中的注意事項(ARC下的迴圈引用是記憶體洩漏的主要原因)
  • 檢查程式碼1 :Core Foundation、Core Graphics等操作

    Core Foundation、CoreGraphics等操作不支援ARC,還需要手動記憶體管理。

    建議: 注意CF、CG物件的建立和釋放。

  • 檢查程式碼2 :NSTimer/CADisplayLink的使用,因為NSTimer/CADisplayLink物件的target會強引用self,而self又強引用NSTimer/CADisplayLink物件。

    建議:使用擴充套件方法,使用blocktarget弱引用目標物件 打破保留環,具體實現參考iOS實錄8:解決NSTimer/CADisplayLink的迴圈引用

  • 檢查程式碼3 :block使用程式碼。

    建議:成對使用weakSelf和strongSelf來打破block迴圈引用(對於self沒有引用的block是不會造成迴圈引用,不需要使用weakSelf和strongSelf)

    原理:在block外定義弱引用(weakSelf),指向的self物件;在block內捕獲的是這個弱引用(weakSelf),保證了self不會被block所持有;在執行block內方法時,生成強引用(strongSelf),指向了弱引用(weakSelf)所指向的物件(self物件);在block內部實際是持有了self物件,但是這個強引用(strongSelf) 的生命週期只在這個block執行的過程中,block執行執行完立刻就被釋放了。

    四、廢棄記憶體(Abandoned Memory)

1、概述
  • 廢棄記憶體(Abandoned Memory)指,依然被引用物件的記憶體,但在程式邏輯中無法再被利用。
  • 排查該類問題建議使用Xcode提供的AllocationAllocation可以跟蹤應用的記憶體分配情況。
2、使用Allocation
  • Xcode提供的Allocation由於可以跟蹤應用的記憶體分配情況。開發者反覆操作App,檢視記憶體基線變化;甚至還可以設定Mark Generation來對比多次Generation之間的記憶體增長,這部分的增長就是我們沒有及時釋放的記憶體。通過Product-> Profile(快捷鍵command+i,喚起Instrument工具介面) -> Allocations。最後點選紅色按鈕開始“錄製”,效果如下圖:
201701-14251b2f3765e441
Allocation介面Statistics Detail 下顯示.png
  • 上圖是Statistics Detail Type下的介面展示,下面是一些名稱的說明
DETAIL列名 說明
Graph 型別的選擇項
Category 型別,或CF物件,或OC物件,或原始塊的記憶體
Persistent Bytes 未釋放的記憶體和大小
Persistent 未釋放的物件個數
Transient 已經釋放的物件個數
Total Bytes 總使用記憶體大小
Total 總使用物件個數
Transient / Total Bytes 已釋放記憶體大小/總使用記憶體大小
ALLOCATION TYPE 說明
All Heap & Anonymous 所有堆記憶體和其他記憶體
All Heap Allocations 所有堆記憶體
All Anonymous VM 所有其他記憶體
  • 下圖是切換到Call Tree下的介面展示。
201701-14251b2f3765e441
Allocation介面Call Tree下顯示.png
CALL TREE列名 說明
Bytes Used 已經使用的記憶體大小
Count 符號使用的總個數
Symbol Name 符號名稱

說明:這些名詞的具體解釋見Instrument-Allocations

  • 間隔一段時間(如2分鐘)點選“Mark Generation”,判斷幾次之間Generation之間的記憶體增長,而這些增長可能就是未能及時釋放的記憶體:根據記憶體佔用的比例,找到佔用比例最高的那部分,然後找到我們自己的程式碼,再來分析並解決問題。
201701-14251b2f3765e441
Allocation介面Mark Generation下顯示.png
3、程式碼中的注意事項

略,與記憶體洩漏部分程式碼中的注意事項相同。

End

相關文章