iOS runtime实战,一次性解决调试火葬场的坑

2,337 阅读6分钟

本文首发掘金,原文链接

iOS runtime实战,一次性解决调试火葬场的坑

说起来这个黑魔法,还是几年前道听途说的一个概念,完全不懂这个到底是做什么的,这边文章就是学习中的笔记,也是系列教程的第一篇,主要是理解黑魔法的运作原理,并在实战中运用,使用中要注意的地方。

原理

系统中查找IMP是根据SEL的,而且他们是一一对应的, 首先,让我们通过两张图片来了解一下Method Swizzling的实现原理

系统中的原来的对应关系:

图1
黑魔法使用之后的关系:

图2
上边图一中,SEL1中对应的IMP1SEL2对应的是IMP2,因为业务需要,我们将SEL2对应的IMP2和我们新增的SEL3对应的IMP3互相交换,交换之后则是如图2所示。

运用

Method Swizzling使用

交换method函数

OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);

我们测试验证一下理论:

+(void)load{
	[UIViewController exchange];
}
+ (void)exchange{
	Method m1 = class_getInstanceMethod(UIViewController.class, @selector(viewDidLoad));
	Method m2 = class_getInstanceMethod(self, @selector(fy_viewDidload));
	NSLog(@"viewDidLoad excheng before:%p",method_getImplementation(m1));
	NSLog(@"fy_viewDidload excheng before:%p",method_getImplementation(m2));
	if (!class_addMethod(self, @selector(fy_viewDidload), method_getImplementation(m2), method_getTypeEncoding(m2))) {
		method_exchangeImplementations(m1, m2);
	}
	NSLog(@"viewDidLoad excheng after:%p",method_getImplementation(m1));
	NSLog(@"fy_viewDidload excheng after:%p",method_getImplementation(m2));
}

输出:

viewDidLoad     excheng before:0x10a74adf9
fy_viewDidload  excheng before:0x106ca5040
viewDidLoad     excheng after:0x106ca5040
fy_viewDidload  excheng after:0x10a74adf9

可以看出来,交换之后fy_viewDidloadviewDidLoadIMP是交换了。 当我们重复调用2次,应该IMP又恢复到原来的样子。

+(void)load{
	[UIViewController exchange];
	[UIViewController exchange];
}
//交换第一次
viewDidLoad     excheng before:0x111626df9
fy_viewDidload  excheng before:0x10db81040
viewDidLoad     excheng after:0x10db81040
fy_viewDidload  excheng after:0x111626df9
//交换第二次
viewDidLoad     excheng before:0x10db81040
fy_viewDidload  excheng before:0x111626df9
viewDidLoad     excheng after:0x111626df9
fy_viewDidload  excheng after:0x10db81040

可以看出来,IMP又被换回去了。 我们在+load中调用,原因是+load方法是只会执行一次,具有线程安全的,所以不用考虑并发问题。在编译阶段load是根据文件先后顺序编译的,所以我们可以把交换文件放到第一个位子。 那么我们看一下系统加载文件的顺序:

加载顺序

就拿我们页面统计来说,这个而需求很多公司都常见,有些sdk是在ViewController基类中的viewDidload中调用统计函数。那么我们该如何做一个对业务无侵入的代码呢?

那么我们在ViewController新建Category,然后在+load中实现viewDidloadfy_viewDidload的交换,则在fy_viewDidload可以添加统计代码。

static NSMutableSet *set;

+(void)initialize{
    [self fy_countViewDidLoad];
}
//统计其他的子类的viewDidLoad方法时长
+ (void)fy_countViewDidLoad{
    if (set== nil) {
        set = [[NSMutableSet alloc]init];
    }
    //UIViewcontroller的子类统计
    if ([self isSubclassOfClass:UIViewController.class] &&
        self != UIViewController.class) {
        if ([set containsObject:self]) {
            return;
        }else{
            [set addObject:self];
        }
        SEL sel = @selector(viewDidLoad);
        Method m1 = class_getInstanceMethod(self, @selector(viewDidLoad));
        IMP imp1 = method_getImplementation(m1);
        // id,SEL 必须传,否则到了执行imp1Func(i,s);内部的id是nil,导致函数无法执行。
        void(*imp1Func)(id,SEL) = (void*)imp1;//imp1原始方法地址
        void (^block)(id,SEL)  = ^(id i,SEL s){
        //code here
            printf("开始\n");
            NSDate *date =[NSDate new];
            imp1Func(i,s);
            NSLog(@"%@ time:%d", NSStringFromClass(self),(int)[[NSDate date] timeIntervalSinceDate:date]);
            printf("结束\n");
        };
        IMP imp2 = imp_implementationWithBlock(block);
        class_replaceMethod(self, sel, imp2, method_getTypeEncoding(m1));
    }
}

首先记录原本的函数imp,使用class_replaceMethod交换selimp使sel指向新的block,执行sel会执行blockimp,不走转发消息的路径,性能更高。

在看似牛逼的代码,其实隐藏着更大的漏洞,当B继承于A,A继承于UIViewController,B自己实现了initialize则B则漏掉了统计。另外A的统计数据会夹杂着B的数据,导致统计数据会失真,

这种情况改怎么处理呢?

C viewDidload会执行B,当B viewDidload会执行A,其实从子类会重复统计了父类

方案作出少许改动即可解决这个问题。

在A的子类A2中统计A的加载次数,在B2中统计B的加载次数,在C2中统计C的加载次数,可以做到精准统计。

关键代码

//B2
[self addObserver:[YQKVOObserver shared] forKeyPath:kUniqueFakeKeyPath options:NSKeyValueObservingOptionNew context:nil];
    
    // Setup remover of KVO, automatically remove KVO when VC dealloc.
    YQKVORemover *remover = [[YQKVORemover alloc] init];
    remover.target = self;
    remover.keyPath = kUniqueFakeKeyPath;
    objc_setAssociatedObject(self, &kAssociatedRemoverKey, remover, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    // NSKVONotifying_ViewController
    Class kvoCls = object_getClass(self);
    
    class_addMethod(kvoCls, @selector(viewDidLoad), (IMP)fy_viewDidLoad, originViewDidLoadEncoding);

static void fy_viewDidLoad(UIViewController *kvo_self, SEL _sel) {
    Class kvo_cls = object_getClass(kvo_self);
    Class origin_cls = class_getSuperclass(kvo_cls);
    IMP origin_imp = method_getImplementation(class_getInstanceMethod(origin_cls, _sel));
    assert(origin_imp != NULL);
    
    void (*func)(UIViewController *, SEL) = (void (*)(UIViewController *, SEL))origin_imp;
    
    CFAbsoluteTime beginTime = CFAbsoluteTimeGetCurrent();
    
    func(kvo_self, _sel);
    
    CFAbsoluteTime endTime = CFAbsoluteTimeGetCurrent();
    
    //这里统计加载时间长度和次数

}

在需要统计类的子类中统计,的确是一种不错的选择,精准统计时间和次数,而且不影响性能

runtime一时爽,一直用一直爽,调试火葬场

用的时候感觉很爽,可以做这么🐂的事,但是其他同学来调试的时候,出问题了也非常的难找。 我做这个工具可以记录runtime的黑魔法日志,使用起来也很简单。

platform :ios, '9.0'
use_frameworks!
target 'MyApp' do
	pod 'FYMSL'
end

函数生命周期和耗时操作回调

// 每个函数的回调,独立可以单独设置的。
FYVCcall *cll = [FYVCcall shared];
[cll setCallback:^(CFAbsoluteTime loadTime, UIViewController * _Nonnull vc, NSString * _Nonnull funcName,NSString *str) {
	const char *clsName = NSStringFromClass(vc.class).UTF8String;
	printf("cls:%s func:%s %f %s \n",clsName,funcName.UTF8String,loadTime,str.UTF8String);
}];

输出日志:

cls:ViewController func:viewDidLoad 2.001058 2019 09-03 16:25:45 
cls:ViewController func:viewWillAppear: 0.000000 2019 09-03 16:25:45 
cls:ViewController func:viewDidAppear: 0.000000 2019 09-03 16:25:45 

查看MethodSwizzling总记录

NSLog(@"%@",[FYNodeManger shared].description);


↴:替换   ⇄ :交换

举个例子:
例子1:test2 交换到test1,然后交换到test3,最终imp是0x105c6c630

⇄ | + test2 -> test1 -> test3 -> imp:0x105c6c630

例子2:test1 的imp替换到0x105c6c660,然后又替换到0x105c6c690,又替换到0x105c6c600,
又交换到了test2,又交换到了test3->又交换到了test4

↴ | + test1 -> imp:0x105c6c660
↴ | +   test1 -> imp:0x105c6c690 
↴ | +     test1 -> imp:0x105c6c600
⇄ | +       test1 -> test2 -> imp:0x105c6c600
⇄ | +         test1 -> test3 -> imp:0x105c6c630
⇄ | +           test1 -> test4 -> imp:0x105c6c660

查看单一SEL单一记录

	NSLog(@"\n%@",[FYNodeManger objectForSEL:@"test1"]);
  
↴ | + test1 -> imp:0x10b5de550 
↴ | +   test1 -> imp:0x10b5de580 
↴ | +     test1 -> imp:0x10b5de4f0 
⇄ | +       test1 -> test2 -> imp:0x10b5de4f0 
⇄ | +         test1 -> test3 -> imp:0x10b5de520 
⇄ | +           test1 -> test4 -> imp:0x10b5de550

喜欢的给个start哦

参考资料