如何更好地利用 iOS Crash Log

2021-02-13 流利說技術團隊

      如果你有過幾款正式線上 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.

troubleshooting

在使用 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 流利說工作機會噢

相關焦點

  • 再談 iOS App Crash 防護
    去年,網易杭州研究院曾經針對 crash 的防護有提出『大白健康系統--iOS APP 運行時 Crash 自動修復系統[1]』方案,使得 crash 防護這個想法真正被落實,但至今該方案的具體實現並沒有被開源。圈子裡也有一些開發朋友,基於這套方案設計並開源了自己的 「Baymax」,比如『老司機 iOS 周報第七期[2]』中曾提到的 BayMaxProtector[3]。
  • iOS App 連續閃退時如何上報 crash 日誌
    crash 日誌上報有兩個難點:這篇文章介紹下出現第二種情況時,如何準確上報 crash 日誌。首先我們需要一種比較可靠的方式,可以在 app 啟動時判斷上次是否發生了啟動 crash。介紹一個可行的思路。如何檢測連續閃退連續閃退包含兩個元素,閃退和連續。只有這兩個元素同時具備時,才會影響我們的日誌上傳。
  • ios crash的原因與抓取crash日誌的方法
    crash的產生來源於兩種問題:違反iOS策略被幹掉,以及自身的代碼bug。1.IOS策略1.1 低內存閃退前面提到大多數crash日誌都包含著執行線程的棧調用信息,但是低內存閃退日誌除外,這裡就先看看低內存閃退日誌是什麼樣的。
  • 大白健康系統--iOS APP運行時Crash自動修復系統
    「小王啊,剛剛上線的X.X.X版本出問題了啊,怎麼樣操作會crash啊,導致新功能都無法使用了,快定位一下是什麼原因,抓緊hotpatch修復一下啊!」。心裡一萬頭草泥馬呼嘯而過,瞬間已經滿頭大汗的你卻還要故作鎮靜地回答:「嗯,老闆我馬上去看看,一定努力解決問題!」 急忙打開電腦的你,知道今夜註定無眠了。
  • MySQL 資料庫崩潰(crash)的常見原因和解決辦法
    MySQL 資料庫 crash 後都會重新啟動,因此我們有時可能不知道 MySQL 資料庫已經 crash 過了,但我們可以從mysql資料庫啟動時間上找到線索,下面介紹四種檢查 MySQL 資料庫啟動時間的方法。
  • Android log分析(Logcat介紹)
    緩衝區的log信息,試著抓一段log看看---- beginning of xxx為起點,開始捕捉Android日誌。xxx表示當前log獲取的日誌緩衝區,示例獲取的是system,main緩衝區log, 我們在之前的文章中曾介紹到Android系統在運行時會時刻在幾個設備文件中的一個中寫入字符串。而這幾個設備文件指向幾個環形緩衝區。這是Android日誌的記錄原理。而這幾個緩衝區合稱日誌記錄器緩衝區。
  • 給iOS 模擬器「安裝」app文件
    files    --zlibCompressionLevel num use compression level 'num' when creating a PKZip archive    --password                 request password for reading from encrypted PKZip archiveDitto比cp命令更好的地方在於
  • 【iOS取證技巧】在無損的情況下完整導出SQLite資料庫
    參考來源:medium,編譯:secist,轉自:FreeBuf.COM通過利用其中的一些特性來提取SQLite資料庫
  • 深度學習中如何更好地利用顯存資源?
    如何更高效地利用GPU顯存,在一張卡或一臺機器上同時承載更多的訓練和預測任務,讓有限的顯存支持多個開發者同時進行實驗,執行各自的任務呢?飛槳v1.7在GPU顯存使用策略方面做了如下3點升級:默認使用顯存自增長AutoGrowth策略,根據模型實際佔用的顯存大小,按需自動分配顯存,並且不影響訓練速度。
  • 在Windows上一鍵啟動IOS穩定性測試
    但是如今Facebook已經放棄了對WDA的後續兼容,轉向idb研發(類似於安卓中的adb工具,但在真機上存在穩定性問題尚無法完全替代WDA,WDA目前已由Appium以社區形式接手繼續迭代。)在Android端,被融合了機器學習和算法,大大的強化了穩定性測試,且其算法已被解耦,也就是ios端也可以使用機器學習和算法,而且持續在更新優化中,最可喜的是它可以輔助其他工具擺脫xcode運行,後續日誌等功能開發也在排期中。
  • 「兩個大局」下,如何更好地應對外部衝擊?
    當前,百年未有之大變局和中華民族偉大復興戰略全局同步交織、相互激蕩,我們該如何更好地應對外部衝擊?歷史上後發展國家經濟突圍的經驗與策略,極具借鑑意義。此外,日本一些跨國公司開拓新的技術發展空間,在美國設立技術研發中心和科研基地,充分利用了美國國內的技術條件好、信息靈敏等優勢,柔性打破美國的技術封鎖。最後,採取靈活對外政策,大力發展公共外交。在面對先發優勢國家的封鎖打壓時,後發展國家往往不能墨守成規,而要採取靈活政策,巧妙利用自身優勢開展公共外交活動,加大與國際社會的聯繫與合作,進而推動新領域融冰。
  • Git log高級用法
    但是,首先你得知道如何使用它。這也就是為什麼會有git log 這個命令。到現在為止,你應該已經知道如何用git log 命令來顯示最基本的提交信息。但除此之外,你還可以傳入各種不同的參數來獲得不一樣的輸出。git log有兩個高級用法:一是自定義提交的輸出格式,二是過濾輸出哪些提交。這兩個用法合二為一,你就可以找到你項目中你需要的任何信息。
  • [系統安全] 八.Windows漏洞利用之CVE-2019-0708復現及藍屏攻擊
    文章目錄:一.漏洞描述二.遠程桌面連接三.Kali系統復現漏洞聲明:本人堅決反對利用教學方法進行犯罪的行為,一切犯罪行為必將受到嚴懲,綠色網絡需要我們共同維護,更推薦大家了解它們背後的原理,更好地進行防護。
  • 已經有攻擊者利用迭代後的Log4Shell漏洞發起攻擊
    Log4j是Apache的一個開源項目,通過使用Log4j,我們可以控制日誌信息輸送的目的地是控制臺、文件、GUI組件,甚至是套接口伺服器、NT的事件記錄器、UNIX Syslog守護進程等;我們也可以控制每一條日誌的輸出格式;通過定義每一條日誌信息的級別,我們能夠更加細緻地控制日誌的生成過程。最令人感興趣的就是,這些可以通過一個配置文件來靈活地進行配置,而不需要修改應用的代碼。
  • 掌握坐標軸的log轉換
    對於跨度很大其分布離散的數據,常用log轉換來縮寫其差距,呈現在圖上的效果也更好,比如在繪製轉錄組的表達量數據時,常用log轉換之後的值進行繪製。
  • 如何更好地使用音響話筒呢?
    隨著無線話筒的普及和廣泛使用,怎樣才能更好地發揮它們的優越作用呢?接下來就讓小編來給您介紹一下。
  • tensorflow(7)利用tensorflow/serving實現BERT模型部署
    在文章tensorflow(4)使用tensorboard查看ckpt和pb圖結構中,我們知道了如何使用tensorboard查看ckpt圖結構。sess.close()在ckpt目錄下運行該腳本,生成log文件夾,使用命令:tensorboard --logdir=log,在瀏覽器中localhost:6006,即可查看模型圖結構,如下圖:模型結構圖從上面的模型結構圖中,我們可以知道,該模型的輸入為input_ids、input_mask、segment_ids、dropout,再通過代碼可知輸出為
  • 小雞模擬器ios版遊戲官方下載_小雞模擬器ios版手遊官方下載_18183...
    最新小雞模擬器ios版官方下載就在18183遊戲庫,快來下載玩玩吧! 小雞模擬器ios版是專為蘋果用戶打造的模擬器遊戲下載平臺。模擬很多掌機和街機,簡直是神器。街機掌機最全、經典遊戲最多的遊戲模擬器!在這裡,你可以玩到萬款經典模擬器遊戲,如:恐龍快打、拳皇97等,趕快下載,回味經典吧!
  • iOS 11原來是這樣!我想升級了.
    蘋果的WWDC2017如期而至,我們更希望看到的是全新系統ios11,現在終於亮相了,在整個ios11介紹中,圖中這些應用,也都有了全新的變化。