1. 背景

Aspects 和 JSPatch 是 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下来,泡杯咖啡,找个安静的地方跟着本文一步一步实践。

https://github.com/zhao0/JPAndAspects

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(JSPatch 是ORIGtest,Aspects 是aspects_test)指向了原来的实现。JSPatch新增了一个方法指向IMP(NEWtest),Aspects则保存block为关联属性

-test变化

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

-forwardInvocation:变化

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

-forwardInvocation:变化

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


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

JSPatch先hook,-test变化

JSPatch先hook,-forwardInvocation:变化

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

Aspects先hook,-test变化

Aspects先hook,-forwardInvocation:变化

这时调用-test同样发生消息转发,进入-forwardInvocation:执行JSPatch的IMP(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:的变化同上一步Aspects先hook。

由于-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之后是什么样的,都只执行Aspects的block

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转发给ORIGINforwardInvocation即IMP(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的类型,进行不同的swizzleForwardInvocation。self本身可能是一个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没有实现的情况。

JSPatch中super的实现是新增加一个方法-SUPER_test,IMP指向了父类的IMP,由于-test指向了_objc_msgForward,调用方法时进入-forwardInvocation:执行IMP(JSPatchforwardInvocation),执行self.super().test()时,实际执行了-SUPER_test,这个-SUPER_test在对象中找不到具体的实现,发生了-ORIGtest一样的异常。

这里是两者兼容出现的第二个比较严重的问题。

2.3 总结

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

  • Aspects先hook某一方法,JSPatch再hook同一方法且JSPatch调用了self.ORIGxxx(),结果是异常崩溃。

  • Aspects先hook父类某一方法,JSPatch再hook子类同一方法且JSPatch调用了self.super().xxx(),结果是异常崩溃。

  • JSPatch先hook某一方法,Aspects以After的方式hook同一方法,结果是执行顺序不对

  • Aspects先hook任何方法,JSPatch再hook另一方法,Aspects再hook和JSPatch相同的方法,结果是最后一次hook不生效

3. 写在最后

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

参考

http://wereadteam.github.io/2016/06/30/Aspects/

http://www.jianshu.com/p/d5c3c2f236b8

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

  1. 移动端全兼容的flexbox速成班

    说起flexbox,都算是件陈年旧事了,它是2009年W3C提出的一种全新的可伸缩的CSS布局方式.依赖flexbox,我们可以更简单,高效的完成可伸缩式页面的布局. 业界与flexbox的相关教程文 ...

  2. 移动端经常出现的兼容问题,谈谈移动端应用或者wap站的一些优化技巧和心得

    移动端经常出现的兼容问题,谈谈移动端应用或者wap站的一些优化技巧和心得 1.        安卓浏览器看背景图片,有些设备会模糊. 因为手机分辨率太小,如果按照分辨率来显示网页,字会非常小,安卓手机 ...

  3. 谈谈一些有趣的CSS题目(十二)-- 你该知道的字体 font-family

    开本系列,谈谈一些有趣的 CSS 题目,题目类型天马行空,想到什么说什么,不仅为了拓宽一下解决问题的思路,更涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题 ...

  4. 谈谈如何使用Netty开发实现高性能的RPC服务器

    RPC(Remote Procedure Call Protocol)远程过程调用协议,它是一种通过网络,从远程计算机程序上请求服务,而不必了解底层网络技术的协议.说的再直白一点,就是客户端在不必知道 ...

  5. 谈谈一些有趣的CSS题目(六)-- 全兼容的多列均匀布局问题

    开本系列,谈谈一些有趣的 CSS 题目,题目类型天马行空,想到什么说什么,不仅为了拓宽一下解决问题的思路,更涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题 ...

  6. 谈谈一些有趣的CSS题目(七)-- 消失的边界线问题

    开本系列,谈谈一些有趣的 CSS 题目,题目类型天马行空,想到什么说什么,不仅为了拓宽一下解决问题的思路,更涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题 ...

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

    [.net 面向对象程序设计深入](4)MVC 6 ——谈谈MVC的版本变迁及新版本6.0发展方向 1.关于MVC 在本篇中不再详细介绍MVC的基础概念,这些东西百度要比我写的全面多了,MVC从1.0 ...

  8. JSPatch使用小记

    hotfix的作用众所周知,Android和iOS都有各自的技术,但是相比Android的当天发布来说(如果你们的项目不需要灰度),iOS热更新的意义更加重大.因为iOS审核周期长不说,而且运气不好会 ...

  9. 转载:谈谈Unicode编码,简要解释UCS、UTF、BMP、BOM等名词

    转载: 谈谈Unicode编码,简要解释UCS.UTF.BMP.BOM等名词 这是一篇程序员写给程序员的趣味读物.所谓趣味是指可以比较轻松地了解一些原来不清楚的概念,增进知识,类似于打RPG游戏的升级 ...

随机推荐

  1. MVC3.0在各个版本IIS中的部署

    概述: 最近在做一个MVC 3的项目,在部署服务器时破费了一番功夫,特将过程整理下来,希望可以帮到大家! 本文主要介绍在IIS5.1.IIS6.0.IIS7.5中安装配置MVC 3的具体办法! 正文: ...

  2. ado.net(class0503)

    ado.net组成 数据提供程序 connection //连接对象 command executeNonQuery //执行增删改 executeScalar //执行查询返回首行首列 execut ...

  3. 初识HTML 5:关于它的三个三

    来源:http://www.ido321.com/949.html 一.HTML 5受欢迎的三个理由 1.IE.Google.Firefox.Safari.Opera等主流浏览器的支持 1.1  微软 ...

  4. C语言——递归练习

    1.炮弹一样的球状物体,能够堆积成一个金字塔,在顶端有一个炮弹,它坐落在一个4个炮弹组成的层面上,而这4个炮弹又坐落在一个9个炮弹组成的层面上,以此类推.写一个递归函数CannonBall,这个函数把 ...

  5. 【Spark学习】Apache Spark集群硬件配置要求

    Spark版本:1.1.1 本文系从官方文档翻译而来,转载请尊重译者的工作,注明以下链接: http://www.cnblogs.com/zhangningbo/p/4135912.html 目录 存 ...

  6. 关于scrollTop的那些事

    大家在实际项目中,应该是要经常用到scrollTop的,它表示的是可视窗口距离页面顶部的距离,这个scrollTop是可读写的,所以可以用来做页面滚动. 但是大家或多或少遇到一些浏览器兼容问题,为什么 ...

  7. CodeForces 682D Alyona and Strings (四维DP)

    Alyona and Strings 题目链接: http://acm.hust.edu.cn/vjudge/contest/121333#problem/D Description After re ...

  8. Spring dependency checking with @Required Annotation

    Spring's dependency checking in bean configuration file is used to make sure all properties of a cer ...

  9. eclispe输入@注解时提示所有注解的设置

    修改输入@提示所有的注解提示方法 eclipse下windows-->preference-->java-->editor-->Content Assist下的Enable a ...

  10. 通用FASTREPORT打印模块及接口方法

    untFastReport.dfm文件: object frmFastReport: TfrmFastReport OldCreateOrder = False Height = 405 Width ...