欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Method Swizzling与JSPatch探究(二)

程序员文章站 2022-04-10 20:59:46
...

以下笔记整理于2017-03-22
当时作为无埋点预研的学习整理,由于小组是客户端和前端混合的小组,所以下面尽可能以偏简单的文字在组内分享。
第一部分地址为:Method Swizzling与JSPatch探究(一)


4、兼容性

以hook UIViewControllerviewWillAppear的方法为例

4.1 仅method Swizzling, 无JSPatch
`ViewController:UIViewController`

- (void)viewWillAppear:(BOOL)animated {
    NSLog(@"HELLO 01");
    [super viewWillAppear:animated];
    NSLog(@"HELLO 02");
}

UIViewController+MLPF.m
- (void)xxx_viewWillAppear:(BOOL)animated {
    NSLog(@"HELLO Swizzle01");
    [self xxx_viewWillAppear:animated] ;
    NSLog(@"HELLO Swizzle02");
}

Output:
HELLO 01
HELLO Swizzle01
HELLO Swizzle02
HELLO 02
  • IMP变化:注意hook的是UIViewController的viewWillAppear而不是ViewController
    Method Swizzling与JSPatch探究(二)

  • 调用路径:
    Method Swizzling与JSPatch探究(二)

4.2 method Swizzling和JSPatch一起,JSPatch覆盖

为了避免swizzle在runtime期间出现诡异的异常,一般会写在+load里面,先于JSPatch加载

  • JSPatch,js文件,覆盖原方法
defineClass("ViewController", {
    viewWillAppear: function(animated) {
        console.log("JSPatch 01");
        self.super().viewWillAppear(animated);
        console.log("JSPatch 02");
    }
}, {});

output:
JSPatch.log: JSPatch 01
HELLO Swizzle01
HELLO Swizzle02
JSPatch.log: JSPatch 02
  • IMP变化:在之前的前一步的基础上
    Method Swizzling与JSPatch探究(二)

  • 调用路径
    Method Swizzling与JSPatch探究(二)

4.3 method Swizzling和JSPatch一起,JSPatch调用原方法
defineClass("ViewController", {
    viewWillAppear: function(animated) {
        console.log("JSPatch 01");
        self.ORIGviewWillAppear(animated);
        console.log("JSPatch 02");
    }
}, {});

Output:
JSPatch.log: JSPatch 01
HELLO 01
HELLO Swizzle01
HELLO Swizzle02
HELLO 02
JSPatch.log: JSPatch 02
  • IMP变化:与前一步一致
  • 调用路径:相对之前略复杂,因此打印的地方也附上
    Method Swizzling与JSPatch探究(二)
4.4 重复hook同一个方法,无JSPatch

重复hook:两处地方都对UIViewController的viewWillAppear进行method swizzle
hook顺序:先UIViewController+MLPF,后UIViewController+MLPF2

`ViewController:UIViewController`

- (void)viewWillAppear:(BOOL)animated {
    NSLog(@"HELLO 01");
    [super viewWillAppear:animated];
    NSLog(@"HELLO 02");
}

UIViewController+MLPF.m
- (void)xxx_viewWillAppear:(BOOL)animated {
    NSLog(@"HELLO Swizzle1-1");
    [self xxx_viewWillAppear:animated] ;
    NSLog(@"HELLO Swizzle1-2");
}

UIViewController+MLPF02.m
- (void)xxx02_viewWillAppear:(BOOL)animated {
    NSLog(@"HELLO Swizzle2-1");
    [self xxx02_viewWillAppear:animated];
    NSLog(@"HELLO Swizzle2-2");
}

Output:
HELLO 01
HELLO Swizzle2-1
HELLO Swizzle1-1
HELLO Swizzle1-2
HELLO Swizzle2-2
HELLO 02
  • IMP变化:为什么这样改变前文已经说明
    Method Swizzling与JSPatch探究(二)

  • 调用路径:
    Method Swizzling与JSPatch探究(二)

4.5 重复hook同一个方法和JSPatch的兼容性,JSPatch调用原方法

在4.4的基础上增加JSPatch,调用原方法

defineClass("ViewController", {
    viewWillAppear: function(animated) {
        console.log("JSPatch 01");
        self.ORIGviewWillAppear(animated);
        console.log("JSPatch 02");
    }
}, {});

output:
JSPatch.log: JSPatch 01
HELLO 01
HELLO Swizzle2-1
HELLO Swizzle1-1
HELLO Swizzle1-2
HELLO Swizzle2-2
HELLO 02
JSPatch.log: JSPatch 02
  • IMP变化:
    Method Swizzling与JSPatch探究(二)

  • 调用路径:经过前面几步的分析,这里实际上没什么难度
    Method Swizzling与JSPatch探究(二)

4.6 重复hook同一个方法和JSPatch的兼容性,JSPatch覆盖原方法

这里就不作累述了,经过前面的分析,个人觉得这部分直接给结果应该看得懂,因此这部分流程图就不画了

defineClass("ViewController", {
    viewWillAppear: function(animated) {
        console.log("JSPatch 01");
        self.super().viewWillAppear(animated);
        console.log("JSPatch 02");
    }
}, {});

JSPatch.log: JSPatch 01
HELLO Swizzle2-1
HELLO Swizzle1-1
HELLO Swizzle1-2
HELLO Swizzle2-2
JSPatch.log: JSPatch 02
4.7 一点补充

hotfix的代码不要出现如下情况,调用原实现,又调super,原实现代码规范是存在调super的,这样会导致重复调用,在4.6的基础上如下试验

defineClass("ViewController", {
    viewWillAppear: function(animated) {
        console.log("JSPatch 01");
        self.ORIGviewWillAppear(animated);
        self.super().viewWillAppear(animated);
        console.log("JSPatch 02");
    }
}, {});

JSPatch.log: JSPatch 01
HELLO 01
HELLO Swizzle2-1
HELLO Swizzle1-1
HELLO Swizzle1-2
HELLO Swizzle2-2
HELLO 02
HELLO Swizzle2-1
HELLO Swizzle1-1
HELLO Swizzle1-2
HELLO Swizzle2-2
JSPatch.log: JSPatch 02
5、结论

实际上,理解了2、3对method swizzling 和 JSPatch hook的原理分析,第四部分的兼容性实验的结果大致也能猜的八九不离十。
因为两者不存在冲突的地方,唯一的重合点就是对函数实现指针的替换
两者共用时,hotfix的方法和method Swizzling hook的函数实现都会按顺序调用,并不会出现覆盖,或者出错的情况

下一步试验:
1、实际上Method Swizzling还是存在一些坑的,上面是常规的实验。后续会更深入一些
2、同时试验以属性的形式KVO监听,进行埋点试验(目前试验了一点,但是case还不全)

6、拓展

当前AOP热门的库Aspects,在hook方案上,跟JSPatch大同小异,但是最后一步在forwardinvocation,Aspects则是保存block为关联属性。与JSPatch共用时在一些case下会产生crash 或者 覆盖执行等问题

关于Aspects和JSPatch兼容性的情况,这篇文章已经解释的较为详细,可供参考
http://www.jianshu.com/p/dc1deaa1b28e

7、也许你有疑问的地方:

主要针对第3部分的问题


Q:万一,原方法不存在怎么办?

A:实际上JSPatch源码实现,会调一个defineClass函数,这个函数最后都会调用overrideMethod方法,而overrideMethod会根据原类是否已定义过这个函数,来决定执行class_addMethod还是class_replaceMethod
如果是add那很简单,根本不存在是否兼容性的问题,所以并没有在上文指出


Q:_objc_msgForward是什么鬼?

A:一个函数指针。发送消息的时候,其实相当于objc_msgSend()执行的过程,这个函数会从Class的缓存中查找IMP,没找到则向父类Class查找,一直到根类都没找到,则会用_objc_msgForward函数指针替代IMP。而_objc_msgForward会尝试进行消息转发


贴一点runtime的伪代码,证明下这不是瞎逼逼

IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;

    if (!cls  ||  !sel) return nil;

    imp = lookUpImpOrNil(cls, sel, nil, 
                         YES/*initialize*/, YES/*cache*/, YES/*resolver*/);

    // Translate forwarding function to C-callable external version
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}

IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}

Q:关于type encoding

A:为了协助运行时系统,编码器用字符串为每个方法的返回值和参数类型和方法选择器编码。官方文档:

To assist the runtime system, the compiler encodes the return and argument types for each method in a character string and associates the string with the method selector.

8、Last

有问题欢迎讨论
2017-03-22