展示線程調用棧
首先看一下函數(shù)調用棧, 該圖來自于 Wiki

上圖表示了一個棧,它分為若干棧幀(frame),每個棧幀對應一個函數(shù)調用,比如藍色的部分是 DrawSquare 函數(shù)的棧幀,它在執(zhí)行的過程中調用了 DrawLine 函數(shù),棧幀用綠色表示。
可以看到棧幀由三部分組成: 函數(shù)參數(shù),返回地址,幀內的變量。舉個例子,在調用 DrawLine 函數(shù)時首先把函數(shù)的參數(shù)入棧,這是第一部分;隨后將返回地址入棧,這表示當前函數(shù)執(zhí)行完后回到哪里繼續(xù)執(zhí)行;在函數(shù)內部定義的變量則屬于第三部分。
Stack Pointer(棧指針)表示當前棧的頂部,由于大部分操作系統(tǒng)的棧向下生長,它其實是棧地址的最小值。根據(jù)之前的解釋,Frame Pointer 指向的地址中,存儲了上一次 Stack Pointer 的值,也就是返回地址。
在大多數(shù)操作系統(tǒng)中,每個棧幀還保存了上一個棧幀的 Frame Pointer,因此只要知道當前棧幀的 Stack Pointer 和 Frame Pointer,就能知道上一個棧幀的 Stack Pointer 和 Frame Pointer,從而遞歸的獲取棧底的幀。
顯然當一個函數(shù)調用結束時,它的棧幀就不存在了。
因此,調用棧其實是棧的一種抽象概念,它表示了方法之間的調用關系,一般來說從棧中可以解析出調用棧。
思路
剛看到這個需求的時候, 想起來 iOS 中Thread.callstackSymbols可以獲取到調用堆棧信息, 但是實施的時候發(fā)現(xiàn)不行, 有兩個原因:
- 的確可以獲取到堆棧信息, 但是沒法得到預估時間.

- 我們程序啟動之后, 就會開啟一個RunLoop(一個循環(huán), 類似于如下代碼), 我們UI刷新, CADisplayLink,CATransition,CAAnimation, GCD中的Main Queue也會放入主循環(huán)中. 其他線程, 以及其他事件會添加到其他的 runloop 中, 每一個線程都維護了自己的runloop, runloop 的調用順序是source0、source1、timer、dispatch_queue, UI 相關都會放入source0(____CFRunLoopDoSources0__)中, 在一步步執(zhí)行到Viewdidload()方法, 事件處理完畢之后陷入休眠, 等待事件喚醒, 這時候如果在子線程想打印主線程的信息就無法抓取到了, 因為已經(jīng)執(zhí)行完其他方法(都已經(jīng)出棧了).
performSelector也是發(fā)送了一個消息(事件), runloop 在執(zhí)行. 所以不可行
int main(int argc, char * argv[]) {
//程序一直運行狀態(tài)
while (AppIsRunning) {
//睡眠狀態(tài),等待喚醒事件
id whoWakesMe = SleepForWakingUp();
//得到喚醒事件
id event = GetEvent(whoWakesMe);
//開始處理事件
HandleEvent(event);
}
return 0;
}
主線程的棧信息:
graph TD
dyld[dyld, 存儲了系統(tǒng)中動態(tài)鏈接庫的位置, 快速查找] -->main.m(UIApplication/main.m)
main.m -->GraphicServices[GSEventRunModal/GraphicServices]
GraphicServices --> RunLoop[RunLoop]
RunLoop --> Event
RunLoop/包含CFRunLoopRunSpecific,__CFRunLoopRun,__CFRunLoopDoSouces0,CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION

實現(xiàn)思路
既然直接使用自帶的Thread.callstackSymbols方法, 那么就需要另辟蹊徑了, 回想一下我們開始看到的線程的調用棧, 我們每一個當前還未出棧的方法還未執(zhí)行, 都是保存在棧里面的, 我們可以通過獲取棧幀然后遞歸到棧底.
所以我們現(xiàn)在需要去拿到棧的指針(StackPointer)以及棧幀指針(FramePointer).
系統(tǒng)提供了 task_threads 方法,可以獲取到所有的線程,注意這里的線程是最底層的 mach 線程.
對于每一個線程,可以用 thread_get_state 方法獲取它的所有信息,信息填充在 _STRUCT_MCONTEXT 類型的參數(shù)中(這個方法中有兩個參數(shù)隨著 CPU 架構的不同而改變).
我們需要存儲線程的StackPointer以及 頂部的FramePointer, 通過遞歸獲取到整個調用棧.
根據(jù)棧幀的 Frame Pointer 獲取到這個函數(shù)調用的符號名
這樣我們的實現(xiàn)思路就是:
- 獲取線程的StackPointer 以及 FramePointer
- 找到FramePointer屬于哪一個鏡像文件(.m)
- 獲取鏡像文件的符號表
- 在符號表中找到函數(shù)調用地址對應的符號名
- return 到上一級調用函數(shù)的FramePointer, 重復第2步.
- 到達棧底, 退出.
NSThread
它是對pthread的面向對象表示.
-
pthread是(POSIX 的簡寫,POSIX 表示 “可移植操作系統(tǒng)接口(Portable Operating System Interface)"). - NSThread的封裝方式.
不同的操作會設計自己的線程模型, 所以底層 API 是不相同的, 但是 POSIX提供的pthread就是相當于對底層進行了一次封裝, 讓不同平臺運行得到相同的效果.
Unix 系統(tǒng)提供的 thread_get_state 和 task_threads 等方法,操作的都是內核線程,每個內核線程由 thread_t 類型的 id 來唯一標識,pthread 的唯一標識是 pthread_t 類型。
內核線程和 pthread 的轉換(也即是 thread_t 和 pthread_t 互轉)很容易,因為 pthread 誕生的目的就是為了抽象內核線程。
我們來看一下NSThread里面用到 pthread 的地方
- (void) start {
pthread_attr_t attr;
pthread_t thr;
int errno = 0;
pthread_attr_init(&attr);
if (pthread_create(&thr, &attr, nsthreadLauncher, self)) {
// Error Handling
}
}
NSThread并沒有存儲pthread_t的信息.
只有在exit()方法再次使用了pthread_exit().
我們來研究一下performSelector的實現(xiàn), 它最終會調用
- (void) performSelector: (SEL)aSelector
onThread: (NSThread*)aThread
withObject: (id)anObject
waitUntilDone: (BOOL)aFlag
modes: (NSArray*)anArray;
該方法只是一層封裝, 用于獲取線程RunLoop, 真正被調用是在RunLoop中的.
- (void) performSelector: (SEL)aSelector
target: (id)target
argument: (id)argument
order: (NSUInteger)order
modes: (NSArray*)modes{
}
封裝成Perfomer作為一個Event放入 Runloop 中等待執(zhí)行.
了解到這里有什么用呢, 我們接著往下看(我們的目標是通過 NSThread 得到 pthread).
系統(tǒng)并沒有提供轉換的方法, 我們需要尋找其他的方式去實現(xiàn).
hook住performSelector的執(zhí)行, 然后在指定線程的代碼部分保存thread_t, 執(zhí)行代碼的時機不能太晚, 在打印的調用棧的時候會破壞目前的結構, 最好是在創(chuàng)建的時候就執(zhí)行, 從上方的代碼可以看見pthread_create(&thr, &attr, nsthreadLauncher, self) 會執(zhí)行nsthreadLauncher, 該方法的定義:
static void *nsthreadLauncher(void* thread)
{
NSThread *t = (NSThread*)thread;
[nc postNotificationName: NSThreadDidStartNotification object:t userInfo: nil];
[t _setName: [t name]];
[t main];
[NSThread exit];
return NULL;
}
驚喜的發(fā)現(xiàn), 它會發(fā)送一個一個線程開始NSThreadDidStartNotification的通知, 然后監(jiān)聽這個通知去調用performSelector方法.
但是performSelector是基于runloop的, runloop 需要到main函數(shù)才有可能會開啟(還有一點, 啟動的線程一般在執(zhí)行完事務之后就會退出銷毀, 要做?;? 就需要傳入runloop(循環(huán))).
所以, 這個方向也無法解除, 既然無法轉換, 那么我們可以找到NSThread和pthread共有的唯一屬性嗎?
打印NSThread
<NSThread: 0x600002fc0cc0>{number = 1, name = main}
有一個地址, 一個線程序號, 線程名稱.
地址是堆區(qū)的, 線程序號不是很清楚, 最后只剩下名稱, 是否名稱是唯一的呢? 去嘗試一下pthread_getname_np(pthread_t, UnsafeMutablePointer<Int8>, Int)方法, 發(fā)現(xiàn)對應的名字是一樣的(根據(jù)bestswifter文中所說, NSThread的setName方法也是直接調用 pthread 接口).
這樣我們就可以通過NSThread 通過設置名字接口(記住是唯一的), 然后遍歷所有的線程找到名字一樣的線程在去獲得StackPointer, FramePointer等關鍵信息.
閱讀bestswifter文章的時候, 作者提到了一個坑
主線程修改名字之后是無法通過pthread_getname_np讀取到的, 所以我們在之前就將值讀取出來.
// 獲取主線程
let main_thread_t = mach_thread_self()