iOS主線程耗時檢測方案

前言

主線程耗時是一個App性能的重要指標。主線程阻塞,立馬會引發用戶操做的卡頓,這是最直接的反應,因此是咱們必須關注的一個性能點。html

檢測方案

Instrument - Time Profiler

Time Profiler模板使用Time Profiler工具對系統CPU上運行的進程執行低開銷,基於時間的採樣,顯示App對多核CPU和線程的使用狀況。ios

隨着時間的推移,使用多個核心和線程的效率越高,App的性能就越好。git

不熟悉的同窗,能夠參考官方文檔Track CPU core and thread usegithub

你須要在用Time Profiler以前,須要開啓生成dSYM符號文件,不然你只能看到系統函數的調用。服務器

Debug模式,默認不會生成dSYM符號文件。數據結構

須要在Build Setting > Debug Infomation Format 選項中,爲Debug開啓dSYM文件的生成。架構

iOS-time-profiler-2019-03-23-1

而後啓動Xcode,build當前項目。app

再打開Instrument,選擇Time Profiler模板,開始錄製。函數

Time Profiler中會記錄每一個線程中的函數調用關係樹,使咱們更容易定位到是哪一段代碼致使了線程的阻塞。工具

iOS-time-profiler-2019-03-23-3

雙擊這條記錄,就能看到這段代碼的源碼

iOS-time-profiler-2019-03-23-4

OK,到此爲止,Time Profiler就介紹到這裏,幾乎都是UI界面,你們很容易就能使用了。

那說說Time Profiler的缺點

  • 檢測時,必須有Xcode支持。
  • 真機檢測時,必須處於聯機狀態。
  • 沒法實現自定義的輸出內容。

自制檢測工具

Time Profiler雖然好用,但也有侷限性,這時本身搭建一套檢測工具,想必是你們都會想的事情。

這個檢測工具的功能能夠參照Time Profiler

  • 函數調用的關係樹
  • 經過配置對統計數據進行篩選
  • 捕獲的函數儘量的多
  • 格式化輸出統計數據
  • 統計數據日誌化管理,上傳到服務器

開始我也是從Method Swizzle思路出發,對UIViewController、UIView的耗時進行了,惋惜僅僅這麼作的話,統計的顆粒度太粗了,實際用起來並很差。

hook objc_msgSend函數會是個更好的選擇。我查閱了objc4的源碼後,發現會涉及到對C庫的Hook,以及使用匯編語言對objc_msgSend實現的重寫,線程的局部存儲

好在,iOS發展到如今,已經有不少的大神給咱們提供了輪子,好比我這找到了戴銘老師的輪子進行二次封裝。

iOS-time-profiler-2019-03-23-2

個人項目還沒整理完,不過實現原理基本借鑑了戴銘老師的設計,你們能夠參考他的項目搭建本身的統計系統。

借鑑的項目地址:GCDFetchFeed

C庫的hook用的是FaceBook的fishhook,我就很少介紹了,這個你們應該耳熟能詳了吧。

這次封裝涉及文件:

​ SMCallTrace.h ​ SMCallTrace.m ​ SMCallTraceCore.c ​ SMCallTraceCore.h

我對hook objc_msgSend方法的主要實現部分進行了代碼註釋,但願能幫助你們理解,hook是如何完成的。

//
// GHObjcMsgSendHook.c
// CommercialVehiclePlatform
//
// Created by JunhuaShao on 2019/3/17.
// Copyright © 2019 JunhuaShao. All rights reserved.
//

/******************************************************** objc_msgSend Hook代碼來源:https://github.com/ming1016/GCDFetchFeed 線程局部存儲:https://blog.csdn.net/vevenlcf/article/details/77882985 ******************************************************** */

#import "GHObjcMsgSendHook.h"

// 此Hook只支持arm64架構
#ifdef __aarch64__

//#import <objc/runtime.h>
//#import <sys/time.h>
//
//#import <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <objc/message.h>
#include <objc/runtime.h>
#include <dispatch/dispatch.h>
#include <pthread.h>

#import "fishhook.h"

/** Configuration */
// 調用記錄工具開關
static bool _call_record_enabled = true;
// 設置最小耗時閾值,單位:微秒
static uint64_t _min_time_cost = 1000; //us
// 設置最大調用深度閾值
static int _max_call_depth = 3;

/** iVar */
// 被替換的原objc_msgSend
__unused static id (*orig_objc_msgSend)(id, SEL, ...);
// 用於和調用線程關聯的key,在開啓工具時初始化
static pthread_key_t _thread_key;
// 格式化的耗時記錄
static GHCallRecord *_ghCallRecords;
// 格式化的耗時記錄佔用空間
static int _ghRecordAlloc;
// 格式化耗時記錄的個數
static int _ghRecordNum;

/** 被hook函數的調用記錄 */
typedef struct {
    // 經過 object_getClass 可以獲得 Class
    id self;
    // 經過 NSStringFromClass 可以獲得類名
    Class cls;
    // 經過 NSStringFromSelector 方法可以獲得方法名
    SEL cmd;
    // us 調用時間(微秒)
    uint64_t time;
    // link register 用於指定下一個函數的地址
    uintptr_t lr;
} thread_call_record;

/** 線程中函數調用棧 */
typedef struct {
    // 當前被hook函數的調用記錄
    thread_call_record *stack;
    // 當前存儲空間大小
    int allocated_length;
    // 當前記錄序號
    int index;
    // 是否在主線程上
    bool is_main_thread;
} thread_call_stack;

/** 得到線程中的函數調用棧 @return 函數調用棧 */
static inline thread_call_stack * get_thread_call_stack() {
    /** int pthread_setspecific (pthread_key_t key, const void *value) 用於將value的副本存儲於一數據結構中,並將其與調用線程以及key相關聯。 參數value一般指向由調用者分配的一塊內存。 當線程終止時,會將該指針做爲參數傳遞給與key相關聯的destructor函數。 void *pthread_getspecific (pthread_key_t key); 當線程被建立時,會將全部的線程局部存儲變量初始化爲NULL。 所以第一次使用此類變量前必須先調用pthread_getspecific()函數來確認是否已經於對應的key相關聯, 若是沒有,那麼能夠經過分配一塊內存並經過pthread_setspecific()函數保存指向該內存塊的指針。 */
    thread_call_stack *cs = (thread_call_stack *)pthread_getspecific(_thread_key);
    if (cs == NULL) {
        // 爲函數調用棧開闢空間
        cs = (thread_call_stack *)malloc(sizeof(thread_call_stack));
        // 爲hook函數記錄開闢空間,大小爲128個記錄大小
        cs->stack = (thread_call_record *)calloc(128, sizeof(thread_call_record));
        // 初始當前存儲空間爲64個記錄大小
        cs->allocated_length = 64;
        // 初始化序號
        cs->index = -1;
        /** int pthread_main_np(void); 若是在主線程上,會返回不爲零的結果 */
        cs->is_main_thread = pthread_main_np();
        // 將調用棧與線程關聯
        pthread_setspecific(_thread_key, cs);
    }
    return cs;
}

static void release_thread_call_stack(void *ptr) {
    thread_call_stack *cs = (thread_call_stack *)ptr;
    if (!cs) return;
    // 釋放調用棧
    if (cs->stack) free(cs->stack);
    free(cs);
}

static inline void push_call_record(id _self, Class _cls, SEL _cmd, uintptr_t lr) {
    // 得到當前線程關聯的調用棧
    thread_call_stack *cs = get_thread_call_stack();
    if (cs) {
        // 序號增一
        int nextIndex = (++cs->index);
        // 若是序號超過了當前存儲空間大小
        if (nextIndex >= cs->allocated_length) {
            // 將當前存儲空間增加64個記錄大小
            cs->allocated_length += 64;
            // 爲指針從新分配調用棧的空間,爲當前存儲空間大小。
            cs->stack = (thread_call_record *)realloc(cs->stack, cs->allocated_length * sizeof(thread_call_record));
        }
        // 得到當前序號對應的內存地址,建立新記錄
        thread_call_record *newRecord = &cs->stack[nextIndex];
        // 記錄調用對象
        newRecord->self = _self;
        // 記錄調用class
        newRecord->cls = _cls;
        // 記錄調用函數
        newRecord->cmd = _cmd;
        // 記錄下一個調用函數地址
        newRecord->lr = lr;
        /** 當前線程爲主線程,而且開啓了調用記錄功能。 目的是隻統計主線程耗時 */
        if (cs->is_main_thread && _call_record_enabled) {
            /** Linux定義的timeval結構體 __darwin_time_t tv_sec; //seconds __darwin_suseconds_t tv_usec; //and microseconds tv_sec爲Epoch到建立struct timeval時的秒數, tv_usec爲微秒數,即秒後面的零頭。 這裏用了高精度,因此對二者進行了相加,取了最近的100秒 */
            struct timeval now;
            // 得到當前時間
            gettimeofday(&now, NULL);
            newRecord->time = (now.tv_sec % 100) * 1000000 + now.tv_usec;
        }
    }
}

static inline uintptr_t pop_call_record() {
    // 獲取當前調用棧
    thread_call_stack *cs = get_thread_call_stack();
    // 當前調用記錄序號
    int curIndex = cs->index;
    // 父級函數調用記錄序號
    int nextIndex = cs->index--;
    // 獲取父級函數調用記錄,出棧
    thread_call_record *pRecord = &cs->stack[nextIndex];
    // 一樣是主線程,而且開啓記錄功能
    if (cs->is_main_thread && _call_record_enabled) {
        // 獲取當前時間
        struct timeval now;
        gettimeofday(&now, NULL);
        uint64_t time = (now.tv_sec % 100) * 1000000 + now.tv_usec;
        // 若是當前時間小於上次記錄的時間,則進位了,這裏加上100秒
        if (time < pRecord->time) {
            time += 100 * 1000000;
        }
        // 得到耗時
        uint64_t cost = time - pRecord->time;
        // 耗時大於耗時閾值,調用深度小於最大深度,則進行記錄。這裏調用序號,即爲深度
        if (cost > _min_time_cost && cs->index < _max_call_depth) {
            // 初始化格式化耗時記錄
            if (!_ghCallRecords) {
                // 建立空間大小爲1024個記錄
                _ghRecordAlloc = 1024;
                _ghCallRecords = (GHCallRecord *)malloc(sizeof(GHCallRecord)*_ghRecordAlloc);
                
            }
            // 記錄個數加一
            _ghRecordNum++;
            // 當前記錄個數大於空間時,從新爲指針分配內存,大小爲比原來多1024個記錄
            if (_ghRecordNum >= _ghRecordAlloc) {
                _ghRecordAlloc += 1024;
                _ghCallRecords = (GHCallRecord *)realloc(_ghCallRecords, sizeof(GHCallRecord) * _ghRecordAlloc);
            }
            // 獲取當前頁數對應的地址,建立格式化記錄。
            GHCallRecord *log = &_ghCallRecords[_ghRecordNum - 1];
            // 保存調用class
            log->cls = pRecord->cls;
            // 保存調用深度
            log->depth = curIndex;
            // 保存調用方法
            log->sel = pRecord->cmd;
            // 保存耗時
            log->time = cost;
        }
    }
    // 返回下個函數的調用地址
    return pRecord->lr;
}

void hook_before_objc_msgSend(id self, SEL _cmd, uintptr_t lr) {
    // 函數調用記錄入棧
    push_call_record(self, object_getClass(self), _cmd, lr);
}

uintptr_t hook_after_objc_msgSend() {
    // 函數調用記錄出棧
    return pop_call_record();
}
// replacement objc_msgSend (arm64)
// https://blog.nelhage.com/2010/10/amd64-and-va_arg/
// http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/IHI0055B_aapcs64.pdf
// https://developer.apple.com/library/ios/documentation/Xcode/Conceptual/iPhoneOSABIReference/Articles/ARM64FunctionCallingConventions.html
#define call(b, value) \ __asm volatile ("stp x8, x9, [sp, #-16]!\n"); \ __asm volatile ("mov x12, %0\n" :: "r"(value)); \ __asm volatile ("ldp x8, x9, [sp], #16\n"); \ __asm volatile (#b " x12\n");

#define save() \ __asm volatile ( \ "stp x8, x9, [sp, #-16]!\n" \ "stp x6, x7, [sp, #-16]!\n" \ "stp x4, x5, [sp, #-16]!\n" \ "stp x2, x3, [sp, #-16]!\n" \ "stp x0, x1, [sp, #-16]!\n");

#define load() \ __asm volatile ( \ "ldp x0, x1, [sp], #16\n" \ "ldp x2, x3, [sp], #16\n" \ "ldp x4, x5, [sp], #16\n" \ "ldp x6, x7, [sp], #16\n" \ "ldp x8, x9, [sp], #16\n" );

#define link(b, value) \ __asm volatile ("stp x8, lr, [sp, #-16]!\n"); \ __asm volatile ("sub sp, sp, #16\n"); \ call(b, value); \ __asm volatile ("add sp, sp, #16\n"); \ __asm volatile ("ldp x8, lr, [sp], #16\n");

#define ret() __asm volatile ("ret\n");

__attribute__((__naked__))
static void hook_Objc_msgSend() {
    // Save parameters.
    save();
    
    __asm volatile ("mov x2, lr\n");
    __asm volatile ("mov x3, x4\n");
    
    // Call our before_objc_msgSend.
    call(blr, &hook_before_objc_msgSend);
    
    // Load parameters.
    load();
    
    // Call through to the original objc_msgSend.
    call(blr, orig_objc_msgSend);
    
    // Save original objc_msgSend return value.
    save();
    
    // Call our after_objc_msgSend.
    call(blr, &hook_after_objc_msgSend);
    
    // restore lr
    __asm volatile ("mov lr, x0\n");
    
    // Load original objc_msgSend return value.
    load();
    
    // return
    ret();
}

void ghAnalyerStart() {
    _call_record_enabled = true;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        pthread_key_create(&_thread_key, &release_thread_call_stack);
        rebind_symbols((struct rebinding[6]){
            {"objc_msgSend", (void *)hook_Objc_msgSend, (void **)&orig_objc_msgSend},
        }, 1);
    });
}

void ghAnalyerStop() {
    _call_record_enabled = false;
}

void ghSetMinTimeCallCost(uint64_t us) {
    _min_time_cost = us;
}
void ghSetMaxCallDepth(int depth) {
    _max_call_depth = depth;
}

GHCallRecord *ghGetCallRecords(int *num) {
    if (num) {
        *num = _ghRecordNum;
    }
    return _ghCallRecords;
}

void ghClearCallRecords() {
    if (_ghCallRecords) {
        free(_ghCallRecords);
        _ghCallRecords = NULL;
    }
    _ghRecordNum = 0;
}

#else

void ghAnalyerStart() {}
void ghAnalyerStop() {}
void ghSetMinTimeCallCost(uint64_t us) {
}
void ghSetMaxCallDepth(int depth) {
}
GHCallRecord *ghGetCallRecords(int *num) {
    if (num) {
        *num = 0;
    }
    return NULL;
}
void ghClearCallRecords() {}

#endif

/** 下面是Hook objc_msgSend的相關部分彙編源碼 .macro MethodTableLookup // push frame SignLR stp fp, lr, [sp, #-16]! mov fp, sp // save parameter registers: x0..x8, q0..q7 sub sp, sp, #(10*8 + 8*16) stp q0, q1, [sp, #(0*16)] stp q2, q3, [sp, #(2*16)] stp q4, q5, [sp, #(4*16)] stp q6, q7, [sp, #(6*16)] stp x0, x1, [sp, #(8*16+0*8)] stp x2, x3, [sp, #(8*16+2*8)] stp x4, x5, [sp, #(8*16+4*8)] stp x6, x7, [sp, #(8*16+6*8)] str x8, [sp, #(8*16+8*8)] // receiver and selector already in x0 and x1 mov x2, x16 bl __class_lookupMethodAndLoadCache3 // IMP in x0 mov x17, x0 // restore registers and return ldp q0, q1, [sp, #(0*16)] ldp q2, q3, [sp, #(2*16)] ldp q4, q5, [sp, #(4*16)] ldp q6, q7, [sp, #(6*16)] ldp x0, x1, [sp, #(8*16+0*8)] ldp x2, x3, [sp, #(8*16+2*8)] ldp x4, x5, [sp, #(8*16+4*8)] ldp x6, x7, [sp, #(8*16+6*8)] ldr x8, [sp, #(8*16+8*8)] mov sp, fp ldp fp, lr, [sp], #16 AuthenticateLR .endmacro */

複製代碼
相關文章
相關標籤/搜索