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,把勾去掉就好了。
demo构造了在非主线程设置UILabel的text属性的情况,代码如下:
- (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会给出提示,暂停程序,并在在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_VERBOSE,将该环境变量置1,
再运行程序,发现Console出现了一些比较有意思的东西。
Console输出了所有被替换的类,总共替换有381个类,被替换的方法一共是11067个,低于这381个类所有方法的数之和17886。
那MTC是如何决定哪些类、哪些方法需要被替换呢?咱们按照如下顺序分析hopper给出的伪代码。
打印错误日志检测是否主线程调用决定对哪些API进行检测MTC发现错误的时候,会调用___ASSERT_API_MUST_BE_CALLED_FROM_MAIN_THREAD_FAILED__方法来打印当前的线程信息和该线程的栈信息。
检测函数也很直接,就是调用了pthread_main_np()这个posix线程的底层函数做的判断。如果发现不是主线程,就去调用___ASSERT_API_MUST_BE_CALLED_FROM_MAIN_THREAD_FAILED__报错了。
上面的注释已经比较清晰地说明了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就是需要这样的特性,需要在每次函数调用之前,先检查是否在主线程,然后再跳转真正的函数实现。
整个替换的流程如下:
在_initialize_trampolines的时候,注册了主线程检查的回调函数。在_add_trampoline的时候,对每个需要替换的函数都生成了trampoline的代码。在_addSwizzler中对函数实现做了替换。trampoline这种设计也被使用在部分操作系统的中断实现上面,就是因为其性能很好,可见苹果为了减少大规模方法替换对性能的影响,也是煞费苦心的。