鍍金池/ 教程/ iOS/ 活動追蹤
與四軸無人機的通訊
在沙盒中編寫腳本
結構體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學
NSString 與 Unicode
代碼簽名探析
測試
架構
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應用數(shù)據(jù)腳本化
一個完整的 Core Data 應用
插件
字符串
為 iOS 建立 Travis CI
先進的自動布局工具箱
動畫
為 iOS 7 重新設計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡應用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應式 Android 應用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調試
項目介紹
Swift 的強大之處
測試并發(fā)程序
Android 通知中心
調試:案例學習
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學習的一代人
視頻
Playground 快速原型制作
Omni 內部
同步數(shù)據(jù)
設計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉場
照片框架
響應式視圖
Square Register 中的擴張
DTrace
基礎集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設計的藝術
導航應用
線程安全類的設計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗證
數(shù)據(jù)同步
自定義 ViewController 容器轉場
游戲
調試核對清單
View Controller 容器
學無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術:Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴展
理解 Scroll Views
使用 VIPER 構建 iOS 應用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機捕捉
語言標簽
同步案例學習
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉字符串
相機工作原理
Build 過程

活動追蹤

追查出異步執(zhí)行的代碼中出現(xiàn)的錯誤往往是一件非常困難的事,因為棧追蹤 (stack trace) 局限于發(fā)生崩潰的線程,這就意味著你無法獲得全部的上下文信息。與此同時,編寫異步代碼卻得益于 libdispatch,運行隊列 (operation queues) 和 XPC 提供的 API 變得越發(fā)的簡單。

活動追蹤是一項 iOS 8 和 OS X 10.10 新引入的技術,它正是為了幫助我們更好地解決上面提到的這個問題。今年的 WWDC 有一個有關這個主題非常棒的 session,不過我們認為在這里為它另外寫一篇簡介也是一個好主意,畢竟知道它的人還不多。

活動追蹤技術最基本的思想是,用一個活動匯集響應某個用戶交互行為或其他事件的全部工作,無論這些工作是同步的執(zhí)行的還是被分配到其他隊列和進程中去的。舉個例子來說,如果用戶在你的應用中觸發(fā)了一個刷新然后崩潰了,即使這個崩潰是發(fā)生在其他的隊列中或者有其他的代碼路徑也能運行到崩潰的代碼,但你總能知道就是“刷新”這個特定的用戶行為造成了接下來發(fā)生的崩潰。

活動追蹤由三部分組成:活動,面包屑 (譯者注:參考面包屑導航) 和追蹤信息。我們會在接下來詳細探討這三部分,在這里先給出要點:活動可以在跨隊列跨進程的情況下幫助你追蹤出導致崩潰發(fā)生的事件。面包屑可以幫你跨越多個活動畫出導致崩潰發(fā)生的事件軌跡。最后,追蹤信息可以幫助你為當前活動添加更詳細的信息。出現(xiàn)任何問題,這三種信息都會出現(xiàn)在最終的崩潰報告中。

在我們進入細節(jié)講解之前,讓我簡單的提一下使用活動追蹤過程中可能的坑:如果活動追蹤信息沒有顯示出來,檢查 system.log 中是否有 diagnosticd 這個守護進程 (譯者注:參考守護進程) 給出的類似 "Signature Validation Failed" 的信息,你可以會遇到代碼簽名的問題。此外,注意在 iOS 上活動追蹤只能在真機上工作,在模擬器上不行。

活動

活動是這項新技術的核心部分。一個活動匯集了響應一個事件所需的全部代碼運行過程,而無論代碼是運行在哪個隊列哪個進程中的。這樣在代碼運行過程中出現(xiàn)任何問題,都可以輕易地追蹤回造成崩潰的源頭的事件。

活動追蹤集成在 AppKit 和 UIKit 中,所以每當一個用戶界面事件通過 target-action 機制傳遞,一個活動會自動開啟。對于不會向響應鏈發(fā)送事件的用戶交互,例如點擊 table view cell,你就需要自己手動初始化一個活動。

啟動一個活動非常簡單:

#import <os/activity.h>

os_activity_initiate("activity name", OS_ACTIVITY_FLAG_DEFAULT, ^{
    // do some work...
});

這個 API 會同步地執(zhí)行 block,在 block 中運行的任何代碼,即使是那些你分配到其他隊列上去運行的工作或者進行了 XPC 調用,都會被歸為這個活動中。第一個參數(shù)是活動的標記,必須賦予一個字符串常量 (活動追蹤 API 中其他字符串參數(shù)也都必須是常量)。

第二個參數(shù),OS_ACTIVITY_FLAG_DEFAULT,是你要新建一個全新活動的標識。如果你想在一個已經存在的活動中創(chuàng)建一個新的活動,那么必須使用 OS_ACTIVITY_FLAG_DETACHED。舉例來說,當響應一個用戶交互控件發(fā)送的行為信息時,AppKit 已經為你開啟一個活動。如果你想在這個用戶交互響應沒有完成的時候開啟一個活動,你就需要一個分離的活動。

There are other variants of this API that work in the same away — a function-based one (os_activity_initiate_f), and one that consists of a pair of macros:

這個 API 有幾個同樣功能的變體,一個基于函數(shù) (os_activity_initiate_f),一個由幾個宏命令組成:

os_activity_t activity = os_activity_start("label", OS_ACTIVITY_FLAG_DEFAULT);
// do some work...
os_activity_end(activity);

注意你必須設置至少一個追蹤信息,否則活動就不會出現(xiàn)在崩潰報告或者其他任何形式的查看方式中。在后面會有更多有關追蹤信息的細節(jié)。

面包屑

面包屑所起到的作用就像它的名字所暗示的那樣:當代碼出現(xiàn)崩潰,你的代碼會留下一個可以說明上下文信息的跨活動事件標記軌跡。添加面包屑非常的簡單:

os_activity_set_breadcrumb("event description");

事件被存儲在一個環(huán)形的緩沖區(qū)內,這個緩沖區(qū)只記錄最近運行的50個事件。所以這個 API 只應該用來標記宏觀的事件,比如像特定的用戶交互行為。

注意調用這個 API 只能在一個活動過程當中時有效:將當前這個活動標記為一個面包屑。這也意味著每一個活動你只能標記一次,接下來的調用會被系統(tǒng)忽略。

追蹤信息

追蹤信息用來為活動添加附加的說明信息,就跟你使用 log 信息的目的一樣。你可以用來向崩潰報告添加一些重要的說明信息,這樣就可以使你更容易理解造成問題出現(xiàn)的根源。在一個活動中,一個簡單的追蹤信息可以這樣添加:

#import <os/trace.h>

os_trace("my message");

除此之外,追蹤信息可以起到更大的作用。os_trace 的第一個參數(shù)是一個格式化字符串,和你在 printfNSLog 中的用法一樣。不過 os_trace 有一些限制:格式化字符串最長只能包含 100 個字符和最多 7 個標量值的占位符。這就意味著你不能輸出字符串,如果你嘗試輸出字符串,字符串會被一個占位符所替換。

下面是兩個在 os_trace 中使用格式化字符串的例子:

os_trace("Received %d creates, %d updates, %d deletes", created, updated, deleted);
os_trace("Processed %d records in %g seconds", count, time);

我在嘗試使用這個 API 時偶然發(fā)現(xiàn)了一個坑,那就是如果沒有從發(fā)生崩潰的線程中發(fā)送出來追蹤信息,那么所有的追蹤信息都不會出現(xiàn)在崩潰報告中。

追蹤信息的變體

os_trace 的基礎上有好幾個功能相似的 API。首先是 os_trace_debug,可以用來只在調試模式下輸出追蹤信息。在生產環(huán)境中這會很有幫助,可以減少環(huán)形緩沖區(qū)存儲的無用追蹤信息,這樣你就可以得到最有價值的那些信息。要開啟調試模式,需要設置環(huán)境變量 OS_ACTIVITY_MODEdebug。

除此之外,還有兩個功能相近的宏命令可以輸出追蹤信息:os_trace_erroros_trace_fault。前者可以用來表明出現(xiàn)了未預料到的錯誤,后者用來表明出現(xiàn)了致命的錯誤,比如說馬上就要崩潰了。

前面我們提到了,標準的 os_trace API 只接受一個限制長度和標量類型的格式化字符串。這樣的設計是基于對隱私性,安全性和運行效率的考慮。但是,當你在調試一個問題的時候,總有一些情況下會想要得到更多的數(shù)據(jù)。這個時候帶有負載 (payload) 的追蹤信息就派上了用場了。

帶有負載的追蹤信息對應的 API 是 os_trace_with_payload,初看可能會有些奇怪:它類似于 os_trace,這個 API 接收一個格式化字符串,一個可變數(shù)量的參數(shù)值,和一個帶有 xpc_object_t 類型參數(shù)的 block。這個 block 在生產環(huán)境中不會被調用,也就不會產生額外開銷。不同的是,在調試的過程中你可以在 block 唯一的字典參數(shù)中存儲任何你想要的數(shù)據(jù)。

os_trace_with_payload("logged in: %d", guid, ^(xpc_object_t xdict) {
    xpc_dictionary_set_string(xdict, "name", username);
});

這個 block 的參數(shù)類型之所以是 XPC 對象,是因為在活動追蹤技術的底層使用了 diagnosticd 守護進程收集數(shù)據(jù)。通過 API xpc_dictionary_set_* 設置這個字典對象的值就是在與這個進程進行通信。你可以通過命令行工具 ostraceutil 來查看負載的數(shù)據(jù),關于這個工具我們接下來會深入探討。

我們之前討論的所有與 os_trace 相似的宏命令都可以進行負載。與上面使用的 os_trace_with_payload 很像,我們還有 os_trace_debug_with_payload,os_trace_error_with_payload,和 os_trace_fault_with_payload。

查看活動追蹤的進行

除了崩潰報告之外你還有兩種辦法可以查看活動追蹤的輸出。首先,活動追蹤集成在調試器中。在 LLDB 的控制臺中輸入 thread info,你可以得到當前線程中當前進行的活動和追蹤的信息。

(lldb) thread info
thread #1: tid = 0x19514a, 0x000000010000125b ActivityTracing2`__24-[ViewController crash:]_block_invoke_4(.block_descriptor=<unavailable>) + 27 at ViewController.m:26, queue = 'com.apple.main-thread', activity = 'crash button pressed', 1 messages, stop reason = EXC_BAD_ACCESS (code=1, address=0x0)

  Activity 'crash button pressed', 0x8e700000005

  Current Breadcrumb: button pressed

  1 trace messages:
    message1

另一種選擇是使用命令行工具 ostraceutil,在終端中運行

sudo ostraceutil -diagnostic -process <pid> -quiet

(將 <pid> 替換為進程 id) 會輸出如下信息 (有縮減)

Process:
==================
PID: 16992
Image_uuid: FE5A6C31-8710-330A-9203-CA56366876E6
Image_path: [...]

Application Breadcrumbs:
==================
Timestamp: 59740.861604, Breadcrumb ID = 6768, Name = 'Opened theme picker', Activity ID: 0x000008e700000001
Timestamp: 59742.202451, Breadcrumb ID = 6788, Name = 'button pressed', Activity ID: 0x000008e700000005

Activity:
==================
Activity ID: 0x000008e700000005
Activity Name: crash button pressed
Image Path: [...]
Image UUID: FE5A6C31-8710-330A-9203-CA56366876E6
Offset: 0x1031
Timestamp: 59742.202350
Reason: none detected

Messages:
==================
Timestamp: 59742.202508
FAULT
Activity ID: 0x000008e700000005
Trace ID: 0x0000c10000001ac0
Thread: 0x1951a8
Image UUID: FE5A6C31-8710-330A-9203-CA56366876E6
Image Path: [...]
Offset: 0x118d
Message: 'payload message'
----------------------
Timestamp: 59742.202508
RELEASE
Trace ID: 0x0000010000001aad
Offset: 0x114c
Message: 'message2'
----------------------
Timestamp: 59742.202350
RELEASE
Trace ID: 0x0000010000001aa4
Thread: 0x19514a
Offset: 0x10b2
Message: 'message1'

真正的輸出會比 LLDB 控制臺的輸出多很多,因為還包括了面包屑軌跡和所有線程的追蹤信息。

除了使用 ostraceutil-diagnostic 參數(shù)之外,我們還可以使用 -watch 參數(shù)動態(tài)地查看追蹤信息和面包屑的輸出。在這種模式下,還會輸出追蹤信息的負載數(shù)據(jù)。

[...]
----------------------
Timestamp: 60059.327207
FAULT
Trace ID: 0x0000c10000001ac0
Offset: 0x118d
Message: 'payload message'
Payload: '<dictionary: 0x7fd2b8700540> { count = 1, contents =
    "test-key" => <string: 0x7fd2b87000c0> { length = 10, contents = "test-value" }
}'
----------------------
[...]

活動追蹤和 Swift

在寫下這篇文章的時候,Swift 還不能直接調用活動追蹤 API。

如果你想在一個 Swift 項目中使用活動追蹤,就必須創(chuàng)建一個 Objective-C 的封裝,使得 Swift 可以通過橋接頭文件 (bridging header) 來調用那些 API。注意活動追蹤那些宏命令要求字符串是常量,也就是說你不能直接用封裝函數(shù)的參數(shù)做活動追蹤 API 的參數(shù)。為了說明這一點,下面這樣的調用是不起作用的:

void sendTraceMessage(const char *msg) {
    os_trace(msg); // this doesn't work!
}

一種可能的解決方法是定義特定的輔助函數(shù)像這樣:

void traceLogin(int guid) {
    os_trace("Login: %d", guid);
}

總結

活動追蹤作為一個新的調試工具非常受歡迎,它使得我們更容易診斷異步代碼中出現(xiàn)的錯誤。我們都應該將標記活動面包屑和追蹤信息作為編碼的新習慣。

對于那些已經在正式生產環(huán)境中使用 Swift 的人來說,目前最大的遺憾是 Swift 不能夠直接調用。希望 Swift 的集成只是一個不會很久遠的時間問題。

上一篇:Build 過程下一篇:UI 測試