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

转载于:https://www.cnblogs.com/fengmin/p/5870573.html

全面谈谈Aspects和JSPatch兼容问题相关推荐

  1. JSPatch实现原理一览

    前言 JSPatch是一个非常棒的热修复框架,10000+star!!!!!虽说2017年被苹果封杀了,但是据我获取到的有限的信息,大家还是偷偷摸摸混淆一下.改改类名继续在使用.毕竟bug还是不可避免 ...

  2. 360安全浏览器浏览模式调整

    亲测可用,若有疑问请私信 360安全浏览器在兼容模式下,默认使用IE6/7模式,有时候web只兼容IE8以上,下面谈谈解决360浏览器兼容模式极速模式调整的问题: 设置360安全浏览器打开模式通过me ...

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

    要有遥不可及的梦想,也要有脚踏实地的本事.----------- Grapefruit.Banuit Gang(香柚帮) 安卓浏览器看背景图片,有些设备会模糊. 因为手机分辨率太小,如果按照分辨率来显 ...

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

    原文链接:http://wetest.qq.com/lab/view/312.html 商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处. 一.项目背景 1. 高价值IP 龙之谷 ,一款优 ...

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

    原文链接:http://wetest.qq.com/lab/view/312.html 商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处. 一.项目背景 1. 高价值IP 龙之谷 ,一款优 ...

  6. MyBatis 如何兼容所有日志框架?

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 来源:blog.csdn.net/zwx900102/ar ...

  7. 谈谈中台架构之交易中台

    中台的概念说了好多年了,起源就是芬兰的游戏公司supercell,之后阿里就提出了大中台小前台的战略,然后和疯狗一样侵蚀了中国. 很多小公司为了显得牛逼,管他呢,干他,就要硬怼个中台出来,反正有个名字 ...

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

    在WWDC2 015上苹果宣布iOS 9将支持纯IPv6的网络服务.2016年初开始所有提交到App Store的应用必须支持IPv6.而今年5月初,苹果宣布6月1日后所有应用必须支持IPv6-onl ...

  9. MyBatis日志到底是如何做到兼容所有常用日志框架的?

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 作者 | 双子孤狼 来源 | https://blog. ...

最新文章

  1. OKR 怎么突然火起来了?
  2. 树莓派python串口中文输出_Python实现树莓派USB串口通讯
  3. 什么是重构,什么不是重构
  4. jsp标签 判断 余数_程序员的数学基础课(三)余数与迭代法
  5. 最近实在抽不出时间更新博客
  6. Centos7安装Redis4.0.8
  7. 《中国人工智能学会通讯》——7.10 总结和展望
  8. MySQL卸载及安装
  9. GDB 01 -- 调试信息与调试原理
  10. 中国ai人工智能发展太快_新的AI计算遥远行星的速度快100,000倍
  11. 汽车之家口啤数据爬虫
  12. html在表格输入文字不显示,Word表格中有部分格子内无法输入文字是怎么回事?...
  13. 通过阿里P9代考这件事,聊聊职级
  14. 会讲故事的前物理学家万维钢解读、推荐过的书24本,好书一半
  15. Java输入三条边判断是否能组成三角形,若能构成则输出什么三角形
  16. 树莓派控制4路5v继电器开关
  17. canvas绘图 echarts 基本使用
  18. dom4j demo
  19. ArcMap基础操作——去除影像背景值
  20. 优雅整洁的 Java 代码命名技巧,风之极·净化

热门文章

  1. 第十一章 Direct3D Initialization
  2. 微软MSE的离线更新方式
  3. 关注国内Pinterest模式网站发展
  4. Jquery easyUI datagrid遇到空行做判断
  5. Substrate之旅4:基于Substrate私有链的搭建
  6. 【Python爆款】这款“成语接龙”太强大了?以后就靠它了、霸气。
  7. 爬取1688网站商家信息
  8. 一、手把手教你 Vue2+Ts
  9. SDM(supervised descent method)算法
  10. fNIRS中的假阳性和假阴性:问题、挑战和方法