最近随着iOS11的正式发布,手淘/天猫也开始逐步用Xcode 9开始编译。在调试过程中,很多同事发现经常报许多API会报线程使用错误的问题。摸索了下,发现是Xcode 9里面带上了一个叫libMainThreadChecker.dylib
的动态库,在运行时提供了主线程检查的功能,今天就从探究苹果的实现开始讲起。
0x1 苹果的实现
把苹果的动态库拖入hopper里面看看,基本上扫一眼以后,比较可疑的是__library_initializer
和__library_deintializer
。
我看反汇编,第一直觉就是猜,然后都试一把。
我们来看看其伪代码实现,可以分为几个部分来探究:
1.1 环境变量
从图中不难看出,libMainThreadChecker
的运行依赖于许多的环境变量,我们可以在Xcode
->Scheme
->Arguments
里面一个个输入这些变量进行测试,我发现比较重要的是MTC_VERBOSE
这个参数,使用后,可以输出究竟对于哪些类进行了线程监控。
...
Swizzling class: UIKeyboardEmojiCollectionViewCell
Swizzling class: UIKeyboardEmojiSectionHeader
Swizzling class: UIPrinterSetupPINScrollView
Swizzling class: UIPrinterSetupPINView
Swizzling class: UIPrinterSetupConnectingView
Swizzling class: UICollectionViewTableHeaderFooterView
Swizzling class: UIPrinterSetupDisplayPINView
Swizzling class: UIStatusBarMapsCompassItemView
Swizzling class: UIStatusBarCarPlayTimeItemView
Swizzling class: UIKeyboardCandidateBarCell
Swizzling class: UIKeyboardCandidateBarCell_SecondaryCandidate
Swizzling class: UIActionSheetiOSDismissActionView
Swizzling class: UIKeyboardCandidateFloatingArrowView
Swizzling class: UIKeyboardCandidateGridOverlayBackgroundView
Swizzling class: UIKeyboardCandidateGridHeaderContainerView
Swizzling class: UIStatusBarBreadcrumbItemView
Swizzling class: UIInterfaceActionGroupView
Swizzling class: UIKeyboardFlipTransitionView
Swizzling class: UIKeyboardAssistantBar
Swizzling class: UITextMagnifier
Swizzling class: UIKeyboardSliceTransitionView
Swizzling class: UIWKSelectionView
Swizzled 10717 methods in 384 classes.
可以看出,苹果会在启动前对于这些类进行所谓的线程监控。
1.2 逻辑
看完了输出,我们来看看其中的逻辑实现,如下所示:
CFAbsoluteTimeGetCurrent();
var_270 = intrinsic_movsd(var_270, xmm0);
*_indirect__main_thread_checker_on_report = dlsym(0xfffffffffffffffd, "__main_thread_checker_on_report");
if (objc_getClass("UIView") != 0x0) {
*_XXKitImage = dyld_image_header_containing_address(objc_getClass("UIView"));
*_CoreFoundationImage = dyld_image_header_containing_address(_CFArrayGetCount);
rax = objc_getClass("WKWebView");
rax = dyld_image_header_containing_address(rax);
*_WebKitImage = rax;
*_InlineCallsMachHeaders = *_XXKitImage;
*0x1ec3e8 = *_CoreFoundationImage;
*0x1ec3f0 = rax;
*___CATransaction = objc_getClass("CATransaction");
*___NSGraphicsContext = objc_getClass("NSGraphicsContext");
*_SEL_currentState = sel_registerName("currentState");
*_SEL_currentContext = sel_registerName("currentContext");
*_MyOwnMachHeader = dyld_image_header_containing_address(___library_initializer);
*_classesToSwizzle = CFArrayCreateMutable(0x0, 0x200, 0x0);
var_240 = objc_getClass("UIView");
_FindClassesToSwizzleInImage(*_XXKitImage, &var_240, 0x2);
if (*_WebKitImage != 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(*_WebKitImage, &var_230, 0x8);
}
rcx = CFArrayGetCount(*_classesToSwizzle);
if (rcx != 0x0) {
rax = 0x0;
var_278 = rcx;
do {
var_288 = rax;
rax = CFArrayGetValueAtIndex(*_classesToSwizzle, rax);
var_258 = rax;
rbx = objc_getClass(rax);
var_290 = dyld_image_header_containing_address(rbx);
var_230 = 0x0;
var_280 = rbx;
r14 = class_copyMethodList(rbx, &var_230);
if (var_230 != 0x0) {
rbx = 0x0;
do {
r13 = *(r14 + rbx * 0x8);
r12 = method_getName(r13);
r15 = sel_getName(r12);
if ((((((((((((((((*(int8_t *)r15 != 0x5f) && (dyld_image_header_containing_address(method_getImplementation(r13)) == var_290)) && (((*(int8_t *)_envIgnoreRetainRelease == 0x0) || (((strcmp(r15, "retain") != 0x0) && (strcmp(r15, "release") != 0x0)) && (strcmp(r15, "autorelease") != 0x0))))) && (((*(int8_t *)_envIgnoreDealloc == 0x0) || ((strcmp(r15, "dealloc") != 0x0) && (strcmp(r15, ".cxx_destruct") != 0x0))))) && (((*(int8_t *)_envIgnoreNSObjectThreadSafeMethods == 0x0) || ((((strcmp(r15, "description") != 0x0) && (strcmp(r15, "debugDescription") != 0x0)) && (strcmp(r15, "self") != 0x0)) && (strcmp(r15, "class") != 0x0))))) && (strcmp(r15, "beginBackgroundTaskWithExpirationHandler:") != 0x0)) && (strcmp(r15, "beginBackgroundTaskWithName:expirationHandler:") != 0x0)) && (strcmp(r15, "endBackgroundTask:") != 0x0)) && (strcmp(r15, "lockFocus") != 0x0)) && (strcmp(r15, "lockFocusIfCanDraw") != 0x0)) && (strcmp(r15, "lockFocusIfCanDrawInContext:") != 0x0)) && (strcmp(r15, "unlockFocus") != 0x0)) && (strcmp(r15, "openGLContext") != 0x0)) && (strncmp(r15, "webThread", 0x9) != 0x0)) && (strncmp(r15, "nsli_", 0x5) != 0x0)) && (strncmp(r15, "nsis_", 0x5) != 0x0)) {
if (*_userSuppressedClasses != 0x0) {
rax = CFStringCreateWithCStringNoCopy(0x0, var_258, 0x8000100, *_kCFAllocatorNull);
var_244 = CFSetContainsValue(*_userSuppressedClasses, rax) != 0x0 ? 0x1 : 0x0;
CFRelease(rax);
}
else {
var_244 = 0x0;
}
if (*_userSuppressedSelectors != 0x0) {
rax = CFStringCreateWithCStringNoCopy(0x0, r15, 0x8000100, *_kCFAllocatorNull);
var_250 = rax;
if (CFSetContainsValue(*_userSuppressedSelectors, rax) != 0x0) {
var_244 = 0x1;
}
CFRelease(var_250);
}
if (*_userSuppressedMethods != 0x0) {
rax = CFStringCreateWithFormat(0x0, 0x0, @"-[%s %s]");
var_250 = CFSetContainsValue(*_userSuppressedMethods, rax);
CFRelease(rax);
rax = var_250 | var_244;
if (rax == 0x0) {
_addSwizzler(r13, r12, var_258, r15, 0x1);
}
else {
*_userSuppressionsCount = *_userSuppressionsCount + 0x1;
}
}
else {
if (var_244 != 0x0) {
*_userSuppressionsCount = *_userSuppressionsCount + 0x1;
}
else {
_addSwizzler(r13, r12, var_258, r15, 0x1);
}
}
}
rbx = rbx + 0x1;
} while (rbx < var_230);
}
_objc_flush_caches(var_280);
free(r14);
rax = var_288 + 0x1;
rcx = var_278;
} while (rax != rcx);
}
*_totalSwizzledClasses = rcx;
if (*(int8_t *)_envVerbose != 0x0) {
rdx = *_totalSwizzledMethods;
fprintf(*___stderrp, "Swizzled %zu methods in %zu classes.\n", rdx, rcx);
}
代码乍一看很多,其实逻辑非常简单,概述如下:
- 通过获取
UIView的类实体
(不理解类实体的去看runtime)所在的地址来反推所在的image(二进制产物,基本是动态库),这里基本能猜测是UIKit
。 - 从
UIKit
中获取所有继承自UIView
和UIApplication
的类及其子类(这也是你为什么会在刚刚上文提到的输出中发现UIIBApplication
这种不知道啥类的原因),过滤到带_
的私有类,然后对剩下的类的所有的方法进行Swizzle。 - 对于需要Swizzle的方法,要额外判断是不是真正属于
UIKit
这个动态库的。 比如我们在调试的时候,Xcode会加载libViewDebugging.dylib
等不会用于用于线上的动态库,里面会给UIView
填上很多奇奇怪怪的方法。 过滤如下的方法,以及以
nsli_
和nsis_
开头的方法。retain release autorelease .cxx_destruct description debugDescription class self beginBackgroundTaskWithExpiratonHandler beginBackgroundTaskWithName:expirationHandler: endBackgroundTask: opneGLContext: lockFocusIfCanDrawInContext: lockFocus lockFocusIfCanDraw unlockFocus
可选,如果还要检查
WebKit
相关的方法,还可以Hook如下这些类的子类:WKWebView WKWebsiteDataStore WKUserScript WKUserContentController WKScriptMessage WKProcessPool WKProcessGroup WKContentExtensionStore
0x2 自己实现
当时看到这,关于苹果的实现我觉得实在是太简单了,即使不用私有API,结合现在Github上的轮子我自己造一个估计1、2个小时就解决了。现在回想起来,自己还是too simple, sometimes native
大致代码获取UIKit
中UIView
和UIApplication
所有子类的代码如下:
NSArray *findAllUIKitClasse()
{
static NSMutableArray *viewClasses = nil;
if (!viewClasses) return classes;
uint32_t image_count = _dyld_image_count();
for (uint32_t image_index = 0; image_index < image_count; image_index++) {
const my_macho_header *mach_header = (const my_macho_header *)_dyld_get_image_header(image_index);
const char *image_name = _dyld_get_image_name(image_index);
NSString *imageName = [NSString stringWithUTF8String:image_name];
if ([imageName hasSuffix:@"UIKit"]) {
unsigned int count;
const char **classes;
Dl_info info;
dladdr(mach_header, &info);
classes = objc_copyClassNamesForImage(info.dli_fname, &count);
for (int i = 0; i < count; i++) {
const char *className = (const char *)classes[i];
NSString *classname = [NSString stringWithUTF8String:className];
if ([classname hasPrefix:@"_"]) {
continue;
}
Class cls = objc_getClass(className);
Class superCls = cls;
bool isNeedChild = NO;
while (superCls != [NSObject class]) {
if (superCls == NSClassFromString(@"UIView") || superCls == NSClassFromString(@"UIApplication")) {
isNeedChild = YES;
break;
}
superCls = class_getSuperclass(superCls);
}
if (isNeedChild) {
// 备注:需要在这同时对这个类的方法进行Hook。
[viewClasses addObject:cls];
}
}
break;
}
return viewClasses;
}
2.1 现有方案Hook的缺陷
到这,我们就只差把这些类的方法都Hook掉就行了。传统的Method Swizzling
肯定不行,那样我们需要对每个方法对应实现一个新的方法进行替换,工作量太大。所以我们需要一个思路能够中心重定向整个过程。
之前跟着网易iOS大佬刘培庆学习iOS的时候,了解到了AnyMethodLog
,听说能监控所有类所有方法的执行,于是我就直接套用了这个框架,嘿嘿,使用起来真方便,看起来大功告成了,Build & Run。
卧槽,怎么运行了就启动崩了,一脸懵逼。
没事,我换个开源库BigBang
改改。卧槽,还是崩了。这下必须要开下源码分析下原因了。
从AnyMethodLog
的实现来看,如下所示:
BOOL qhd_replaceMethod(Class cls, SEL originSelector, char *returnType) {
Method originMethod = class_getInstanceMethod(cls, originSelector);
if (originMethod == nil) {
return NO;
}
const char *originTypes = method_getTypeEncoding(originMethod);
IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
if (qhd_isStructType(returnType)) {
//Reference JSPatch:
//In some cases that returns struct, we should use the '_stret' API:
//http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html
//NSMethodSignature knows the detail but has no API to return, we can only get the info from debugDescription.
NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:originTypes];
if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) {
msgForwardIMP = (IMP)_objc_msgForward_stret;
}
}
#endif
IMP originIMP = method_getImplementation(originMethod);
if (originIMP == nil || originIMP == msgForwardIMP) {
return NO;
}
//把原方法的IMP换成_objc_msgForward,使之触发forwardInvocation方法
class_replaceMethod(cls, originSelector, msgForwardIMP, originTypes);
//把方法forwardInvocation的IMP换成qhd_forwardInvocation
class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)qhd_forwardInvocation, "v@:@");
//创建一个新方法,IMP就是原方法的原来的IMP,那么只要在qhd_forwardInvocation调用新方法即可
SEL newSelecotr = qhd_createNewSelector(originSelector);
BOOL isAdd = class_addMethod(cls, newSelecotr, originIMP, originTypes);
if (!isAdd) {
DEV_LOG(@"class_addMethod fail");
}
return YES;
}
// 中心重定向函数
void qhd_forwardInvocation(id target, SEL selector, NSInvocation *invocation) {
NSArray *argList = qhd_method_arguments(invocation);
SEL originSelector = invocation.selector;
NSString *originSelectorString = NSStringFromSelector(originSelector);
[invocation setSelector:qhd_createNewSelector(originSelector)];
[invocation setTarget:target];
[invocation invoke];
}
作者的意图比较简单,主要可以概述为如下几点:
- 把每个类的
forwardInvocation
,替换成自己实现的一个C函数。 - 把需要Hook原来
selector
获取的method
的IMP指向objc_msgForward
,通过其触发消息转发,也就是触发forwardInvocation; - 对每个需要重定向的
selector
,生成一个特定的格式的新selector
,将其IMP指向原来method
的IMP。 - 对于刚刚重定向的C函数,通过
NSInvocation
获取要调用的target和selector,再次将这个selector
生成特定格式的新selector
,反射调用。
为啥能把OC的函数
forwardInvocation
换成C函数,原因就在于只要补上OC函数隐式的前两个参数self, selector
,让其的函数签名一致即可。
读到这,看起来没有啥问题吧?为什么会崩溃呢!!
原因在于这种调用方式,缺少了super上下文。
假设我们现在对UIView
、UIButton
都Hook了initWithFrame:
这个方法,在调用[[UIView alloc] initWithFrame:]
和[[UIButton alloc] initWithFrame:]
都会定向到C函数qhd_forwardInvocation
中,在UIView
调用的时候没问题。但是在UIButton
调用的时候,由于其内部实现获取了super initWithFrame:
,就产生了循环定向的问题。
问题本质的原因是,由于我们对于父类、子类同名的方法都换成了同一个IMP,那么不论是走objc_msgSend
抑或是objc_msgSendSuper2
,获取到的IMP都是一致的。而在Hook之前,objc_msgSendSuper2
拿到的是super_imp, objc_msgSend
拿到是imp,从而不会有问题。
2.2 基于桥的全量Hook方法
好,上面的一个小节我们说,如果我们把所有方法都重定向到一个IMP上的时候,就会丧失关于继承关系之间的父子上下文关系,导致重定向循环。所以,我们需要一个思路,能够正确解决上下文的问题。
首先我们来回顾下runtime的消息转发机制:
1. 调用resolveInstanceMethod:方法 (或 resolveClassMethod:)。允许用户在此时为该 Class 动态添加实现。如果有实现了,则调用并返回YES,那么重新开始objc_msgSend流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。如果仍没实现,继续下面的动作。
2. 调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作。注意,这里不要返回 self ,否则会形成死循环。
3. 调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。
4. 调用forwardInvocation:方法,将第3步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了,并返回非ni。
5. 调用doesNotRecognizeSelector: ,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。
对于我们来说,我们至少要在第四步之前(确切的是第三步之前),我们就要保留好super
上下文。一旦到了forwardInvocation
函数,留给我们的又只有self
这样的残缺信息了。
哎,我就是卡在这思考了一天,最终我想出了一个思路。
- 提供一个桩
WZQMessageStub
,这个桩保留了class和selector,拼接成不一样的函数名,这样就能区分UIButton
和UIView
的同名initWithFrame:
方法,因为不同的selector找到的IMP肯定不一样。 - 在
NSObject
里面实现forwardingTargetForSelector
,在消息转发的时候指定把消息全部转发给WZQMessageStub
。 WZQMessageStub
实现methodSignatureForSelector
和forwardInvocation:
方法,承担真正的方法反射调用的职责。
好,思路确定了,难点还剩一个。对于forwardingTargetForSelector
这个函数来说,能拿到的参数也是target
和selector
。在super
和self
调用场景下,这个参数毫无价值,因此我们需要从selector
上着手。如果不做任何改变,我们这里拿到的selector
肯定是诸如initWithFrame:
的old selector
,所以我们需要在这之前桥一下,可以按照下述流程理解:
每个方法置换到不同的IMP桥上 -> 从桥上反推出当前的调用关系(class和selector)-> 构造一个中间态新名字 -> forwardingTargetForSelector(self, 中间态新名字)
OK,大功告成。具体桥的实现我待会再单独开篇博客讲一讲。
嘿嘿,看起来很简单的任务也学习到了不少新知识。一会把代码开源到Github上。
0x3 遗留问题
我在开启Main Thread Chekcer
后,build了一次产物,但是在通过Mach-O
文件中Load Commands
部分的时候,却没有发现libMainThreadChecker.dylib
的踪影,如下所示:
符号断点dlopen
也并没有发现这个动态库调用的踪影,所以非常好奇苹果是怎么加载这个动态库的,有大佬知道请赐教。