iOS實錄15:淺談iOS Crash(二)

[這是第15篇]

導語:在當前的iOS開發中,雖然ARC為開發者解決了手動內存管理時代 的許多麻煩,但是內存方面的問題依然是產生iOS Crash的元兇之一,本文介紹內存方面,有關僵尸對象、野指針、內存泄漏、廢棄內存這四類問題的調試方法和代碼中的注意事項。

一、僵尸對象(Zombie Objects)

1、概述
  • 僵尸對象:已經被釋放掉的對象。一般來說,訪問已經釋放的對象或向它發消息會引起錯誤。因為指針指向的內存塊認為你無權訪問或它無法執行該消息,這時候內核會拋出一個異常( EXC ),表明你不能訪問該存儲區域(BAD ACCESS)。(EXC_BAD_ACCESS類型錯誤)

  • 調試解決該類問題一般采用NSZombieEnabled(開啟僵尸模式)。

2、使用NSZombieEnabled
  • Xcode提供的NSZombieEnabled,通過生成僵尸對象來替換dealloc的實現,當對象引用計數為0的時候,將需要dealloc的對象轉化為僵尸對象。如果之后再給這個僵尸對象發消息,則拋出異常。先選中Product -> Scheme -> Edit Scheme -> Diagnostics -> 勾選Zombie Objects 項,顯示如下:
設置NSZombieEnabled.png
  • 然后在Product -> Scheme -> Edit Scheme -> Arguments設置NSZombieEnabled、MallocStackLoggingNoCompact兩個變量,且值均為YES。顯示如下:
設置NSZombieEnabled和MallocStackLoggingNoCompact.png
  • 僅設置Zombie Objects的話,如果Crash發生在當前調用棧,系統可以把崩潰原因定位到具體代碼中;但是如果Crash不是發生在當前調用棧,系統僅僅告知崩潰地址,所以我們需要添加變量MallocStackLoggingNoCompact,讓Xcode記錄每個地址alloc的歷史,然后通過命令將地址還原出來。

  • Xcode 6之前還可以使用gdb,可以使用info malloc-history address命令來將發生崩潰的地址還原成具體的代碼行,Xcode 7之后只能使用lldb,使用命令bt來打印調用堆棧。下面是某Crash通過僵尸模式調試,使用bt查看的效果。

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,也可能沒有問題。下圖是 訪問野指針(指向已刪除對象的指針)可能發生的情況。

訪問野指針可能發生的情況圖.png
  • 從上圖可以知道,野指針造成的Crash的隨機性比較大,但是被隨機填入的數據是不可訪問的情況下,Crash是必現的。我們的思路是:想辦法給 野指針指向的內存填寫不可訪問的數據,讓隨機的Crash變成必現的Crash。
2、設置Malloc Scribble
  • Xcode提供的Malloc Scribble,可以將對象釋放后在內存上填上不可訪問的數據,將隨機發生變成不隨機發生的事情,選中Product->Scheme->Edit Scheme ->Diagnostics - >勾選 Malloc Scribble項,結果如下:
設置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

    - (UIImage *)clipImageWithRect:(CGRect)rect{
    
      CGFloat scale = self.scale;
      CGImageRef clipImageRef = CGImageCreateWithImageInRect(self.CGImage,
                                                        CGRectMake(rect.origin.x * scale,
                                                                   rect.origin.y  * scale,
                                                                   rect.size.width * scale,
                                                                   rect.size.height * scale));
    
      CGRect smallBounds = CGRectMake(0, 0, CGImageGetWidth(clipImageRef)/scale, CGImageGetHeight(clipImageRef)/scale);
      UIGraphicsBeginImageContextWithOptions(smallBounds.size, YES, scale);
      CGContextRef context = UIGraphicsGetCurrentContext();
    
      CGContextTranslateCTM(context, 0, smallBounds.size.height);
      CGContextScaleCTM(context, 1.0, -1.0);
      CGContextDrawImage(context, CGRectMake(0, 0, smallBounds.size.width, smallBounds.size.height), clipImageRef);
    
      UIImage* clipImage = UIGraphicsGetImageFromCurrentImageContext();
    
      UIGraphicsEndImageContext();
      CGImageRelease(clipImageRef);  //不添加,內存泄漏,會警告:Potential leak of an object stored into 'clipImageRef'
      return clipImage;
    }
    

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

    2)無效數據警告(Dead store)

    eg

    //錯誤做法,Analyzer分析后會告知:Value stored to ‘dataArray’ during its initialization is never read
    NSMutableArray *dataArray = [[NSMutableArray alloc] init];
    dataArray = _otherDataArray;
    
    //正確做法
    NSMutableArray *dataArray = nil;
    dataArray = _otherDataArray;
    

    分析: dataArray已經被初始化分配了內存,然后被另一個可變數組賦值,導致一個數據源卻申請了兩塊內存,造成了內存泄露。

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

    eg

    //錯誤做法,Analyzer分析后會告知:Property of mutable type ’NSMutableArray’ has ‘copy’ attribute,an immutable object will be stored instead
    @property (nonatomic, copy) NSMutableArray *dataArr;  
    
    //正確做法
    @property (nonatomic, strong) NSMutableArray *dataArr;  
    

    分析: 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(隱藏系統庫文件)。最后點擊紅色按鈕開始“錄制”,效果如下圖:
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。最后點擊紅色按鈕開始“錄制”,效果如下圖:
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下的界面展示。
Allocation界面Call Tree下顯示.png
Call Tree列名 說明
Bytes Used 已經使用的內存大小
Count 符號使用的總個數
Symbol Name 符號名稱

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

  • 間隔一段時間(如2分鐘)點擊“Mark Generation”,判斷幾次之間Generation之間的內存增長,而這些增長可能就是未能及時釋放的內存:根據內存占用的比例,找到占用比例最高的那部分,然后找到我們自己的代碼,再來分析并解決問題。
Allocation界面Mark Generation下顯示.png
3、代碼中的注意事項

略,與內存泄漏部分代碼中的注意事項相同。

End####

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Baymax:網易iOS App運行時Crash自動防護實踐 版權聲明本文轉自網易杭州前端技術部公眾號,由作者授權...
    IOS開發攻城獅_Fyc閱讀 6,818評論 2 34
  • 前言 iOS崩潰是讓iOS開發人員比較頭痛的事情,app崩潰了,說明代碼寫的有問題,這時如何快速定位到崩潰的地方很...
    齊滇大圣閱讀 65,459評論 29 443
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,447評論 25 708
  • 回想過去的一年,找工作真的是很艱難的事,運氣也是實力的一部分,一直這樣安慰自己,但到了真正的時候又會覺得自己怎么怎...
    記然閱讀 86評論 0 0
  • 我出生的那年,有一位年輕的詩人殞滅了,他們說,從此再也沒有詩。 什么是詩?可能每個中國人最早記誦的詩莫過于“鵝鵝鵝...
    冷瀟湘閱讀 300評論 7 1