深入iOS系統(tǒng)底層之crash解決方法

眾里尋他千百度,驀然回首,那人卻在燈火闌珊處。--《青玉案·元夕》

要學(xué)會看crash崩潰和報告

一個應(yīng)用程序并不總會一直運行的很好,它總會有出現(xiàn)crash崩潰的情況。如果在應(yīng)用程序中接入了一些第三方的crash收集工具或者自建crash收集報告平臺的話將會很好的幫助開發(fā)者去分析和解決應(yīng)用程序在線上運行的問題,當(dāng)出現(xiàn)的崩潰問題能得到及時的解決和快速的修復(fù)時必將會大大的提升應(yīng)用程序的用戶體驗。

當(dāng)前比較流行的crash收集分析工具很多都是基于開源的KSCrash代碼來進(jìn)行封裝和改進(jìn)的。蘋果自身也構(gòu)建了一套crash采集和分析的機制,你可以從真機的聯(lián)機日志或者從開發(fā)者賬號中去查看對應(yīng)的crash信息。網(wǎng)絡(luò)上也有很多關(guān)于crash分析的文章,以及crash堆棧符號化處理的文章。這里假定你已經(jīng)了解了一些查看crash報告的方法和技巧以及一些簡單的crash分析技巧,因為這些是作為開發(fā)者需要具備的技能之一。

一個objc_msgSend+16崩潰棧

應(yīng)用程序出現(xiàn)的crash崩潰異常有一些能夠簡單的被分析和解決,往往這些crash崩潰異常都會帶有明確的上下文信息和函數(shù)調(diào)用層級堆棧。但并不是所有的crash崩潰異常都能被簡單的解決,尤其是那些沒有明確上下文信息的函數(shù)調(diào)用堆棧或者那些調(diào)用堆棧中沒有一個函數(shù)或者方法能夠被直接定位到源代碼的場景,就如下面這個崩潰的函數(shù)調(diào)用棧(部分信息):

Incident Identifier: 85BE3461-D7FD-4043-A4B9-1C0D9A33F63D
CrashReporter Key:   9ec5a1d3b8d5190024476c7068faa58d8db0371f
Hardware Model:      iPhone7,2
Code Type:       ARM-64
Parent Process:  ? [1]
Date/Time:       2018-08-06 16:36:58.000 +0800
OS Version:      iOS 10.3.3 (14G60)
Report Version:  104

Exception Type:  EXC_BAD_ACCESS (SIGBUS)
Exception Codes: 0x00000000 at 0x00000005710bbeb8
Crashed Thread:  2

Thread 2 name:  WebThread
 Thread 2 Crashed:
0   libobjc.A.dylib                 objc_msgSend + 16
1   UIKit                           -[UIWebDocumentView _updateSubviewCaches] + 40
2   UIKit                           -[UIWebDocumentView subviews] + 92
3   UIKit                           -[UIView(CALayerDelegate) _wantsReapplicationOfAutoLayoutWithLayoutDirtyOnEntry:] + 72
4   UIKit                           -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1256
5   QuartzCore                      -[CALayer layoutSublayers] + 148
6   QuartzCore                      CA::Layer::layout_if_needed(CA::Transaction*) + 292
7   QuartzCore                      CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 32
8   QuartzCore                      CA::Context::commit_transaction(CA::Transaction*) + 252
9   QuartzCore                      CA::Transaction::commit() + 504
10  QuartzCore                      CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*) + 120
11  CoreFoundation                  __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32
12  CoreFoundation                  __CFRunLoopDoObservers + 372
13  CoreFoundation                  CFRunLoopRunSpecific + 456
14  WebCore                         RunWebThread(void*) + 456
15  libsystem_pthread.dylib         _pthread_body + 240
16  libsystem_pthread.dylib         _pthread_body + 0

Thread 2 crashed with ARM-64 Thread State:
  cpsr: 0x0000000020000000     fp: 0x000000016e18d7c0     lr: 0x000000018e2765fc     pc: 0x0000000186990150 
    sp: 0x000000016e18d7b0     x0: 0x0000000174859740     x1: 0x000000018eb89b7b    x10: 0x0000000102ffc000 
   x11: 0x00000198000003ff    x12: 0x0000000102ffc290    x13: 0xbadd8a65710bbead    x14: 0x0000000000000000 
   x15: 0x000000018caeb48c    x16: 0x00000005710bbea8    x17: 0x000000018e2765d4    x18: 0x0000000000000000 
   x19: 0x0000000103a52800     x2: 0x0000000000000000    x20: 0x00000000000002a0    x21: 0x0000000000000000 
   x22: 0x0000000000000000    x23: 0x0000000000000000    x24: 0x0000000000000098    x25: 0x0000000000000000 
   x26: 0x000000018ebade52    x27: 0x00000001ad018624    x28: 0x0000000000000000    x29: 0x000000016e18d7c0 
    x3: 0x000000017463db60     x4: 0x0000000000000000     x5: 0x0000000000000000     x6: 0x0000000000000000 
    x7: 0x0000000000000000     x8: 0x00000001acfb9000     x9: 0x000000018ebf8829 

Binary Images:
       0x100030000 -        0x1022cbfff +xxxx arm64  <6b98f446542b3de5818256a8f2dc9ebf> /var/containers/Bundle/Application/441619EF-BD56-4738-B6CF-854492CDFAC9/xxxx.app/xxxx
       0x1063f8000 -        0x106507fff  MacinTalk arm64  <0890ce05452130bb9af06c0a04633cbb> /System/Library/TTSPlugins/MacinTalk.speechbundle/MacinTalk
       0x107000000 -        0x1072e3fff  TTSSpeechBundle arm64  <d583808dd4b9361b99a911b40688ffd0> /System/Library/TTSPlugins/TTSSpeechBundle.speechbundle/TTSSpeechBundle
...
       0x18e03d000 -        0x18ede3fff  UIKit arm64  <314063bdf85f321d88d6e24a0de464a2> /System/Library/Frameworks/UIKit.framework/UIKit
       0x18ede4000 -        0x18ee0cfff  CoreBluetooth arm64  <ced176702d7c37e6a9027eeb3fbf7f66> /System/Library/Frameworks/CoreBluetooth.framework/CoreBluetooth

這是一個在iOS10.3.3版本的64位設(shè)備上的一條crash異常報告的片段信息,要記住這些信息,它對定位crash崩潰異常有很大的幫助。從崩潰的函數(shù)調(diào)用棧中可以看出異常是出現(xiàn)在最頂層的函數(shù)調(diào)用objc_msgSend+16處,也就是在objc_msgSend函數(shù)的第5條指令處(通常情況下arm體系結(jié)構(gòu)中每條指令占用4個字節(jié),上述的信息表明是崩潰在函數(shù)的第16個字節(jié)的偏移地址處,也就是函數(shù)的第5條指令處)。崩潰異常類型顯示為EXC_BAD_ACCESS表明是產(chǎn)生了無效的地址的讀寫訪問,整個崩潰函數(shù)調(diào)用棧中沒應(yīng)用程序中的任何上下文信息。objc_msgSend函數(shù)是runtime方法執(zhí)行的核心引擎而且調(diào)用如此的頻繁,函數(shù)內(nèi)部是不可能有BUG的。 那么為什么會崩潰在這呢?

當(dāng)異常出現(xiàn)在沒有源代碼的函數(shù)內(nèi)部時,唯一的方法就是去看它內(nèi)部的“源代碼”實現(xiàn)

既然出現(xiàn)問題是在objc_msgSend函數(shù)的第5條指令處,可以來看看這個函數(shù)實現(xiàn)的匯編代碼指令開頭片段:

;iOS10以后的objc_msgSend的部分實現(xiàn)代碼。
_objc_msgSend:
00000001800bc140<+0>    cmp     x0, #0x0     ;判斷對象receiver和0進(jìn)行比較
00000001800bc144<+4>    b.le    0x1800bc1ac    ;如果對象指針為0或者高位為1則執(zhí)行特殊處理跳轉(zhuǎn)。
00000001800bc148<+8>    ldr     x13, [x0]           ;取出對象的isa指針賦值給x13
00000001800bc14c<+12>   and x16, x13, #0xffffffff8   ;得到對象的Class對象指針賦值給x16
00000001800bc150<+16>   ldp x10, x11, [x16, #0x10]    ;取出Class對象的cache成員分別保存到x10,x11寄存器中 
-----------------------------------------------------------上面的指令就是代碼崩潰處。
00000001800bc154<+20>   and     w12, w1, w11

無論是真機還是模擬器,XCODE都支持在運行時來查看任何調(diào)用的函數(shù)的匯編代碼實現(xiàn),你可以通過設(shè)置符號斷點或者進(jìn)入?yún)R編調(diào)試模式以及單指令跳轉(zhuǎn)的方式來查看函數(shù)的匯編代碼實現(xiàn)。

從代碼中可以看出是在讀取對象的Class對象指針的數(shù)據(jù)成員cache時出現(xiàn)了無效的地址訪問異常。但是對象的Class對象這部分定義數(shù)據(jù)是存儲在進(jìn)程內(nèi)存的數(shù)據(jù)區(qū)段中,并且伴隨著整個應(yīng)用的生命周期而存在,是不可能被釋放和銷毀的,因此正常情況下是不可能存在非法內(nèi)存地址訪問異常的。會出現(xiàn)這種問題的原因就是調(diào)用方法的OC對象被銷毀了,再說具體一點就是對一個已經(jīng)被釋放掉的OC對象繼續(xù)調(diào)用了實例方法而導(dǎo)致的。因此當(dāng)出現(xiàn)這種類型的崩潰時,不管是否有明確上下文,其原因都是一致的。下面這張圖就能很清楚的說明其中的原因了:

對象被銷毀前后內(nèi)存布局對比圖

實際上在arm64位系統(tǒng)中isa中保存的并不是對象的Class對象地址,上面的圖目的是為了更加直觀的顯示問題原因。

一個OC對象obj在被銷毀前,其中的isa指針會指向正確的Class對象所在的內(nèi)存地址。因此調(diào)用objc_msgSend方法將會正常的運行,而一旦obj對象被銷毀后,為其分配的堆內(nèi)存將被回收用作其他用途,因此有可能這部分內(nèi)存區(qū)域的數(shù)據(jù)會被覆寫。當(dāng)對一個已經(jīng)釋放了的OC對象繼續(xù)調(diào)用實例方法時,在objc_msgSend函數(shù)內(nèi)部讀取到obj的isa指針得到的將是一個未知或者有可能無效的指針值。所以當(dāng)對這個未知地址指向的內(nèi)存進(jìn)行訪問時就出現(xiàn)了上面的EXC_BAD_ACCESS的異常崩潰了。

CPU指令中操作寄存器和常數(shù)的指令一般不會產(chǎn)生崩潰異常,比如上面的第1,2,4,6條指令;而一般產(chǎn)生訪問異常的指令是發(fā)生在那些訪問內(nèi)存地址的指令當(dāng)中,比如第3條和5條。

也許你會好奇既然obj對象已經(jīng)被釋放了,為什么崩潰會出現(xiàn)在objc_msgSend函數(shù)的第5條指令,其中的第3條指令是訪問對象的isa數(shù)據(jù)的,為什么不崩潰在這呢? 其實答案很簡單,因為幾乎所有的OC對象都是從堆內(nèi)存區(qū)域中分配內(nèi)存的,所以當(dāng)某個OC對象被銷毀后,其所占用的內(nèi)存仍然會放回堆內(nèi)存區(qū)域中進(jìn)行管理,而堆內(nèi)存區(qū)域的地址是可以進(jìn)行任意的讀寫訪問的,所以即使對象被銷毀釋放,仍然是可以訪問對象所指向的內(nèi)存區(qū)域的數(shù)據(jù)的。

應(yīng)用程序出現(xiàn)崩潰異常時除了函數(shù)調(diào)用棧可提供分析參考外,還可以從寄存器中的值來進(jìn)行一步分析。根據(jù)上述的函數(shù)指令實現(xiàn)中可以看出:

x0 寄存器中的保存的就是那個被銷毀了的對象指針。
x1 寄存器中保存的就是產(chǎn)生崩潰的對象的方法名稱的地址。
x13 寄存器中保存的就是對象的isa指針值。
x16 寄存器中保存的就是對象的Class指針對象。

函數(shù)崩潰處指令為:

ldp x10, x11, [x16, #0x10] 

這時候因為x16中其實保存的是一個非法的Class對象指針地址了,所以當(dāng)執(zhí)行l(wèi)dp指令來從x16所指向地址的偏移0x10處讀取內(nèi)存數(shù)據(jù)時就產(chǎn)生了崩潰,而崩潰的異常代碼:

  Exception Codes: 0x00000000 at 0x00000005710bbeb8

中的地址值也剛好和x16寄存器中的值是一致的。也就是表明x16中所保存的Class對象指針就是一個非法和無效的內(nèi)存地址。

在所有的OC方法中如果你設(shè)置了符號斷點那么在方法開始執(zhí)行時x0中保存的總是執(zhí)行方法的對象,也是第一個方法的參數(shù);x1中總是保存的執(zhí)行的方法的名稱字符串,也是第二個方法的參數(shù);然后x2到x15有可能依次是方法的其他參數(shù)。因此通常情況下你可以在調(diào)試控制臺中輸入: po $x0 來顯示對象信息, p (char*)$x1 來顯示方法名稱。 具體的詳細(xì)介紹可以參考我的另外一篇文章:寄存器介紹

上面的崩潰調(diào)用棧中,所有的函數(shù)和方法都是系統(tǒng)函數(shù)并沒有程序自身的源代碼,因此很難跟蹤或者發(fā)現(xiàn)問題產(chǎn)生的原因,因為此時是無法知道是哪個類的對象執(zhí)行方法調(diào)用而產(chǎn)生的crash了,唯一的線索就是x1寄存器中的值了。這個寄存器中的值保存的是調(diào)用的方法名, 它是一個SEL類型的數(shù)據(jù),因此可以根據(jù)x1中保存的方法名來進(jìn)行反推,也就是從方法名來反推出產(chǎn)生崩潰的對象的類名。

x1寄存器中保存的方法的內(nèi)存地址是存在于某個加載的庫Image的代碼段中,因此可以在崩潰日志的Binary Images列表中找到定義方法名的庫Image信息,Binary Images列表中的每個庫Image都有這個庫加載的開始和結(jié)束地址以及路徑名稱,可以很容易就從這些區(qū)間列表中找到x1寄存器所指的方法名到底屬于哪個庫。就上面的例子來說可以很明確的看到方法地址0x18eb89b7b是屬于:

 0x18e03d000 -  0x18ede3fff  UIKit arm64  <314063bdf85f321d88d6e24a0de464a2> /System/Library/Frameworks/UIKit.framework/UIKit

也就是UIKit庫中定義的某個對象在執(zhí)行x1所指的方法而產(chǎn)生了崩潰。有了這個更進(jìn)一步的信息后就可以在源代碼中進(jìn)行檢查看看哪部分代碼調(diào)用到了產(chǎn)生崩潰的庫中所定義的對象了(當(dāng)然UIKit這里不具備代表性,實際中崩潰時方法名也許會在其他的庫中)。這樣就從一定程度上能夠縮小排查問題的范圍。

常見的崩潰異常分析定位方法

當(dāng)出現(xiàn)了沒有上下文的崩潰異常調(diào)用棧時,并不是對它束手無策。除了可以根據(jù)異常類型(signal的類型)分析外,還可以借助搜索引擎以及一些常見的問題解答站點來尋找答案,當(dāng)然還可以借助下面列出幾種定位和分析的方法:

1.開源代碼法

這個方法其實很簡單,蘋果其實開源了非常多的基礎(chǔ)庫的源代碼,因此當(dāng)程序崩潰在這些開源的基礎(chǔ)庫上時就可以去下載對應(yīng)的基礎(chǔ)庫的源代碼進(jìn)行閱讀。然后從源代碼上進(jìn)行問題的分析,從而找到產(chǎn)生異常崩潰的原因。你可以從https://opensource.apple.com處去下載開源的最新的源代碼。這種方法的缺點是并不是所有的代碼都是開源的,而且開源的代碼并不一定是你真機設(shè)備上運行的iOS版本。因此這種方法只能是一種輔助方法。

2.方法符號斷點法

采用這種方法時,確保你手頭上要有一臺和產(chǎn)生崩潰異常問題的操作系統(tǒng)版本相同的真機設(shè)備,以方便聯(lián)機調(diào)試和運行。你可以在崩潰異常報告的:

OS Version:      iOS 10.3.3 (14G60)

部分看到產(chǎn)生異常的操作系統(tǒng)版本號,就如本文的例子里面產(chǎn)生異常的操作系統(tǒng)版本號為iOS 10.3.3。因為相同的操作系統(tǒng)版本號中所有庫中代碼實現(xiàn)的都是一樣的。如果實在沒有對應(yīng)的版本號的設(shè)備則可以試圖找一臺版本號最相近的設(shè)備。明確了操作系統(tǒng)版本和真機設(shè)備后再從代碼倉庫中檢出和你線上相同版本的應(yīng)用程序的源代碼(假如崩潰調(diào)用棧中沒有任何我們編寫的函數(shù)代碼則這個條件要求不必那么嚴(yán)格)。并打開項目工程,然后為產(chǎn)生崩潰的函數(shù)調(diào)用棧的棧頂函數(shù)或者方法名添加一個符號斷點。如果你不知道如何添加符號斷點請參考文章:https://blog.csdn.net/xuhen/article/details/77747456, 或者查找關(guān)鍵字:“XCODE 符號斷點"。

設(shè)置符號斷點的方法或者函數(shù)名時可以有如下的選擇:

  1. 如果產(chǎn)生崩潰的棧頂是一個OC對象的方法則可以直接用這個類名和方法名來設(shè)置符號斷點。
  2. 如果產(chǎn)生崩潰的棧頂是一個通用的C函數(shù)比如objc_msgSend、free、objc_release則考慮用函數(shù)調(diào)用棧的第二層函數(shù)和方法名來設(shè)置符號斷點。比如文本例子中的-[UIWebDocumentView _updateSubviewCaches]方法。
  3. 如果產(chǎn)生崩潰的函數(shù)調(diào)用棧頂是一個沒有對外暴露的C函數(shù),因為這種函數(shù)設(shè)置符號斷點的難度比交大,所以往往考慮采用函數(shù)調(diào)用棧的第二層函數(shù)或者方法名來做為符號斷點。

設(shè)置符號斷點的目的是為了在崩潰函數(shù)調(diào)用堆棧重現(xiàn)時,能在運行時的斷點處進(jìn)行動態(tài)分析。當(dāng)你設(shè)置了符號斷點后,如果程序邏輯運行到這個函數(shù)或者方法時,系統(tǒng)就會在設(shè)置的方法或者函數(shù)的第一條指令處停止下來。這時候就可以查看此時的函數(shù)調(diào)用棧是否和產(chǎn)生崩潰時的調(diào)用棧相符,如果相符合那么表明能夠重現(xiàn)可能發(fā)生問題的邏輯了,如果斷點處的調(diào)用棧和產(chǎn)生崩潰的調(diào)用棧不相同,則可能需要讓程序繼續(xù)運行,以便下次在同樣斷點處時進(jìn)行調(diào)用棧的比較,因為設(shè)置斷點的方法名并不一定只在一處被調(diào)用。

符號斷點的設(shè)置

當(dāng)程序停在了設(shè)置符號斷點的函數(shù)或者方法的開始地址后,接下來就需要在這個方法內(nèi)進(jìn)行第二個斷點的設(shè)置,設(shè)置的地方就是崩潰函數(shù)調(diào)用棧中函數(shù)調(diào)用上層函數(shù)的偏移處,這個可以在崩潰的報告中看到:

0   libobjc.A.dylib                 objc_msgSend + 16
1   UIKit                          -[UIWebDocumentView _updateSubviewCaches] + 40

也就是需要在_updateSubviewCaches函數(shù)的第11條指令或者函數(shù)的第40個偏移字節(jié)附近處添加一個斷點。這樣當(dāng)程序運動到斷點處時就可以在函數(shù)調(diào)用上層函數(shù)前查看各寄存器的值從而進(jìn)行問題的定位和分析。

運行到產(chǎn)生崩潰異常的指令

一般情況下崩潰函數(shù)棧報告中除棧頂函數(shù)外的每一層函數(shù)名后 + 的數(shù)字表明是在當(dāng)前函數(shù)的對應(yīng)的地址偏移處附近進(jìn)行了上層函數(shù)的調(diào)用,也就是對應(yīng)的地址偏移附近一般都會存在一條bl指令或者blr這兩條指令,這兩條指令的作用就是執(zhí)行函數(shù)的調(diào)用。

通過二次斷點的設(shè)置,程序運行到斷點時的指令是:

0x18c0248fc <+36>: bl     0x1893042dc   ;0x1893042dc 這個地址就是objc_msgSend的函數(shù)地址

本例子的異常崩潰的原因是對一個已經(jīng)釋放的對象繼續(xù)調(diào)用方法而產(chǎn)生的崩潰。所以當(dāng)斷點停在指令處時,我們可以在右下角的lldb控制臺中打印指令:

(lldb)po $x0
<__NSArrayM 0x1c044c2a0>(
<UIWebOverflowScrollView: 0x1281d7e00; frame = (0 0; 375 603); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x1c0851190>; layer = <WebLayer: 0x1c4426ba0>; contentOffset: {0, 0}; contentSize: {375, 12810}; adjustedContentInset: {0, 0, 0, 0}>
)

(lldb) p (char*)$x1
(char *) $6 = 0x000000018cb9dd70 "release"
(lldb) 

可以看出x0是一個數(shù)組對象,而x1中則是release方法。這樣就進(jìn)一步明確了是對一個已經(jīng)釋放了的數(shù)組對象調(diào)用了release方法而導(dǎo)致異常崩潰了。至于x0是一個什么數(shù)組以及保存在哪里,則可以通過匯編指令中的x0寄存器的使用進(jìn)行回溯往上查找指令來進(jìn)一步分析了。其實這個問題如果進(jìn)一步觀察就可以看出:崩潰的線程并不是出現(xiàn)在主線程,而是在一個工作線程中。而視圖的操作基本都應(yīng)該放在主線程進(jìn)行,因此當(dāng)主線程的某些子視圖數(shù)組對象被釋放后,這里又在輔助線程中進(jìn)行讀取訪問,就出現(xiàn)了上面的異常崩潰問題了。

在函數(shù)調(diào)用bl或者blr指令處設(shè)置斷點后,因為根據(jù)ABI規(guī)則所有非浮點數(shù)的參數(shù)分別依次保存在x0,x1,....這些寄存器中。所以可以在斷點處分別打印出這些寄存器的值就可以知道函數(shù)調(diào)用前所傳遞的參數(shù)值了。這個方法非常有助于進(jìn)行問題的定位和分析。

3.手動重現(xiàn)法

有時候即使你設(shè)置了符號斷點,場景依然無法重現(xiàn),這時候就需要采用一些特殊的手段,那就是手動的執(zhí)行方法調(diào)用。實現(xiàn)方式很簡單就是在某個演示代碼中人為的進(jìn)行崩潰棧頂函數(shù)的調(diào)用。就比如上面的例子當(dāng)[UIWebDocumentView _updateSubviewCaches]方法一直不被執(zhí)行時,就可以自己手動的去創(chuàng)建一個UIWebDocumentView對象,并手動的調(diào)用對應(yīng)的方法_updateSubviewCaches即可。這里存在的兩個問題是有可能這個類并沒有對外進(jìn)行聲明,或者我們并不知道方法的參數(shù)類型或者需要傳遞的值。對于第一個問題解決的方法可以采用NSClassFromString來得到類信息并進(jìn)行對象創(chuàng)建。而第二個問題則可以借助一些工具比如class-dump或者一些其他的手段來確認(rèn)方法的參數(shù)個數(shù)和參數(shù)類型。總之,目的就是為了能夠進(jìn)入函數(shù)的斷點,甚至都可以在不知道如何傳遞參數(shù)時將所有的參數(shù)都傳值為0或者nil來臨時解決問題。下面就是模擬崩潰函數(shù)的調(diào)用實現(xiàn)代碼:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    //因為類名和方法名都未對外公開,我們可以借助一些技術(shù)手段來讓某個特定的方法執(zhí)行,目的是為了能夠進(jìn)入到方法的內(nèi)部實現(xiàn)。
    Class cls = NSClassFromString(@"UIWebDocumentView");
    id obj = [[cls alloc] init];
    SEL sel = sel_registerName("_updateSubviewCaches");
    [obj performSelector:sel];

   //...
}

測試代碼可以寫在任何一個地方,這里為了方便就在程序啟動處加上測試代碼。等代碼編寫完畢后,就可以為方法設(shè)置符號斷點。這樣當(dāng)程序一運行時就一定能夠進(jìn)入到這個函數(shù)的內(nèi)部去。一旦函數(shù)被執(zhí)行后出現(xiàn)了斷點,就可以按照第2種方法中的介紹進(jìn)行崩潰分析了。

其實第3種方法的原則就是只要能讓產(chǎn)生崩潰異常的方法被調(diào)用,這其中可以嘗試著采用各種手段將對象和方法run起來。

4.第三方工具靜態(tài)分析法

前面兩種介紹的都是動態(tài)分析法, 有時候還可以借助一些反編譯的工具來對程序代碼進(jìn)行靜態(tài)分析。比如像Hopper或者IDA之類的工具。缺點就是這些工具是收費的,而且效果沒有動態(tài)分析那么的好。在使用上個人覺得IDA分析工具更加友好和強大一些。

采用第三方工具時需要找到產(chǎn)生崩潰的函數(shù)所在的庫,函數(shù)所在的庫在崩潰的函數(shù)調(diào)用棧列表中就能找到了。如果崩潰函數(shù)是在應(yīng)用程序本身中被定義,那么需要將上傳到appstore的ipa文件解壓縮并提取出其中的可執(zhí)行程序用工具打開即可。如果崩潰函數(shù)是在某個系統(tǒng)庫中被定義,那么可從如下的路徑:

~/Library/Developer/Xcode/iOS DeviceSupport/

iOS DeviceSupport這個文件夾下的內(nèi)容將展示你所有曾經(jīng)聯(lián)機調(diào)試過的各種操作系統(tǒng)版本的庫的一份拷貝,如果你沒有真機調(diào)試過出現(xiàn)崩潰的操作系統(tǒng)版本,請找一個安裝了這個操作系統(tǒng)版本的真機設(shè)備,并聯(lián)機,這樣你的文件夾中就會有對應(yīng)的操作系統(tǒng)版本下的系統(tǒng)庫的拷貝信息了。

UIKit庫的路徑

中找到對應(yīng)的產(chǎn)生崩潰的手機操作系統(tǒng)版本號的庫文件:10.3.3(14G60)/Symbols/System/Library/Frameworks/UIKit.framework/UIKit

當(dāng)用IDA工具打開對應(yīng)的庫文件或者可執(zhí)行文件時你看到的將是這個庫文件的所有匯編形式的代碼和數(shù)據(jù)。因此你可以通過搜索菜單來查找產(chǎn)生崩潰的函數(shù)或者方法名。這時候你就可以進(jìn)一步對產(chǎn)生問題的函數(shù)的匯編代碼進(jìn)行分析了。采用IDA工具進(jìn)行匯編代碼分析的缺點是靜態(tài)分析無法看到運行時的各個寄存器的真實的值,因此采用這種方法可能更需要考慮你對匯編代碼的理解能力。下面就是本文例子中的[UIWebDocumentView _updateSubviewCaches]方法的實現(xiàn)匯編代碼:

IDA工具查看_updateSubviewCaches的實現(xiàn)

采用IDA工具進(jìn)行分析時,需要了解一些比如庫基地址和代碼數(shù)據(jù)偏移地址以及地址重定向相關(guān)的知識。蘋果系統(tǒng)為安全對每個庫的加載都采用了ASLR的方式,也就是庫所加載的基地址每次運行時都是隨機的,這樣當(dāng)某次崩潰發(fā)生時需要將產(chǎn)生崩潰時的地址轉(zhuǎn)化為我們通過IDA工具打開的地址。 轉(zhuǎn)換公式為:

   轉(zhuǎn)換后的地址 = 崩潰時寄存器中保存的原始地址值 -  崩潰時地址所在的庫的基地址值 + 工具打開庫時所設(shè)定的基地址。

就以上面崩潰異常為例,當(dāng)我們用IDA工具看看x1寄存器中的值到底是一個什么方法名,那么只需要把x1的值(0x018eb89b7b),減去其所在的庫UIKit的基地址值(0x18e03d000),在加上IDA工具打開庫時的基地址(要想看基地址則滾動到IDA視圖的最開始部分,本次打開的基地址為:0x187769000)。所以x1寄存器中的地址值被轉(zhuǎn)化后應(yīng)該為:

0x018eb89b7b -  0x18e03d000 + 0x187769000 = 0x1882B5B7B

在IDA工具中將地址跳轉(zhuǎn)到0x1882B5B7B就可以看到本例子中產(chǎn)生崩潰的方法名是叫release:

導(dǎo)致崩潰異常的方法名

當(dāng)然IDA工具是可以手動進(jìn)行基地址的自定義設(shè)置的,這樣就不需要進(jìn)行計算以便和線上崩潰的基地址對齊。

如果你手頭上沒有第三方工具,其實系統(tǒng)內(nèi)置的otools工具也可以幫我們進(jìn)行問題的定位以及匯編代碼的查看和分析了,具體的方法大家就去查找相關(guān)的對otools使用的教程即可,這里就不展開了。

總結(jié)

上面列出的所有分析方法中有靜態(tài)分析的也有動態(tài)分析。當(dāng)出現(xiàn)了崩潰時除了從崩潰函數(shù)調(diào)用棧去分析問題,還可以從寄存器,以及加載的鏡像列表,以及崩潰棧頂部的函數(shù)的匯編代碼等等進(jìn)行綜合的分析和判斷。當(dāng)然即使這樣也不能保證所有問題就一定能夠得到解決,本文中列舉的例子只是在實際中的一種非常常見的崩潰異常,希望通過這個示例來起到一個拋磚引玉的效果,畢竟不同的崩潰異常的差異是比較大的。遇到問題需要具體分析,走進(jìn)函數(shù)的內(nèi)部實現(xiàn)就一定能夠找到產(chǎn)生問題的根源。

??【返回目錄


歡迎大家訪問我的github地址簡書地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內(nèi)容

  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 29,478評論 8 265
  • 組件 計算機是一種數(shù)據(jù)處理設(shè)備,它由CPU和內(nèi)存以及外部設(shè)備組成。CPU負(fù)責(zé)數(shù)據(jù)處理,內(nèi)存負(fù)責(zé)存儲,外部設(shè)備負(fù)責(zé)數(shù)...
    哆啦灬少A夢閱讀 1,605評論 1 2
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,121評論 1 32
  • 楔子: 那些年,被你牽著的手仿佛已經(jīng)成為習(xí)慣。 “要高考了,怎么辦……”南宮芷在前面頭也不回的問葉語。 “說好了要...
    0愿你0閱讀 241評論 0 0
  • 今晚,爸爸媽媽都早早上了床。爸爸日志寫完了,想和媽媽聊會兒天,媽媽把爸爸拉回了群里,但過了一會兒,便又躲了起來,似...
    畫念風(fēng)閱讀 305評論 0 0