使用 LLDB、互動式記憶體圖形除錯器和 Instruments 來除錯 iOS App 並分析效能。適用於崩潰、保留循環檢查、卡頓、建置失敗,以及一般的 CPU、記憶體、電量或網路分析。如需 .memgraph 擷取、leaks CLI 所有權路徑或持續堆積成長分析,請使用 ios-memgraph-analysis;如需 ETTrace 擷取與 JSON 輸出,請使用 ios-ettrace-performance。
除錯與 Instruments
將互動式圖形與 Instruments 的初步分析集中在這裡。詳細的 .memgraph 命令列所有權/成長分析以及 ETTrace 工作請交由專門的技能處理。
目錄
LLDB 除錯
從一個小而可重複的工作流程開始:
- 在 Debug 建置中重現問題,並停在最精確的中斷點。
- 在不執行程式碼的情況下檢查區域變數,然後擷取當前堆疊。
- 移動到相關的 frame 或執行緒,確認失敗狀態。
- 只有在錯誤轉換仍不明確時,才加入條件或監視點。
(lldb) br set -f ViewModel.swift -l 42 # 停在指定檔案與行號
(lldb) v myLocal # 不執行程式碼直接檢查
(lldb) po myObject # 需要時使用 debugDescription
(lldb) bt all # 擷取所有執行緒的 backtrace
(lldb) frame select 3 # 檢查相關的 frame
(lldb) br modify 1 -c "count > 10" # 縮小雜亂中斷點的範圍
(lldb) w set v self.score # 在非預期的寫入時停下
當你只需要區域變數的值時,使用 v 而非 po——它不會執行程式碼,也不會觸發副作用。表達式求值可能會執行或改變程式狀態,而硬體監視點數量有限,因此請謹慎使用兩者。
載入 references/lldb-patterns.md 以取得完整的檢查、中斷點/logpoint、表達式、監視點、執行緒導航與符號中斷點命令表。
記憶體除錯
記憶體圖形除錯器工作流程
- 以 Debug 設定執行 App。
- 重現疑似洩漏的操作(例如進入某個畫面再返回)。
- 點擊 Xcode 除錯列上的記憶體圖形按鈕。
- 尋找紫色警告圖示——這些表示已洩漏的物件。
- 選取一個洩漏的物件,查看其參考圖形與 backtrace。
在執行前啟用 Malloc Stack Logging(Scheme > Diagnostics),這樣記憶體圖形才能顯示配置的 backtrace。
常見保留循環模式
閉包強烈捕捉 self:
// 洩漏 — 閉包持有 self 的強參考
class ProfileViewModel {
var onUpdate: (() -> Void)?
func startObserving() {
onUpdate = {
self.refresh() // 強烈捕捉 self
}
}
}
// 修正 — 使用 [weak self]
func startObserving() {
onUpdate = { [weak self] in
self?.refresh()
}
}
強委派參考:
// 洩漏 — 強委派造成循環
protocol DataDelegate: AnyObject {
func didUpdate()
}
class DataManager {
var delegate: DataDelegate? // 應為 weak
}
// 修正 — 弱委派
class DataManager {
weak var delegate: DataDelegate?
}
計時器保留目標:
// 洩漏 — Timer.scheduledTimer 保留其目標
timer = Timer.scheduledTimer(
timeInterval: 1.0, target: self,
selector: #selector(tick), userInfo: nil, repeats: true
)
// 修正 — 使用閉包 API 搭配 [weak self]
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.tick()
}
Instruments:Allocations 與 Leaks
- Allocations 模板:追蹤隨時間的記憶體成長。使用「Mark Generation」功能隔離使用者操作之間(例如開啟/關閉畫面)所配置的物件。
- Leaks 模板:偵測已洩漏的配置,包括程序無法再觸及的孤立保留循環。與 Allocations 一起執行以獲得完整圖像。
- 依 App 的模組名稱過濾,排除系統配置。
對於洩漏或記憶體成長的處理,搭配使用工具:在重現步驟前後使用 Allocations 的 Mark Generation 來證明持續成長,然後使用記憶體圖形除錯器檢查物件所有權,並透過 Malloc Stack Logging 取得配置的呼叫堆疊。
Malloc Stack Logging
在 Scheme > Run > Diagnostics > Malloc Stack Logging 中啟用。這會記錄配置的 backtrace,讓記憶體圖形除錯器、Allocations 工具以及匯出的 .memgraph 檔案能夠顯示物件是在哪裡建立的。
# 檢查從 Xcode 或 Instruments 匯出的記憶體圖形
leaks MyApp.memgraph
卡頓診斷
識別主執行緒卡頓
對於離散互動,低於 100 毫秒的延遲通常不易察覺。Apple 開發者工具通常會報告主執行緒執行迴圈忙碌超過 250 毫秒的情況,但該報告門檻並非產品目標:幾百毫秒仍可能讓人感覺反應遲鈍。常見的偵測工具:
- Thread Checker(Xcode Diagnostics):警告非主執行緒的 UI 呼叫
- Thread Performance Checker:在除錯時報告優先權反轉
- 裝置端卡頓偵測:開發者設定會報告來自實際裝置使用的卡頓
- Time Profiler / CPU Profiler / Hitches:分析可重現的卡頓
- os_signpost 與
OSSignposter:在 Instruments 中標記區間 - MetricKit 卡頓診斷:生產環境的卡頓偵測(請參閱
metrickit技能以了解HangDiagnostic與 iOS 26 相容性)
import os
let signposter = OSSignposter(subsystem: "com.example.app", category: "DataLoad")
func loadData() async {
let state = signposter.beginInterval("loadData")
let result = await fetchFromNetwork()
signposter.endInterval("loadData", state)
process(result)
}
使用 Time Profiler
- Product > Profile(Cmd+I)啟動 Instruments。
- 選取 Time Profiler 模板。
- 在重現緩慢互動時進行錄製。
- 聚焦於主執行緒——依「Weight」排序以找出熱點路徑。
- 勾選「Hide System Libraries」以僅查看你的程式碼。
- 雙擊一個耗時的 frame 以跳轉到原始碼。
常見卡頓原因
| 原因 | 症狀 | 修正 |
|---|---|---|
| 主執行緒上的同步 I/O | 網路/檔案讀取阻塞 UI | 移至 Task { } 或背景 actor |
| 鎖競爭 | 主執行緒等待背景工作持有的鎖 | 使用 actor 或減少鎖範圍 |
| 版面配置震盪 | 重複呼叫 layoutSubviews |
批次處理版面變更,避免強制佈局 |
| 解析大型 JSON 負載 | 資料載入時 UI 凍結 | 在背景執行緒解析 |
| 同步圖片解碼 | 圖片密集列表的滾動卡頓 | 使用 AsyncImage 或在主執行緒外解碼 |
建置失敗處理
閱讀編譯器診斷
- 從第一個錯誤開始——後續錯誤通常是連鎖反應。
- 在建置記錄中搜尋錯誤代碼(例如
error: cannot convert)。 - 使用 Report Navigator(Cmd+9)查看完整的建置記錄與時間戳。
SPM 依賴解析
# 常見:版本衝突
error: Dependencies could not be resolved because root depends on 'Package' 1.0.0..<2.0.0
# 修正:檢查 Package.resolved 並更新版本範圍
# 必要時重設套件快取:
rm -rf ~/Library/Caches/org.swift.swiftpm
rm -rf .build
swift package resolve
找不到模組 / 連結器錯誤
| 錯誤 | 檢查項目 |
|---|---|
No such module 'Foo' |
Target 成員資格、匯入路徑、framework 搜尋路徑 |
Undefined symbol |
連結階段缺少 framework、架構錯誤 |
duplicate symbol |
兩個 target 定義了相同符號;檢查 ObjC 命名衝突 |
應優先檢查的建置設定:
FRAMEWORK_SEARCH_PATHSOTHER_LDFLAGSSWIFT_INCLUDE_PATHSBUILD_LIBRARY_FOR_DISTRIBUTION(適用於 XCFramework)
Instruments 概覽
模板選擇指南
| 模板 | 使用時機 |
|---|---|
| Time Profiler | CPU 使用率高、UI 反應慢、需要找出熱點程式碼路徑 |
| Allocations | 記憶體隨時間成長、需要追蹤物件生命週期 |
| Leaks | 懷疑有保留循環或遺棄物件 |
| Network | 檢查 HTTP 請求/回應的時機與負載 |
| SwiftUI | 分析 View body 的評估次數與更新頻率 |
| Animation Hitches / Core Animation instruments | 影格掉落、卡頓、混合與提交/渲染工作 |
| Power Profiler | 電池耗電、熱壓力、背景能源影響 |
| File Activity | 過多的磁碟 I/O、緩慢的檔案操作 |
| System Trace | 執行緒排程、系統呼叫、虛擬記憶體錯誤 |
用於 CI 分析的 xctrace 命令列工具
# 從命令列錄製 trace
xcrun xctrace record --device "My iPhone" \
--template "Time Profiler" \
--instrument "Allocations" \
--output profile.trace \
--launch -- /path/to/MyApp.app
# 將 trace 資料匯出為 XML 以進行自動化分析
xcrun xctrace export --input profile.trace --xpath '/trace-toc/run/data/table'
# 列出可用模板
xcrun xctrace list templates
# 列出已連接的裝置
xcrun xctrace list devices
每次錄製使用一個 --template;使用 --instrument 加入額外工具。在 CI 流程中使用 xctrace 自動偵測效能回歸。比較不同建置間的匯出指標。
常見錯誤
不要:使用 print() 除錯而非 os.Logger
使用 Logger 以獲得層級、隱私中繼資料以及子系統/類別過濾功能;.debug 層級保留在記憶體中,不會在 Release 建置中持久化。
// 錯誤 — 非結構化,無法依子系統/類別過濾
print("user tapped button, state: \(viewModel.state)")
print("network response: \(data)")
// 正確 — 使用 Logger 進行結構化記錄
import os
let logger = Logger(subsystem: "com.example.app", category: "UI")
logger.debug("Button tapped, state: \(viewModel.state, privacy: .public)")
logger.info("Network response received, bytes: \(data.count)")
不要:在記憶體除錯前忘記啟用 Malloc Stack Logging
// 錯誤 — 未啟用 Malloc Stack Logging 就開啟記憶體圖形
// 結果:可看到洩漏物件,但沒有配置 backtrace
// 正確 — 在執行前啟用:
// Scheme > Run > Diagnostics > 勾選 "Malloc Stack Logging: All Allocations"
// 然後執行、重現洩漏,再開啟記憶體圖形
不要:在最佳化程式碼上除錯卻期望完整的變數可見性
// 錯誤 — 使用 Debug 建置分析,使用 Release 建置除錯
// Debug 建置:額外的執行時期檢查會扭曲效能測量
// Release 建置:變數在除錯器中顯示為 "<optimized out>"
// 正確做法:
// 除錯:使用 Debug 設定(完整符號,無最佳化)
// 分析:使用 Release 設定(真實效能)
不要:在沒有條件中斷點的情況下停在每個迴圈迭代
// 錯誤 — 中斷點設在迴圈內的行,會停 10,000 次
for item in items {
process(item) // 中斷點在這裡,每個項目都會停
}
// 正確 — 使用條件中斷點:
// (lldb) br set -f MyFile.swift -l 42 -c "item.id == targetID"
// 或在 Xcode 中:右鍵點擊中斷點 > Edit > 加入 Condition
不要:忽略 Thread Sanitizer 警告
Thread Sanitizer(TSan)警告表示資料競爭,可能僅間歇性崩潰。除非你已確認是工具問題,否則應將其視為真正的錯誤。
// 錯誤 — 忽略關於並行存取的 TSan 警告
var cache: [String: Data] = [:] // 從多個執行緒存取
// 正確 — 保護共享的可變狀態
actor CacheActor {
var cache: [String: Data] = [:]
func get(_ key: String) -> Data? { cache[key] }
func set(_ key: String, _ value: Data) { cache[key] = value }
}
啟用 TSan:Scheme > Run > Diagnostics > Thread Sanitizer。對於 iOS、iPadOS、tvOS、visionOS 和 watchOS App,請在模擬器中執行 TSan;Apple 文件指出僅 64 位元 macOS App 支援裝置端執行。
審查清單
- [ ] 使用
os.Logger而非print()進行診斷輸出 - [ ] 在記憶體除錯階段前已啟用 Malloc Stack Logging
- [ ] 在 dismiss/dealloc 流程後檢查記憶體圖形除錯器
- [ ] 委派宣告為
weak var以防止保留循環 - [ ] 儲存為屬性的閉包使用
[weak self]捕捉列表 - [ ] 計時器使用閉包 API 並搭配
[weak self] - [ ] 在模擬器測試方案中啟用 Thread Sanitizer 以處理競爭問題
- [ ] 主執行緒上沒有同步 I/O 或大量計算
- [ ] 在 Release 建置上執行 Time Profiler 以取得效能基準
- [ ] 從建置記錄的第一個錯誤開始處理建置失敗
- [ ] 使用
OSSignposter標記自訂效能區間 - [ ] 對迴圈/集合除錯使用條件中斷點




