美文网首页营养文章python专题H5前端资料技术分享专题
[转载]iOS runtime实战,一次性解决调试火葬场的坑

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

作者: _兜兜转转_ | 来源:发表于2019-09-26 17:48 被阅读0次

    本文首发掘金,原文链接

    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的数据,导致统计数据会失真,

    image

    这种情况改怎么处理呢?

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

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

    image

    在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哦

    参考资料

    相关文章

      网友评论

        本文标题:[转载]iOS runtime实战,一次性解决调试火葬场的坑

        本文链接:https://www.haomeiwen.com/subject/zlkcuctx.html