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

本文首发掘金,原文连接ios

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

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

原理

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

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

图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又恢复到原来的样子。post

+(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哦

参考资料

相关文章
相关标签/搜索