如果你有過幾款正式線上 App 的開發經歷,你一定也曾被一些奇奇怪怪的 crash 困擾過,他們在你的友盟、Bugly、Crashlystics 上陰魂不散,頻率不高但偶有出現,仔細檢查崩潰棧卻發現他們有的崩在了一些莫名其妙的,你認為不可能會崩潰的位置;有的崩潰棧根本找不到對應的行號在哪裡,更別說 debug 了。這時,我們需要一些更給力一點的武器 —— crash log。
通過 crash log,iOS 開發者能獲取到的信息其實遠超乎大多數人的想像。本文不打算過多地介紹那些眾所周知的 crash log 中的內容,在繼續讀下去之前我默認你已經知道什麼是 stack traces,什麼是符號化(symbolicate)等術語,也有用過 crash log 解決過諸如數組越界等一些比較淺顯的問題。如果你對 LLDB 不夠熟悉,是個只知道 p 和 po 的 LLDB 初級選手,閱讀本文前強烈推薦你閱讀 objc.io 的這篇文章(https://objccn.io/issue-19-2/)。除了蘋果每隔幾年都會少許調整一下獲取 crash log 的方式外,其餘的 crash log 相關技術基本沒有什麼變化。
從哪裡獲取 Crash Log
巧婦難為無米之炊,我們想要用 crash log 解決問題,首先得拿到 crash log。
一般來說沒有特別大的必要獲取模擬器的日誌,因為絕大多數使用模擬器的情況都是從 Xcode 裡直接 run 的,崩潰現場都有要啥 crash log 呢。然後天有不測風雲,如果哪天真的你直接從模擬器打開 App,而且出現了 crash,日誌還是找得回來的。用 Finder 打開 ~/Library/Logs/DiagnosticReports 你能看到以 進程名+日期+時間戳+設備名命.crash 命名的文件,這些就是 crash 日誌了。需要注意的是,crash 日誌路徑和系統還有 Xcode 版本有關,本文的所有操作的環境均為 macOS 10.14 + Xcode 10.1。
如果能直接拿到需要 crash log 的設備,把它連到你的電腦上,在 Xcode 的 Window -> Devices and Simulators 裡的 Device 面板,在左側選中你的設備,點擊右側 View Device Logs 就會列出這個設備上的所有 log 了,其中 type 為 crash 的就是我們關注的 crash log。
但除了內部測試,大多數情況我們並不能拿到發生 crash 的設備。蘋果為 TestFlight 和 App Store 上的 app 提供了 crash log 收集功能。該功能只有在用戶每次更新或激活 iOS / macOS 系統後主動選擇同意 Apple 分享 App 使用數據和 crash 信息給開發者的選項後才能工作。
此外,為了獲得訪問日誌的權限,你需要在 App Store Connect 上擁有對應 App 的 developer 權限後,在你的 Xcode 的 Preferences -> Accounts 裡登錄你的帳號,到 Window -> Organizer 窗口裡選擇 Crashes 面板,最左側一欄就會列出所有你有權限訪問的 App,第二欄會分 App 版本列出蘋果收集到的你的用戶發生的 crash 內容。但我們要的不僅是這些,我們需要 .crash 文件來繼續後面的操作。右擊你關注的某個 crash,選擇 Show in Finder,會定位到一個位置為 ~/Library/Developer/Xcode/Products/${你 app的bundleID}/${版本號 (build號)}/Crashes/AppStore/${一串亂碼編號}.xccrashpoint 文件夾。右擊被選中的那個 .xccrashpoint 文件,選擇 Show Package Contents,再依次進入 DistributionInfos -> all -> Logs,裡面會有一個到數個的 .crash 文件,蘋果是按崩潰棧和崩潰類型歸類 crash 的,通常來說這些 crash log 都是同一個 bug 導致的 crash,但有時同一個 bug 會有多種不同的表現。
除了蘋果的服務外,還有一些第三方提供了這些服務,國外的有 Crashlytics,國內常用的有友盟、騰訊的 bugly 等。相對蘋果的服務,他們通常會有提供一些更漂亮的統計功能,以及將用戶 ID 與 crash 關聯等功能,但由於程序的運行機制限制,他們能拿到的信息必然要比系統少,一般只有堆棧信息和設備型號、系統版本等,本文所說的 crash log 指的都是 iOS 系統生成的日誌,不包括這些第三方日誌。
Crash Log 裡有啥
就在不久前我們也遇到了開頭所說的硬骨頭 bug,本地極難復現,崩潰信息又莫名其妙。慶幸的是 App Store 有為我們成功捕獲到幾個崩潰日誌,讓我們修復這個 bug 變為可能。我們拿出這次 crash log 作為例子給大家分享一下,如何更好地利用 crash log。由於文件很長,我們一段一段地看。
Incident Identifier: 92863615-57CE-45C1-84CD-E030A7A6C429
CrashReporter Key: affee827f837b5a5be6f46df116d48cafd2fe552
Hardware Model: iPhone9,2
Process: Telis [397]
Path: /private/var/containers/Bundle/Application/04136047-A194-40B1-B7A3-FA650D30BB66/Telis.app/Telis
Identifier: com.liulishuo.Telis
Version: 4316 (2.7.0)
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: com.liulishuo.Telis [495]
Date/Time: 2018-07-11 10:34:40.3214 +0800
Launch Time: 2018-07-11 09:34:56.2155 +0800
OS Version: iPhone OS 11.4 (15F79)
Baseband Version: 3.70.00
Report Version: 104
這段沒什麼特別需要說明的信息,從 Crashlytics 的統計信息中我們已經知道這個 crash 和設備型號,系統版本都沒有什麼關聯。
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x00000005bee2bec8
VM Region Info: 0x5bee2bec8 is not in any region. Bytes after previous region: 16624303817
REGION TYPE START - END [ VSIZE] PRT/MAX SHRMOD REGION DETAIL
MALLOC_NANO (reserved) 00000001d8000000-00000001e0000000 [128.0M] rw-/rwx SM=NUL ...(unallocated)
--->
UNUSED SPACE AT END
Termination Signal: Segmentation fault: 11
Termination Reason: Namespace SIGNAL, Code 0xb
Terminating Process: exc handler [0]
Triggered by Thread: 0
這段我們得知崩潰的原因是出現 EXC_BAD_ACCESS 異常,子類型是 KERN_INVALID_ADDRESS,通常是訪問到非法地址導致的,訪問的地址則是 0x5bee2bec8。接下來提供了一段更詳細的信息,告訴我們訪問的地址不屬於虛擬內存(VM)的任何 region,並給出了訪問地址在虛擬內存中位置和周圍的 region 布局。這裡簡單介紹一下 region,region 是在 page 再下一級的內存分配單元,通過讓不同大小的對象在不同 region 上分配空間,以減少內存碎片,並使用更適合的分配策略。在最後 Triggered by Thread: 0 告訴我們導致崩潰的線程是主線程。
回到我們的 crash log 上,有時這一段後面還會跟上一些更具體的描述信息:
Application Specific Information:
Fatal error: Unexpectedly found nil while unwrapping an Optional value
這種明確告訴我們是什麼原因崩潰的是最喜聞樂見的了,譬如上面這段,很明顯是 Swift 的 optional 類型被強制解包。這種 bug 通常都比較好修復,實在不懂可以把這段文本貼到 Stack Overflow 上搜一下。然而可惜的是,本次要討論的 crash 並沒有這些內容。繼續往下看,就是大家通常最關心的部分了,術語叫 stack traces 或 backtraces,可以翻譯成堆棧回溯,通常我們也會叫它崩潰堆棧(crash stack)。前面已經知道崩潰發生在主線程,這裡先省略其他線程的內容。
Thread 0 name:
Thread 0 Crashed:
0 libobjc.A.dylib 0x00000001805b17f4 objc_object::release() + 16 (objc-object.h:531)
1 SingleQuestionTestModule 0x0000000102692f7c TelisFlowQuestionStreamer.reset() + 416 (TelisFlowQuestionStreamer.swift:0)
2 SingleQuestionTestModule 0x0000000102696310 specialized TelisFlowQuestionStreamer.socketClosed(reason:code:wasClean:error:) + 308 (TelisFlowQuestionStreamer.swift:224)
3 SingleQuestionTestModule 0x0000000102698e50 partial apply for closure #4 in TelisFlowQuestionStreamer.buildSocket() + 92 (TelisFlowQuestionStreamer.swift:0)
4 TPNetworking 0x0000000102fda9ac closure #6 in InnerWebSocket.step() + 228 (WebSocket.swift:783)
5 TPNetworking 0x0000000102fec784 partial apply for closure #1 in InnerWebSocket.fire(_:) + 20 (WebSocket.swift:936)
6 TPNetworking 0x0000000103025ad8 thunk for @callee_guaranteed () -> () + 36 (HTTPServiceSessionDelegate.swift:0)
7 libdispatch.dylib 0x0000000180ccca60 _dispatch_client_callout + 16 (object.m:507)
...
通常拿到崩潰堆棧信息,我們最關心的無非是兩塊:實際發生崩潰的 frame(棧幀) 0 的代碼,和調用方法在我們自己的 binary 內的 frame。在這裡,frame 0 是 libobjc 的 objc_object::release() 方法,說明是內存管理出問題了,很有可能是對象被 overrelease。可能有些人會問,為什麼 ARC 也會有內存管理問題?這個問題我們先留著不答,到後面我們自然會明白。繼續往下看,這段代碼中涉及到我們自己代碼的 frame 號最小的一行是 frame 1,這裡告訴我們是 SingleQuestionTestModule 模塊的 TelisFlowQuestionStreamer.reset() 方法裡觸發了崩潰。後面緊接著的是此時執行到的行號,TelisFlowQuestionStreamer.swift 的 0 行。
這可真是大事不妙,我們最不想見到的事情發生了:行號信息丟失。在進行代碼優化的編譯配置下,行號信息丟失是時不時會出現的,有時我們可以從調用棧的前後幀推斷出實際出現問題的是哪一行,但像這個例子裡,下一幀調用的是系統的 release 方法,在 ARC 中我們很難判斷哪些位置會被編譯器自動插入 release,更何況有那麼多的 retain/release,也不知道是哪個呀。
這時候需要拿出我們的秘密武器,很多人熟悉又陌生的 lldb。lldb 是 llvm 的編譯工具鏈中的 debug 工具,平時使用 Xcode 時打斷點、檢查運行中的變量值等操作其實都是 Xcode 將 lldb 的運行結果進行了可視化的結果。除了 lldb 外,我們還需要準備好的是當時提交 App Store 的 archive 文件,在 Xcode 的 Organizer 裡的 Archives 面板裡能找到導出功能。然後我們需要做的是打開 Terminal,輸入 lldb。沒看錯,我們天天接觸的 lldb 其實是個可以脫離 Xcode 獨立使用的命令行工具。
$ lldb
(lldb)
接著調用 command script import lldb.macosx.crashlog 命令,你能看到類似下文的輸出。
(lldb) command script import lldb.macosx.crashlog
"crashlog" and "save_crashlog" command installed, use the "--help" option for detailed help
"malloc_info", "ptr_refs", "cstr_refs", and "objc_refs" commands have been installed, use the "--help" options on these commands for detailed help.
在使用 LLDB command script import lldb.macosx.crashlog 的時候你有可能碰到 import 標準庫出錯的問題,這是因為你安裝了非 macOS 自帶 python 從而導致 python 標準庫搜索路徑會指向你安裝的 python 版本,而 lldb 會無視你的搜索路徑設置,強制使用系統自帶 python 來 import 所需要的標準庫, 最終導致的 import 不兼容版本的標準庫。我暫時沒有找到太好的解決方案,建議你如果碰到這個問題,可以臨時修改 python 的搜索路徑使其指向系統內建 python /usr/bin/python;如果是用 brew 安裝的 python,可以執行 brew unlink python2 臨時解除 python 的符號連結。
lldb 是支持通過 python 腳本調用的,在腳本中我們只需要 import lldb 即可,本文不詳細介紹 python 中 lldb 庫的使用,感興趣的可以參考 lldb Python API文檔(https://lldb.llvm.org/python_reference/index.html)。剛剛通過 command script import 引入的 便是一段 Xcode 自帶的 python 腳本,位於 /Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/Python 下。該目錄下,還有一些其他的 lldb 腳本,本文也不多做介紹,不過此外如果你有自己的腳本也可以通過 command script import 命令引入。如果你希望某個腳本每次 lldb 啟動都自動加載,可以在~/.lldbinit文件中加入下面的內容(如果文件不存在就自己新建一個,你懂的):
# ~/.lldbinit
...
command script import path/to/your/script.py
說回到 lldb.macosx.crashlog ,這個腳本的主要功能是解析 Darwin 內核的 crash log,通過 crash log 中的信息還原儘可能地還原事故現場。是不是特別神奇,小小 crash log 竟然有如此能量。使用方法很簡單,crashlog 命令加上我們需要解析的 crash log 文件路徑即可。
(lldb) crashlog path/to/crashlog.crash
如果你心急地按前面步驟操作的話,這裡肯定已經看到滿屏幕的報錯了。這是由於這個腳本的一些局限性導致的。知其然更要知其所以然,解決這個問題需要我們真正理解這個腳本是怎麼工作的。但在我們揭開這個腳本的神秘面紗前,我們還需要 crash log 中的一些額外信息。
我們先把關注點回到 crash log 上。緊跟在崩潰堆棧下面的內容是崩潰發生時崩潰線程的寄存器信息,平時聯機調試崩潰的時候我們可以隨意地切換到不同線程的不同棧幀上查看當前的寄存器內容以及所有內存中變量的內容,但在用戶發生 crash 的時候,把整個虛擬內存中的內容全部導出給開發者 debug 顯然是不切實際的,我們沒辦法要求用戶傳一個幾百兆的日誌給我們,更何況內存中或許有大量敏感信息。
Thread 0 crashed with ARM Thread State (64-bit):
x0: 0x00000001c0a290c0 x1: 0x00000001c0a290c0 x2: 0x0000000000000008 x3: 0x0000000180ea906c
...
x28: 0x00000001026c64ea fp: 0x000000016da86810 lr: 0x0000000102692f7c
sp: 0x000000016da86810 pc: 0x00000001805b17f4 cpsr: 0x20000000
儘管系統只為我們留了寄存器中這麼一點信息,但如果使用得當,我們還是能通過它獲得一些額外的幫助。譬如當問題出在 crash 線程的頂部棧幀,若 crash 位置前後有 br/blr 指令,可以通過算出要移動到的地址進而幫助你解決 crash。不過這裡我們不需要寄存器裡的信息,繼續往下看到 Binary Image。
Binary Images:
0x102378000 - 0x10245ffff YourApp arm64 <98a3f237f7b1345b93cb685fbbab7c08> /var/containers/Bundle/Application/B8D077AF-2EC6-47F9-9367-C8DD91E14BA2/Telis.app/Telis
0x1024c4000 - 0x1024f3fff IGListKit arm64 <413e42f511d833f6972b4a5ed9c05573> /var/containers/Bundle/Application/B8D077AF-2EC6-47F9-9367-C8DD91E14BA2/Telis.app/Frameworks/IGListKit.framework/IGListKit
...
0x180d6d000 - 0x180deafff libsystem_c.dylib arm64 <4fdfb9bed517340693481047718c8b0b> /usr/lib/system/libsystem_c.dylib
...
Binary Image 欄位列出了 app 已經連結的所有動態連結庫信息,不僅包括 app 的主可執行文件(YourApp),我們自己的 Framework(IGListKit),還有系統的動態連結庫(libsystem_c.dylib)
以第一條 app 主可執行文件的記錄為例,0x102378000 - 0x10245ffff 是其在內存中的地址範圍,接下來以此是其名字,指令集類型、uuid、存儲位置。出於安全原因,對於支持 PIE(Position Independent Execution)的 App iOS 每次加載過程都會進行內存隨機化,因此即使在同一個設備上同一個 binary image 在不同 crash log 裡地址也是不同的。此外 UUID 是編譯時決定的,即使是同一份代碼在同一臺設備上每次編譯也是不一致的,這也是為什麼 dSYM 文件不能混用,且我們在上面說到準備工作的要求一定要是提交 App Store 時的 archive。另一個涉及到隨機化的點是最後一條安裝位置,iOS App 在安裝時不像 macOS 一樣直接安裝在 Application 目錄,而是會被指定在 Application 目錄的一個 UUID 構成的目錄下,這就是前面我們運行 crash log 命令會報錯的原因。
所以說到底那個腳本做的事情很容易理解:讀取每一段 Binary Image 的信息,在其描述的路徑找到對應的二進位文件,將其中對應指令集的 _TEXT 段加載到正確的內存位置,然後使用 dSYM 文件將所有 symbols 符號化以便於閱讀。
但這裡有幾個問題。
首先,這個腳本最初的設計的目的是調試本機的 crash log,而如果我們的 crash log 是來自於真實設備而不是模擬器,那我們電腦上相同的路徑下必然找不到對應的二進位文件,之後的操作完全沒法繼續。這個問題需要分兩種情況分別處理:我們打包進去的和 iOS 系統提供的。我們自己的 Binary Image 可以通過修改 crash log 中對應記錄的磁碟位置讓腳本找到我們本地 archive 文件裡的二進位文件,具體來說就是把文件中諸如 /var/containers/Bundle/Application/B8D077AF-2EC6-47F9-9367-C8DD91E14BA2/YourAppName.app 這樣的路徑全部替換成 path/to/YourAppName.xcarchive/Products/Applications/YourAppName.app。此外因為 Swift 直到 4.2 都未實現 ABI 穩定,所以不能通過系統內置一個 Swift 來統一為所有 Swift 提供運行時支持,因此如果你的項目使用了 Swift 開發,還會包括 Swift 的 runtime / 標準庫 / 用於輔助 iOS SDK 標準庫橋接到 Swift 的動態連結庫等。對於系統自帶的動態連結庫會比較麻煩,你需要一個和 crash log 中描述的系統版本完全一致的設備,將其連結到你的電腦,讓 Xcode 完成 copy debug info 的操作,再將那些動態連結庫指向 ~/Library/Developer/Xcode/iOS DeviceSupport/ 目錄中的版本。
其次, crashlog 這個腳本自身有些問題,腳本不是通過指定 dSYM 文件路徑,而是會使用另一個腳本利用 macOS 的 spotlight 功能尋找 dSYM 文件。然而不知道蘋果開源這些 lldb 工具集的時候是怎麼考慮的,這個輔助腳本很神奇地沒有被開源。
第三個問題是 Bitcode 引起的。如果你不了解 Bitcode 技術,這裡簡單解釋一下。蘋果在 WWDC 2015 開始在 App Store 開始提供 Bitcode 功能,開發者提交到 App Store 的二進位文件不只是用戶設備上真正運行的機器碼,會額外附帶一份中間碼,也就是 Bitcode。Bitcode 是 clang / swiftc -> llvm 編譯體系下的中間產物,可以進一步編譯成機器碼。按蘋果的說法,當編譯器有優化或者有新的指令集架構時,他們會使用 Bitcode 生成新的機器碼代替你的。但非常尷尬的是,因為 Bitcode 生成是在 swiftc 編譯之後的流程,swift 社區每天都有大量對編譯器優化,而這些優化是不能通過使用 Bitcode 技術享受到的;其次 Bitcode 可能導致你的代碼和你本地編譯的版本不一致,而你只能從 App Store Connect 下載到 dSYM 文件而沒有二進位文件。在我們的例子下就完全不能進行後續的 crash log 分析工作了。
題外話:絕大多數情況 Bitcode 編譯的版本和你直接使用最新版本 Xcode 編譯的基本是一致的。雖然不清楚蘋果主動對你的代碼重新從 Bitcode 生成機器碼的時機,但畢竟儘管理論上可行,實際上蘋果不太可能頻繁地重新生成你的程序的機器碼。通過一些簡單的 hack 操作強行使用本地編譯的版本解析開啟 Bitcode 後的用戶 crash log 通常是能夠被 lldb 正確解析的。
這裡我提供了一個修改版本的 crash log 腳本,能通過指定 archive 文件位置來幫你解決前面提到的這些問題,加載 dSYM 時會優先搜索 archive 文件內的版本,且使用名字匹配而非 uuid 匹配,以可能會匹配錯誤為代價換取儘可能高的腳本兼容性。你可以在 (https://github.com/huajiahen/crashlog-cracker) 找到,用法在項目 README 中有介紹,這裡不再贅述。
$ python CrashlogCracker.py --archive Telis.xcarchive/ crashlog.crash
crashlog rebuild at converted.crashlog.crash
使用這個腳本「破解」 crashlog 後,我們回到 lldb 重新還原一下事故現場:
(lldb) crashlog path/to/converted.crashlog.crash
如果你提供的 Archive 沒有問題(與 crash log 裡寫明的 App 版本一致,且沒有文件丟失或損壞),接下來能看到數條 Getting symbols for 9951067F-2686-3CC0-936C-43B682E0A0CD /xxx/YourFramework.framework/YourFramework... ok 的消息,緊接著就是修復成功的 crash log 了!
Thread[0] EXC_BAD_ACCESS (SIGSEGV) (KERN_INVALID_ADDRESS at 0x00000005cb31bec8)
[ 0] 0x00000001805b17f4 libobjc.A.dylib`objc_object::release() + 16
0x00000001805b17e4: stp x29, x30, [sp, #-0x10]!
0x00000001805b17e8: mov x29, sp
0x00000001805b17ec: ldr x8, [x0]
0x00000001805b17f0: and x8, x8, #0xffffffff8
-> 0x00000001805b17f4: ldrb w8, [x8, #0x20]
0x00000001805b17f8: tbz w8, #0x1, 0x1800cd854
0x00000001805b17fc: tbnz x0, #0x3f, 0x1800cd834
0x00000001805b1800: orr x8, xzr, #0x200000000000
0x00000001805b1804: ldxr x9, [x0]
[ 1] 0x0000000102692f7b SingleQuestionTestModule`SingleQuestionTestModule.TelisFlowQuestionStreamer.(reset in _18DE03F5A23DB8E8946B005331DA88B6)() -> () [inlined] SingleQuestionTestModule.TelisFlowQuestionStreamer.(socket in _18DE03F5A23DB8E8946B005331DA88B6).setter : Swift.Optional<TPNetworking.WebSocket> + 11 at TelisFlowQuestionStreamer.swift:0
[ 1] 0x0000000102692f70 SingleQuestionTestModule`SingleQuestionTestModule.TelisFlowQuestionStreamer.(reset in _18DE03F5A23DB8E8946B005331DA88B6)() -> () + 404 at TelisFlowQuestionStreamer.swift:81
...
瞧瞧 lldb 幫我們找到了什麼!
首先它幫我們直接找到了 crash 所在行的彙編指令,其次還幫我們準確定位到了 frame 1 實際上是由 TelisFlowQuestionStreamer.swift:81 這行 inline 調用了 socket 屬性的 setter,由於某種編譯器問題導致一些 inline 方法行號丟失了。
按一般的理解,當使用譬如 Codable 協議等會觸發編譯器自動生成代碼的技術,顯然會產生 debug 信息為 0 行的情況,因為這些指令本就沒有對應的實際代碼;不過在 inline 調用時插入代碼也會有行號不正常的情況,這個行為可能有點反直覺。實際上解釋起來也很簡單,因為 inline 方法調用實質上是編譯器把被調用方法的方法體複製到每一個調用處替換原始代碼,而複製過去的代碼很難說是屬於被替換行還是原始所屬行;另一方面每一個棧幀所處的行號和文件只能是一個,不能同時屬於多個文件或行號。當沒有足夠的信息來將一個棧幀中的 inline 調用拆解出來是,你就會得到這樣莫名其妙的崩潰信息。對於大多數語言,你可以自己標記 inline 方法,而你沒有標記 inline 的方法,在開啟編譯器優化時也會經常碰到自動內聯優化,典型的就是文中碰到的 setter 方法 inline,此外有些語言會有尾遞歸優化導致的調用棧信息丟失,而這些通常是你在 debug 程序時不會碰到的情況。通過本文的介紹,再碰到這類問題時,你就可以通過熟悉又陌生的老朋友 lldb 來幫助你解決問題。
為什麼 setter 內部會發生 objc_object::release() + 16 的 crash 呢?實際上 objc_object::release() + 16 處發生 EXC_BAD_ACCESS 的 crash 是一種比較常見的問題,如果你是有經驗的開發者,尤其是對 Objective-C 的對象結構及的內存分配策略有了解,你會很快意識到這裡發生了 over-release。ObjC 對象會在對象頭部存儲指向該對象所屬的類的 isa 指針,在調用 release 方法時會訪問到這一欄位。然而,在對象被釋放(dealloc)時,對象佔用內存會被釋放,頭部指針會被修改指向一塊未被分配到內存區域。當嘗試訪問這個指針時,系統發現這塊區域內存還未分配,屬於非法區域,自然會拋出 EXC_BAD_ACCESS 的錯誤了。
為什麼會出現 over-release 的情況超出了本文的範圍。如果你想了解更多,可以訪問蘋果的開源網站(https://opensource.apple.com/),Objective-C 的對象結構源碼在 objc4 項目中能找到,而關於內存分配管理則在 libmalloc 項目中。
擴展閱讀文中介紹的 crashlog 使用技巧,主要參考了(WWDC18-Session 414 Understanding Crashes and Crash Logs )以及 Apple 關於 crash log 的 technical note (Technical Note TN2151: Understanding and Analyzing Application Crash Reports)。你可以觀看 session 視頻了解更多的關於 crash log 的知識。
戳「閱讀原文」get 流利說工作機會噢