閱讀232 返回首頁    go 阿裏雲 go 技術社區[雲棲]


搞一搞Main Thread Checker

Main Thread Checker(後麵簡稱MTC)簡單來說就是一個適用於Swift和C語言的小工具。當必須在主線程執行的API在非主線程被調用的時候, MTC會報錯並暫停程序執行。該類API包括
AppKit的接口、**UIKit的接口**和**其他需要在主線程執行的API**等。

MTC的原理官網也說的比較明白了。在App啟動的時候,加載動態庫——**libMainThreadChecker.dylib**,每個裝了Xcode 9的人都能在/Applications/Xcode.app/Contents/Developer/usr/lib/目錄下找到該動態庫。這個動態庫**替換了所有應該在主線程調用的方法**,替換後的方法會在函數執行之前先檢查當前執行的線程是否是主線程,如果不是的話就報錯。

因為MTC是通過動態庫的方式來實現的,所以想要開啟該功能隻要鏈接進該動態庫就可以了,完全不需要重新編譯工程,方便的不要不要的。

更屌的是,其對性能的影響可以直接忽略不計,所以Xcode 9是**默認開啟MTC的**。

如何開啟MTC

如何開啟MTC

如果想要關閉MTC,把勾去掉就好了。

DEMO

demo構造了在非主線程設置UILabeltext屬性的情況,代碼如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    UILabel *label = [[UILabel alloc] init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [label setText:@"setText here will cause Xcode to pause!"];
    });
    [self.view addSubview:label];
}

下圖是開啟MTC的結果:
MTC報錯界麵

當發現問題的時候,MTC會給出提示,暫停程序,並在在Console裏麵給出了詳細的棧信息,讓開發者可以及時發現並這類問題。

非主線調用的修複也比較簡單,這裏給出一種可能的解決方案。

    if ([NSThread isMainThread]) {
      block();

    } else {
      dispatch_sync(dispatch_get_main_queue(), block);
    }

不過,可能Xcode 9 beta版的緣故,MTC還存在不少問題,已知發現的有:

  • 存在較多誤報,比如自己針對UIView的一些線程安全的擴展就會被誤判。
  • [label performSelectorInBackground:@selector(setText:) withObject:@"setText here will cause Xcode to pause!"];是不會被檢測出來的。

如果僅希望在實際工程中使用MTC,看完上麵的信息就可以了,文章的剩下部分是對實現原理的探索,有興趣的讀者可以花點時間一起探究。

反向工程

因為對libMainThreadChecker.dylib的實現感興趣,就花點時間做了反向工程,工具以hopper為主,ida為輔。因為篇幅限製,對工具的使用說明就不囉嗦了。

通過hopper的分析,發現MTC定義了一係列的環境變量。

MTC的環境變量

這裏麵我們比較關心的是MTC_VERBOSE,將該環境變量置1,

設置MTC_VERBOSE

再運行程序,發現Console出現了一些比較有意思的東西。

Console輸出了所有被替換的類,總共替換有**381個類**,被替換的方法一共是**11067個**,低於這381個類所有方法的數之和**17886**。

被替換的所有類

那MTC是如何決定哪些類、哪些方法需要被替換呢?咱們按照如下順序分析hopper給出的偽代碼。

  1. 打印錯誤日誌
  2. 檢測是否主線程調用
  3. 決定對哪些API進行檢測

打印錯誤日誌

int ___ASSERT_API_MUST_BE_CALLED_FROM_MAIN_THREAD_FAILED__(int arg0) {
    ......
    // 打印當前線程信息
    rax = __snprintf_chk(r14, sign_extend_64(r15), 0x0, 0xffffffffffffffff, "PID: %d, TID: %llu, Thread name: %s, Queue name: %s, QoS: %d\n", var_4B8, var_4D8, r13, var_4C0, rbx);
    if (r15 > 0x0) {
        // 打印當前線程堆棧信息
        rax = __snprintf_chk(r14, sign_extend_64(r15), 0x0, 0xffffffffffffffff, "Backtrace:\n");
        ......
    }
    ......
}

MTC發現錯誤的時候,會調用___ASSERT_API_MUST_BE_CALLED_FROM_MAIN_THREAD_FAILED__方法來打印當前的線程信息和該線程的棧信息。

檢測是否主線程調用

void _checker_c(int arg0, int arg1) {
    rbx = arg1;
    r14 = arg0;
    if (*(int8_t *)_envPrintSelectorStats != 0x0) {
            *(r14 + 0x28) = *(r14 + 0x28) + 0x1;
    }
    // 是否是主線程檢查
    if (pthread_main_np() == 0x0) goto loc_291c7;
    loc_291c7:
    ......
    loc_292b6:
    if (*(int8_t *)__tlv_bootstrap(_in_report_callback) == 0x0) {
            rbx = __tlv_bootstrap(_in_report_callback);
            *(int8_t *)rbx = 0x1;
            ___ASSERT_API_MUST_BE_CALLED_FROM_MAIN_THREAD_FAILED__(*(r14 + 0x20));
            *(int8_t *)rbx = 0x0;
    }
    return;
}

檢測函數也很直接,就是調用了pthread_main_np()這個posix線程的底層函數做的判斷。如果發現不是主線程,就去調用___ASSERT_API_MUST_BE_CALLED_FROM_MAIN_THREAD_FAILED__報錯了。

決定對哪些API進行檢測

if (objc_getClass("UIView") != 0x0) {
        ......

        // 注冊檢測函數
        _initialize_trampolines(_checker_c);

        ......

        // 找到UIKit或者APPKit中的所有需要檢測的類
        *var_240 = objc_getClass("UIView");
        *(var_240 + 0x8) = objc_getClass("UIApplication");
        _FindClassesToSwizzleInImage(r12, var_240, 0x2);

        // 找到WebKit中所有需要檢測的類
        if (r14 != 0x0) {
              *var_230 = objc_getClass("WKWebView");
              *(var_230 + 0x8) = objc_getClass("WKWebsiteDataStore");
              *(var_230 + 0x10) = objc_getClass("WKUserScript");
              *(var_230 + 0x18) = objc_getClass("WKUserContentController");
              *(var_230 + 0x20) = objc_getClass("WKScriptMessage");
              *(var_230 + 0x28) = objc_getClass("WKProcessPool");
              *(var_230 + 0x30) = objc_getClass("WKProcessGroup");
              *(var_230 + 0x38) = objc_getClass("WKContentExtensionStore");
              _FindClassesToSwizzleInImage(r14, var_230, 0x8);
        }
        rcx = CFArrayGetCount(*_classesToSwizzle);
        if (rcx != 0x0) {
          ......
                    // 通過runtime找出一個類下所有的方法進行替換,這就是自己擴展的線程安全的函數會被誤報的原因
                    r14 = class_copyMethodList(rax, 0x0);
                    if (0x0 != 0x0) {
                          rbx = 0x0;
                          do {
                                r13 = *(r14 + rbx * 0x8);
                                r15 = method_getName(r13);
                                r12 = sel_getName(r15);
                                if (*(int8_t *)r12 != 0x5f) {
                                // 過濾掉一些不需要檢測的方法,包括retain、release、autorelease、
                                // description、debugDescription、self、class、beginBackgroundTaskWithExpirationHandler、
                                // beginBackgroundTaskWithName:expirationHandler:、endBackgroundTask:
                                if (/*不需要檢測的方法*/) {
                                      ......
                                      // 替換方法實現,進行檢測
                                      _addSwizzler(r13, r15, var_258, r12, 0x1);
                                    ......
                                }
                                ......
                          } while (rax != rcx);
          ......
            // 如果設置了MTC_VERBOSE,打印日誌
            if (*(int8_t *)_envVerbose != 0x0) {
                    rdi = *___stderrp;
                    rdx = *_totalSwizzledMethods;
                    fprintf(rdi, "Swizzled %zu methods in %zu classes.\n", rdx, rcx);
            }
            ......
          }
        }
      }
    }

void _FindClassesToSwizzleInImage(int arg0, int arg1, int arg2) {
    ......
    // 獲取該庫下的所有類
    rax = getsectiondata(arg0, "__DATA", "__objc_classlist", var_48);
    var_38 = rax;
    if (rax == 0x0) {
            rax = getsectiondata(var_40, "__DATA_CONST", "__objc_classlist", var_48);
            var_38 = rax;
            if (rax != 0x0) {
                    rax = var_48 >> 0x3;
                    var_2C = rax;
            }
            else {
                    var_2C = 0x0;
                    // 拷貝所有的類
                    var_38 = objc_copyClassList(var_2C);
                    rax = *(int32_t *)var_2C;
            }
    }
    ......
  }

上麵的注釋已經比較清晰地說明了MTC是遍曆了UIKit或者APPKit,以及WebKit的所有類,然後再遍曆每個類的所有方法進行替換,不過是排除了為數不多的幾個方法而已。是不是這一切都看起來很簡單呢?

替換實現

DEMO階段我們提到過,MTC對性能的損耗是很小的,替換了11067個方法隻會增加1-2%的CPU損耗和<0.1的啟動時間影響,通過_initialize_trampolines以及_addSwizzler的偽代碼可以知道,這一切都跟trampoline有關係。trampoline為何能這麼屌呢?

// 傳入的arg0就是checker_c函數
int _initialize_trampolines(int arg0) {
    *_registered_callback = arg0;
    *_first_trampoline = ___trampolines;
    return ___trampolines;
}

// arg0 函數方法體,類型Method
// arg1 函數selector,類型SEL
// arg2 函數名字,類型char *
// arg3 函數所在類,類型Class
// arg4 是否快速替換,類型BOOL
int _addSwizzler(int arg0, int arg1, int arg2, int arg3, int arg4) {
    // 根據需要替換的函數生成相應的trampoline代碼
    rbx = _add_trampoline(method_getImplementation(r13), var_230);
    r12 = _trampoline_address_from_index(rbx);
    *(_trampoline_data_from_index(rbx, var_230, 0x0, 0x200, "-[%s %s]", arg2) + 0x10) = r13;
    *(_trampoline_data_from_index(rbx, var_230, 0x0, 0x200, "-[%s %s]", arg2) + 0x18) = r14;
    // 將需要替換的函數替換成trampoline實現
    if (arg4 != 0x0) {
            _swizzleImplementationFast(r14, r13, r12);
    }
    else {
            method_setImplementation(r13, r12);
    }
    *_totalSwizzledMethods = *_totalSwizzledMethods + 0x1;
    rax = *___stack_chk_guard;
    if (rax != var_30) {
            rax = __stack_chk_fail();
    }
    return rax;
}

int _add_trampoline(int arg0, int arg1) {
    r14 = *_trampolines_used;
    *_trampolines_used = r14 + 0x1;
    *(r14 * 0x38 + _data) = r14;
    *(r14 * 0x38 + 0x2b3a8) = arg0;
    *(r14 * 0x38 + 0x2b3c0) = strdup(arg1);
    *(r14 * 0x38 + 0x2b3d0) = 0x0;
    *(r14 * 0x38 + 0x2b3c8) = 0x0;
    rax = r14;
    return rax;
}

GCC對trampoline的描述對我們理解trampoline比較有幫助。

A trampoline is a small piece of code that is created at run time when the address of a nested function is taken. It normally resides on the stack, in the stack frame of the containing function. These macros tell GCC how to generate code to allocate and initialize a trampoline.

The instructions in the trampoline must do two things: load a constant address into the static chain register, and jump to the real address of the nested function

GCC告訴我們,trampoline就是根據一個函數的地址創建一小段代碼,這一小段代碼就給了程序機會去處理一些事情,然後再跳轉到真正的函數。

MTC就是需要這樣的特性,需要在每次函數調用之前,先檢查是否在主線程,然後再跳轉真正的函數實現。

整個替換的流程如下:

  1. _initialize_trampolines的時候,注冊了主線程檢查的回調函數。
  2. _add_trampoline的時候,對每個需要替換的函數都生成了trampoline的代碼。
  3. _addSwizzler中對函數實現做了替換。

trampoline這種設計也被使用在部分操作係統的中斷實現上麵,就是因為其性能很好,可見蘋果為了減少大規模方法替換對性能的影響,也是煞費苦心的。

參考文檔

最後更新:2017-06-30 11:03:12

  上一篇:go  阿裏大幅增持Lazada 東南亞6億用戶進入天貓版圖
  下一篇:go  Immutable & Redux in Angular Way