全面谈谈Aspects和JSPatch兼容问题

1. 背景

AspectsJSPatch 是 iOS 开发中非常常见的两个库。Aspects 提供了方便简单的方法进行面向切片编程(AOP),JSPatch可以让你用 JavaScript 书写原生 iOS APP 和进行热修复。关于实现原理可以参考 面向切面编程之 Aspects 源码解析及应用JSPatch wiki。简单地概括就是将原方法实现替换为_objc_msgForward(或_objc_msgForward_stret),当执行这个方法是直接进入消息转发过程,最后到达替换后的-forwardInvocation:,在-forwardInvocation:内执行新的方法,这是两者的共同原理。最近项目开发中需要用 JSPatch 替换方法修复一个 bug ,然而这个方法已经使用 Aspects 进行 hook 过了,那么两者同时使用会不会有问题呢?关于这个问题,网上介绍比较详细的是 面向切面编程之 Aspects 源码解析及应用有关Swizzling的一个问题,深入研究后发现这两篇文章讲得都不够全面。本文基于 Aspects 1.4.1 和 JSPatch 1.1 介绍几种测试结果和原因。

2. 测试

2.0. 源码

这是本文使用的测试代码,你可以clone下来,泡杯咖啡,找个安静的地方跟着本文一步一步实践。

2.1. 代码说明

ViewController.m中首先定义一个简单类MyClass,只有-test-test2方法,方法内打印log

@interface MyClass : NSObject
- (void)test;
- (void)test2;
@end

@implementation MyClass
- (void)test {
    NSLog(@"MyClass origin log");
}
- (void)test2 {
    NSLog(@"MyClass test2 origin log");
}
@end

接着是三个hook方法,分别是对-test进行hook-jp_hook-aspects_hook和对-test2进行hook-aspects_hook_test2

- (void)jp_hook {
    [JPEngine startEngine];
    NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
    NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
    [JPEngine evaluateScript:script];
}
- (void)aspects_hook {
    [MyClass aspect_hookSelector:@selector(test) withOptions:AspectPositionAfter usingBlock:^(id aspects) {
        NSLog(@"aspects log");
    } error:nil];
}
- (void)aspects_hook_test2 {
    [MyClass aspect_hookSelector:@selector(test2) withOptions:AspectPositionInstead usingBlock:^(id aspects) {
        NSLog(@"aspects test2 log");
    } error:nil];
}

demo.js代码也非常简单,对MyClass-test进行替换

require(‘MyClass‘)
defineClass(‘MyClass‘, {
    test: function() {
//        self.ORIGtest();
        console.log("jspatch log")
    }
});

2.2. 具体测试

2.2.1. JSPatch 先 hook 、Aspects 采用 AspectPositionInstead (替换) hook

那么代码就是下面这样,注意把-aspects_hook方法设置为AspectPositionInstead

// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    [self jp_hook];
    [self aspects_hook];
    MyClass *a = [[MyClass alloc] init];
    [a test];
}

执行结果:

JPAndAspects[2092:1554779] aspects log

结果是 Aspects 正确替换了方法

2.2.2. Aspects 先采用随便一种Position hook,JSPatch再hook

那么代码就是下面这样

- (void)viewDidLoad {
    [super viewDidLoad];
    [self aspects_hook];
    [self jp_hook];
    MyClass *a = [[MyClass alloc] init];
    [a test];
}

执行结果:

JPAndAspects[2774:1565702] JSPatch.log: jspatch log

结果是 JSPatch 正确替换了方法

Why?

前面说到,hook 会替换该方法和 -forwardInvocation:,我们先看看方法被 hook 前后的变化

原方法对应关系

方法替换后原方法指向了_objc_msgForward,同时添加一个方法PREFIXtest(JSPatchORIGtestAspectsaspects_test)指向了原来的实现。JSPatch新增了一个方法指向IMP(NEWtest)Aspects则保存block为关联属性

-test变化

-forwardInvocation: 的变化也相似,原来的-forwardInvocation: 没实现是这样的

-forwardInvocation:变化

如果原来的-forwardInvocation:有实现,就新加一个-ORIGforwardInvocation:指向原IMP(forwardInvocation:)

-forwardInvocation:变化

由于-test方法指向了_objc_msgForward,这时调用-test方法就会进入消息转发,消息转发的第三步进入-forwardInvocation:执行新的IMP(NEWforwardInvocation),拿到invocationinvocation.selector拼上前缀,然后拼上其他信息直接invoke,最终执行IMP(NEWtest)(Aspects是执行替换的block)。



以上是只有一次hook的情况,我们看看两者都hook的变化

JSPatch先hook,-test变化

JSPatch先hook,-forwardInvocation:变化

这时调用-test同样发生消息转发,进入-forwardInvocation:执行AspectsIMP(AspectsforwardInvocation),上文提到Aspects把替换的block保存为关联属性了,到了-forwardInvocation:直接拿出来执行,和原来的实现没有任何关系,所以有了2.2.1 正确的结果。



Aspects先hook,-test变化

Aspects先hook,-forwardInvocation:变化

这时调用-test同样发生消息转发,进入-forwardInvocation:执行JSPatchIMP(JSPatchforwardInvocation),执行_JPtest,和原来的实现
没有任何关系,所以有了2.2.2 正确的结果。
看到这里,如果细心的话会发现ORIGtest指向了_objc_msgForward,如果我们在JSPatch代码里调用self.ORIGtest()会怎么样呢?

2.2.3. Aspects 先采用随便一种Position hook,JSPatch再hook,JSPatch代码里调用self.ORIGtest()

代码是下面这样的

// demo.js
require(‘MyClass‘)
defineClass(‘MyClass‘, {
    test: function() {
        self.ORIGtest();
        console.log("jspatch log")
    }
});
// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    [self aspects_hook];
    [self jp_hook];
    MyClass *a = [[MyClass alloc] init];
    [a test];
}

执行结果:

JPAndAspects[8668:1705052] -[MyClass ORIGtest]: unrecognized selector sent to instance 0x7ff592421a30
Why?

-test-forwardInvocation:的变化同上一步Aspectshook
由于-ORIGtest指向了_objc_msgForward,调用方法时进入-forwardInvocation:执行IMP(JSPatchforwardInvocation)JSPatchforwardInvocation中有这样一段代码

static void JPForwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation *invocation)
{
...
  JSValue *jsFunc = getJSFunctionInObjectHierachy(slf, JPSelectorName);
  if (!jsFunc) {
      JPExecuteORIGForwardInvocation(slf, selector, invocation);
      return;
  }
...
}

这个-ORIGtest在对象中找不到具体的实现,因此转发给了-ORIGINforwardInvocation:注意:这里直接把-ORIGtest转发出去了,很显然IMP(AspectsforwardInvocation)也是处理不了这个消息的。因此,出现了unrecognized selector异常。
这里是两者兼容出现的最大问题,如果JSPatch在转发前判断一下这个方法是自己添加的-ORIGxxx,把前缀ORIG去掉再转发,这个问题就解决了。

2.2.4. JSPatch先hook, Aspects 再采用AspectPositionInstead(替换)hook,JSPatch代码里调用self.ORIGtest()

和2.2.1 相同,不管JSPatch hook之后是什么样的,都只执行Aspectsblock

2.2.5. JSPatch先hook, Aspects 再采用AspectPositionBefore(替换)hook

代码如下,注意把AspectPositionInstead替换为AspectPositionBefore

// demo.js
require(‘MyClass‘)
defineClass(‘MyClass‘, {
    test: function() {
        console.log("jspatch log")
    }
});
// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    [self jp_hook];
    [self aspects_hook];
    MyClass *a = [[MyClass alloc] init];
    [a test];
}

执行结果:

JPAndAspects[10943:1756624] aspects log
JPAndAspects[10943:1756624] JSPatch.log: jspatch log

执行结果如期是正确的。
IMP(AspectsforwardInvocation)的部分代码如下

    SEL originalSelector = invocation.selector;
    SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
    invocation.selector = aliasSelector;
    AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
    AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
    AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];

    // Before hooks.
    aspect_invoke(classContainer.beforeAspects, info);
    aspect_invoke(objectContainer.beforeAspects, info);

    // Instead hooks.
    BOOL respondsToAlias = YES;
    if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
        aspect_invoke(classContainer.insteadAspects, info);
        aspect_invoke(objectContainer.insteadAspects, info);
    }else {
        Class klass = object_getClass(invocation.target);
        do {
            if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
                [invocation invoke];
                break;
            }
        }while (!respondsToAlias && (klass = class_getSuperclass(klass)));
    }

    // After hooks.
    aspect_invoke(classContainer.afterAspects, info);
    aspect_invoke(objectContainer.afterAspects, info);

    // If no hooks are installed, call original implementation (usually to throw an exception)
    if (!respondsToAlias) {
        invocation.selector = originalSelector;
        SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
        if ([self respondsToSelector:originalForwardInvocationSEL]) {
            ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
        }else {
            [self doesNotRecognizeSelector:invocation.selector];
        }
    }

首先执行Before hooks;接着查找是否有Instead hooks,如果有就执行,如果没有就在类继承链中查找父类能否响应-aspects_test,如果可以就invoke这个invocation,否则把respondsToAlias置为NO;接着执行After hooks;接着if (!respondsToAlias)把这个-test转发给ORIGINforwardInvocationIMP(JSPatchforwardInvocation)处理了这个消息。注意这里是把-test转发

2.2.6. JSPatch先hook, Aspects 再采用AspectPositionAfter hook

代码同2.2.5,注意把AspectPositionBefore替换为AspectPositionAfter

JPAndAspects[11706:1776713] aspects log
JPAndAspects[11706:1776713] JSPatch.log: jspatch log

结果都输出了,但是顺序不对。
IMP(AspectsforwardInvocation)代码中不难看出,After hooks先执行了,再将这个消息转发。这也可以说是Aspects的不足。

2.2.7. Aspects随便一种Position hook方法-test2,JSPatch再hook -test,JSPatch代码里调用self.ORIGtest(), Aspects 以随便一种Position hook方法-test

同2.2.5和2.2.6很像,不过前面多了对-test2的hook,代码如下:

// demo.js
require(‘MyClass‘)
defineClass(‘MyClass‘, {
    test: function() {
        self.ORIGtest();
        console.log("jspatch log")
    }
});
// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    [self aspects_hook_test2];
    [self jp_hook];
    [self aspects_hook];
    MyClass *a = [[MyClass alloc] init];
    [a test];
}

代码执行结果:

JPAndAspects[12597:1797663] MyClass origin log
JPAndAspects[12597:1797663] JSPatch.log: jspatch log

结果是Aspects对-test的hook没有生效。

Why?

不废话,直接看Aspects代码:

static Class aspect_hookClass(NSObject *self, NSError **error) {
    NSCParameterAssert(self);
    Class statedClass = self.class;
    Class baseClass = object_getClass(self);
    NSString *className = NSStringFromClass(baseClass);

    // Already subclassed
    if ([className hasSuffix:AspectsSubclassSuffix]) {
        return baseClass;

        // We swizzle a class object, not a single object.
    }else if (class_isMetaClass(baseClass)) {
        return aspect_swizzleClassInPlace((Class)self);
        // Probably a KVO‘ed class. Swizzle in place. Also swizzle meta classes in place.
    }else if (statedClass != baseClass) {
        return aspect_swizzleClassInPlace(baseClass);
    }

    // Default case. Create dynamic subclass.
    const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
    Class subclass = objc_getClass(subclassName);

    if (subclass == nil) {
        subclass = objc_allocateClassPair(baseClass, subclassName, 0);
        if (subclass == nil) {
            NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
            AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
            return nil;
        }

        aspect_swizzleForwardInvocation(subclass);
        aspect_hookedGetClass(subclass, statedClass);
        aspect_hookedGetClass(object_getClass(subclass), statedClass);
        objc_registerClassPair(subclass);
    }

    object_setClass(self, subclass);
    return subclass;
}

这段代码的作用是区分self的类型,进行不同的swizzleForwardInvocationself本身可能是一个Class;或者self通过-class方法返回的self真正的Class不同,最典型的KVO,会创建一个子类加上NSKVONotify_前缀,然后重写class方法,看不懂的可以参考Objective-C 对象模型。这两种情况都对self真正的Class进行aspect_swizzleClassInPlace;如果self是一个普通对象,则模仿KVO的实现方式,创建一个子类,swizzle子类的-forwardInvocation:,通过object_setClass强行设置Class



再看aspect_swizzleClassInPlace

static Class aspect_swizzleClassInPlace(Class klass) {
    ...
        if (![swizzledClasses containsObject:className]) {
            aspect_swizzleForwardInvocation(klass);
            [swizzledClasses addObject:className];
        }
    ...
}

问题就出在这个aspect_swizzleClassInPlace,它会判断如果这个类的-forwardInvocation: swizzle过,就什么都不做,但是通过数组这种方式是会出问题,第二次hook的时候就不会-forwardInvocation:替换成IMP(AspectsforwardInvocation),所以第二次hook不生效。相比,JSPatch的实现就比较合理,判断两个IMP是否相等。

if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {

}
2.2.8. Aspects 先采用随便一种Position hook父类,JSPatch再hook子类,JSPatch代码里调用self.super().xxx()

代码是下面这样的

// demo.js
require(‘MySubClass‘)
defineClass(‘MySubClass‘, {
    test: function() {
        self.super().test();
        console.log("jspatch log")
    }
});
// ViewController.m

// 增加一个子类
@interface MySubClass : MyClass
@end

@implementation MySubClass
- (void)test {
    NSLog(@"MySubClass origin log");
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    [self aspects_hook];
    [self jp_hook];
    MySubClass *a = [[MySubClass alloc] init];
    [a test];
}

执行结果:

JPAndAspects[89642:1600226] -[MySubClass SUPER_test]: unrecognized selector sent to instance 0x7fa4cadabc70
Why?

父类MyClass-test-forwardInvocation:的变化同2.2.1中原-forwardInvocation没有实现的情况。
JSPatchsuper的实现是新增加一个方法-SUPER_test,IMP指向了父类的IMP,由于-test指向了_objc_msgForward,调用方法时进入-forwardInvocation:执行IMP(JSPatchforwardInvocation),执行self.super().test()时,实际执行了-SUPER_test,这个-SUPER_test在对象中找不到具体的实现,发生了-ORIGtest一样的异常。
这里是两者兼容出现的第二个比较严重的问题。

2.3 总结

写到这里,除了Aspects对对象的hook(这种情况很少见,你可以自己测试),可能已经解答了两者兼容的大部分问题。通过以上分析,得出不兼容的四种情况:

  • Aspectshook某一方法,JSPatchhook同一方法且JSPatch调用了self.ORIGxxx(),结果是异常崩溃。
  • Aspectshook父类某一方法,JSPatchhook子类同一方法且JSPatch调用了self.super().xxx(),结果是异常崩溃。
  • JSPatchhook某一方法,AspectsAfter的方式hook同一方法,结果是执行顺序不对
  • Aspectshook任何方法,JSPatchhook另一方法,AspectshookJSPatch相同的方法,结果是最后一次hook不生效

3. 写在最后

简书作为一个优质原创内容社区,拥有大量优质原创内容,提供了极佳的阅读和书写体验,吸引了大量文字爱好者和程序员。简书技术团队在这里分享技术心得体会,是希望抛砖引玉,吸引更多的程序员大神来简书记录、分享、交流自己的心得体会。这个专题以后会不定期更新简书技术团队的文章,包括Android、iOS、前端、后端等等,欢迎大家关注。

参考

文/zhao0(简书作者)
原文链接:http://www.jianshu.com/p/dc1deaa1b28e
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

时间: 2024-10-25 00:48:08

全面谈谈Aspects和JSPatch兼容问题的相关文章

360浏览器兼容调整

360安全浏览器在兼容模式下,默认使用IE6/7模式,有时候web只兼容IE8以上,下面谈谈解决360浏览器兼容模式极速模式调整的问题: 设置360安全浏览器打开模式通过meta设置即可,如下: 一.打开页面的模式 1. 默认使用极速模式(webkit内核) <meta name="renderer" content="webkit"> 2.默认使用兼容模式(IE6/7) <meta name="renderer" conten

谈谈龙之谷手游兼容测试的一百个坑

一.项目背景 1. 高价值IP 龙之谷 ,一款优秀的端游移植到手游平台,凭借的丰富的游戏内容和优秀的游戏品质,公测首日便在畅销榜登顶,取得了巨大的成功.  游戏内容不仅继承了端游的内容,还根据手游操作方式以及平台特性进行了改进,使之更适合移动用户操作,界面分部也更加合理.  2.初期兼容性问题较多 龙之谷与其他游戏产品一样,版本初期暴露的兼容性问题很多,类似无法安装以及必现的CRASH等致命问题多次出现外,还存在着大量UI错位.资源加载异常.屏幕分辨率适应差等严重级别的兼容性问题.  二.定制测

针对苹果iOS最新审核要求为应用兼容IPv6

最新消息 今天苹果推出重磅消息,6月1日后所有应用必须支持IPv6-only网络!!!当iOS开发者看到这个消息的第一反应可能就是IPv6是个什么鬼!!其实IPv6早在很早之前就已经推出,而且苹果在2015年的开发者大会上就已经提出苹果将力挺IPv6 官网链接 IPv6-only 说了这么多,IPv6到底是个什么鬼!我们来看看百度百科是怎么解释的: 我们再来看看官方给出的IPv6与IPv4的对比: 看不懂??,我们来解释下: 1.IPv6:避免了网络地址转换(NAT) 2.通过网络通过使用简化的

XAMPP 的 Linux 版 (x86 兼容处理器版)安装配置使用详细介绍,教你建好一个LAMPP站!

XAMPP 的 Linux 版 (x86 兼容处理器版) 以前被称作 LAMPP,但为了避免误解,将其重名命为 ?XAMPP 的 Linux 版?.所以,如果您在寻找 LAMPP下载.安装.配置.使用方法,您就来对地方了. 安装过程仅 4 个步骤 步骤 1:下载 只需点击下面的链接.下载最新版总是好主意.:)完整的下载列表(老版本)可在 SourceForge 找到. 详细的 XAMPP 各版本更新记录可在 发布说明 中找到. XAMPP Linux 1.8.2 107 MB Apache 2.

[.net 面向对象程序设计深入](4).NET MVC ——谈谈MVC的版本变迁及新版本6.0发展方向

[.net 面向对象程序设计深入](4).NET MVC ——谈谈MVC的版本变迁及新版本6.0发展方向 1.关于MVC 在本篇中不再详细介绍MVC的基础概念,这些东西百度要比我写的全面多了,MVC从1.0到5.0的时间也不短了,很多人只是按照范例去使用MVC的一些基础功能,并没有更加深入的了解MVC.在这一系列中,我主要介绍MVC的一些原理和使用技巧,以及MVC的发展方向. 先说说MVC,首先他是一种设计模式,如果你百度为什么GOF23种设计模式中没有MVC,答案很有意思,如下: “在他们看来

一个简单页面的背后的一大串交互和兼容问题

前几天本前端小虾切了一个炒鸡简单的页面: http://dev.360.cn/mod/h5 对!你没有看错,这个界面除了第二部分鼠标滑过的特效和点击申请接入部分的弹框表单之外,其他部分毫无难度,就是这么一张页面……把本小虾大大的整了一遍,所以小虾和大牛之间的差距绝不仅是物种上的不同! 首先是制作时间的估计.这个页面并不是一个新鲜的页面,在之前多多少少切过几个异常相似的,比如下面那两个: http://dev.360.cn/mod/mediaV/intro http://dev.360.cn/mo

谈谈单元测试之(三):测试工具 JUnit 4

前言 上一篇文章<测试工具 JUnit 3>简单的讨论了 JUnit 3 的使用以及内部的方法.这篇文章将会在 JUnit 3 的基础上,讨论一下 JUnit 4 的新特性.同时,与 JUnit 3 做一个简单的对比.那么,废话就不多说了,直接进入正题. 介绍 JUnit 4.x 是利用了 Java 5 的特性(Annotation)的优势,使得测试比起 3.x 版本更加的方便简单,JUnit 4.x 不是旧版本的简单升级,它是一个全新的框架,整个框架的包结构已经彻底改变,但 4.x 版本仍然

兼容之css中的hack

Hack有三种方式,谈谈其中之一的css中的hack兼容 常见的css属性hack: /* CSS属性级Hack */ color:red; /* 所有浏览器可识别*/ _color:red; /* 仅IE6 识别 */ *color:red; /* IE6.IE7 识别 */ +color:red; /* IE6.IE7 识别 */ *+color:red; /* IE6.IE7 识别 */ [color:red; /* IE6.IE7 识别 */ color:red\9; /* IE6.IE

谈谈webpack 的优势

其优势主要可以归类为如下几个: 1. webpack 是以 commonJS 的形式来书写脚本滴,但对 AMD/CMD 的支持也很全面,方便旧项目进行代码迁移. 2. 能被模块化的不仅仅是 JS 了. 3. 开发便捷,能替代部分 grunt/gulp 的工作,比如打包.压缩混淆.图片转base64等. 4. 扩展性强,插件机制完善,特别是支持 React 热插拔(见 react-hot-loader )的功能让人眼前一亮. 我们谈谈第一点.以 AMD/CMD 模式来说,鉴于模块是异步加载的,所以