debugging-instruments

debugging-instruments

熱門

使用 LLDB、互動式記憶體圖形除錯器和 Instruments 來除錯 iOS App 並分析效能。適用於崩潰、保留循環檢查、卡頓、建置失敗,以及一般的 CPU、記憶體、電量或網路分析。如需 .memgraph 擷取、leaks CLI 所有權路徑或持續堆積成長分析,請使用 ios-memgraph-analysis;如需 ETTrace 擷取與 JSON 輸出,請使用 ios-ettrace-performance。

932星標
47分支
更新於 2026/7/15
SKILL.md
唯讀
名稱
debugging-instruments
描述

使用 LLDB、互動式記憶體圖形除錯器和 Instruments 來除錯 iOS App 並分析效能。適用於崩潰、保留循環檢查、卡頓、建置失敗,以及一般的 CPU、記憶體、電量或網路分析。如需 .memgraph 擷取、leaks CLI 所有權路徑或持續堆積成長分析,請使用 ios-memgraph-analysis;如需 ETTrace 擷取與 JSON 輸出,請使用 ios-ettrace-performance。

除錯與 Instruments

將互動式圖形與 Instruments 的初步分析集中在這裡。詳細的 .memgraph 命令列所有權/成長分析以及 ETTrace 工作請交由專門的技能處理。

目錄

LLDB 除錯

從一個小而可重複的工作流程開始:

  1. 在 Debug 建置中重現問題,並停在最精確的中斷點。
  2. 在不執行程式碼的情況下檢查區域變數,然後擷取當前堆疊。
  3. 移動到相關的 frame 或執行緒,確認失敗狀態。
  4. 只有在錯誤轉換仍不明確時,才加入條件或監視點。
(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、表達式、監視點、執行緒導航與符號中斷點命令表。

記憶體除錯

記憶體圖形除錯器工作流程

  1. 以 Debug 設定執行 App。
  2. 重現疑似洩漏的操作(例如進入某個畫面再返回)。
  3. 點擊 Xcode 除錯列上的記憶體圖形按鈕。
  4. 尋找紫色警告圖示——這些表示已洩漏的物件。
  5. 選取一個洩漏的物件,查看其參考圖形與 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_signpostOSSignposter:在 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

  1. Product > Profile(Cmd+I)啟動 Instruments。
  2. 選取 Time Profiler 模板。
  3. 在重現緩慢互動時進行錄製。
  4. 聚焦於主執行緒——依「Weight」排序以找出熱點路徑。
  5. 勾選「Hide System Libraries」以僅查看你的程式碼。
  6. 雙擊一個耗時的 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_PATHS
  • OTHER_LDFLAGS
  • SWIFT_INCLUDE_PATHS
  • BUILD_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 標記自訂效能區間
  • [ ] 對迴圈/集合除錯使用條件中斷點

參考資料