搞一搞Main Thread Checker

简介: Main Thread Checker(后面简称MTC)简单来说就是一个适用于Swift和C语言的小工具。当必须在主线程执行的API在非主线程被调用的时候, MTC会报错并暂停程序执行。该类API包括 **AppKit的接口**、**UIKit的接口**和**其他需要在主线程执行的API**等。 MTC的原理官网也说的比较明白了。在App启动的时候,加载动态库——**libMainThre

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这种设计也被使用在部分操作系统的中断实现上面,就是因为其性能很好,可见苹果为了减少大规模方法替换对性能的影响,也是煞费苦心的。

参考文档

目录
相关文章
|
3月前
|
Kubernetes Go 数据库
分享48个Go源码,总有一款适合您
分享48个Go源码,总有一款适合您
53 0
|
3月前
|
前端开发 JavaScript 对象存储
【面试题】面试官为啥总是让我们手写call、apply、bind?
【面试题】面试官为啥总是让我们手写call、apply、bind?
|
9月前
|
Go
Go中两个Nil可能不相等吗,面试官说回家等通知
Go中两个Nil可能不相等吗,面试官说回家等通知
|
9月前
|
安全 Go
大白话讲讲 Go 语言的 sync.Map(二)
上一篇文章《大白话讲讲 Go 语言的 sync.Map(一)》讲到 entry 数据结构,原因是 Go 语言标准库的 map 不是线程安全的,通过加一层抽象回避这个问题……
86 1
|
9月前
|
存储 程序员 Go
大白话讲讲 Go 语言的 sync.Map(一)
在讲 sync.Map 之前,我们先说说什么是 map(映射)。我们每个人都有身份证号码,如果我需要从身份证号码查到对应的姓名,用 map 存储是非常合适的……
96 1
|
9月前
|
存储 缓存 Go
sync.singleflight 到底怎么用才对?
sync.singleflight 到底怎么用才对?
58 0
|
JavaScript 前端开发 API
js基础-面试官想知道你有多理解call,apply,bind?[不看后悔系列]
函数原型链中的 apply,call 和 bind 方法是 JavaScript 中相当重要的概念,与 this 关键字密切相关,相当一部分人对它们的理解还是比较浅显,所谓js基础扎实,绕不开这些基础常用的API,这次让我们来彻底掌握它们吧! 目录 call,apply,bind的基本介绍 call/apply/bind的核心理念:借用方法 call和apply的应用场景 bind的应用场景 中高级面试题:手写call/apply、bind call,apply,bind的基本介绍 语法: fun.call(thisArg, param1, param2, ...) fun.apply(
137 0
js基础-面试官想知道你有多理解call,apply,bind?[不看后悔系列]
|
Oracle Java 关系型数据库
聊一聊多线程的 run() 和 start(),挖一挖start0
聊一聊多线程的 run() 和 start(),挖一挖start0
119 0
聊一聊多线程的 run() 和 start(),挖一挖start0
|
负载均衡 算法 Java
bthread源码剖析(二): 工作窃取与TaskGroup的run_main_task()
上一篇文章,介绍了TaskControl(简称TC)的初始化逻辑、worker的基本概念,并引出了TaskGroup(简称TG)的主要函数:run_main_task()。在谈run_main_task()之前,我们先看一下TG的几个主要成员。
344 0
|
Java
Continue与Break在使用过程中的爱恨情仇| Java Debug 笔记
Continue与Break在使用过程中的爱恨情仇| Java Debug 笔记
82 0