ios项目,对jspatch 和 aspect 兼容问题研究

背景

在我们实际开发的ios项目中,使用了JspatchAspect两个第三方库

先说下这两个是什么东东,能用来做什么,我们项目为何要引入这两个库

  • Aspect 源于切面编程的概念,可以在一个类的seletor的前后添加代码,或替换一个selector的实现。应用场景主要是处理UI表现,修改系统控件满足我们的需求。简易教程可参看aspect doc

  • Jspatch 在后期添加,appstore发布周期长,有线上问题需要即时修复,等不及发版本,需要做hotfix。到目前为止也帮助我们解决了很多线上问题,贡献很大

之前两者从未遇到过冲突问题,但在有一次用Jspatch在做hotfix的时候,修改了UIWebview的一个selector A,可偏偏Aspect也修改了UIWebview的一个selector B, 就是两者修改了同一个类的不同方法!

代码运行起来

必挂在aspect的__ASPECTS_ARE_BEING_CALLED__方法里,具体位置在

但当时我们仍需要做hotfix,只能绕路了,不能hook UIWebview了!找其他方法。

为了找寻其原因,我们来脱离项目,做个demo,来完整看看Jspatch和Aspect的兼容问题。

Demo

我们做的这个Demo分以下几个步骤,正好不熟悉Jspatch的同学和Aspect的同学也可以熟悉一下。

  1. Jspatch修改Viewcontroller的viewWillAppear的方法,在方法里修改背景色为黑色
  2. 屏蔽Jspatch,Aspect单独修改的viewWillAppear的方法,在方法里修改背景色为黄色
  3. Jspatch和Aspect,都替换viewWillAppear的方法,颜色仍修改为各自颜色
  4. Jspatch和Aspect,替换不同方法
  5. Jspatch替换类的实例方法,Aspect替换实例对象方法

步骤一(Jspatch修改背景色)

首先我们创建一个新的工程,xcode -> file -> new -> project -> Signal view。工程目录下创建Podfile。

Podfile内容如下, 将jspatch和aspect两个库引入进来

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'JADemo' do
  # Uncomment the next line if you're using Swift or would like to use dynamic frameworks
  # use_frameworks!

  # Pods for JADemo
  pod 'JSPatch'
  pod 'Aspects'
end


本地添加一个demo.js的文件,js代码替换viewWillAppear, 设置背景色为黑色

require('UIColor')
defineClass('ViewController', {
viewWillAppear: function(animated) {
        self.super().viewWillAppear(animated);
        self.view().setBackgroundColor(UIColor.blackColor());
    },
});

接下来Appdelegate里加入

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    [JPEngine startEngine];
    NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
    if (sourcePath) {
        NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
        [JPEngine evaluateScript:script];
    }
    
    return YES;
}

运行...


修改完成!

步骤二(Aspects修改背景色)

屏蔽Jspatch,将demo.js 重命名为demo1.js

加入aspects代码

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    
    [ViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info, BOOL animated) {
        [super viewWillAppear:animated];
        self.view.backgroundColor = [UIColor yellowColor];
    } error:nil];
}

步骤三(Jspatch和Aspects同时修改)

现在将demo1.js名字改回demo.js, 恢复Jspatch修改功效。

运行...

结果是Aspects修改生效了

步骤四(Jspatch和Aspects同时修改不同方法)

我们这时要有两种不同的表现,那用Jspatch来修改背景色,Aspects来弹一个alert。看看是否都生效了吧。Aspects的修改方法,改为另一个viewDidAppear

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    
    [ViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info, BOOL animated) {
        [super viewDidAppear:animated];
        UIAlertController* alertController = [UIAlertController alertControllerWithTitle:@"alert" message:@"content" preferredStyle:UIAlertControllerStyleAlert];
        [self presentViewController:alertController animated:true completion:nil];
    } error:nil];
}

运行...

哎呀,没有冲突啊,都生效了,是不是搞错了。注意,我们用Aspects替换的是类的实例方法,也就是ViewController创建出来对象,都会弹一个alert出来。而我们项目中遇到情况是用Aspects修改了具体实例。所以进入最后一个步骤。

步骤五(Jspatch和Aspects同时修改不同方法, Aspects修改具体实例)

修改Aspects代码, self代替ViewController

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info, BOOL animated) {
        [super viewDidAppear:animated];
        UIAlertController* alertController = [UIAlertController alertControllerWithTitle:@"alert" message:@"content" preferredStyle:UIAlertControllerStyleAlert];
        [self presentViewController:alertController animated:true completion:nil];
    } error:nil];
}

运行...

如果你加了exception断点的话,就会重现项目中情况了。

Demo总结

这里先做个小总结, Jspatch 和 Aspects 实际冲突的表现

  • Jspatch 与 Aspects 在修改同一个类的同一方法时, 是可行的,但只有一个生效,取决于先后顺序

步骤三演示中,我只验证了Jspatch在前的情况,有兴趣的同学可以试验一下反过来的情况

  • Jspatch 与 Aspects 在修改同一个类的不同方法时,如果Aspects修改的是一个类的实例方法,而非具体实例,两者可以同时生效

  • Jspatch 与 Aspects 在修改同一个类的不同方法时,如果Aspects修改的是一个具体类的实例, Aspects assert,结果是Aspects生效

因此针对步骤五的情况是需要我们注意的。

现象研究

由表及里,先研究下我们最关心的步骤五。

forwardInvocation

冲突的原由是Jspatch和Aspects都使用了forwardInvocation,来重写消息转发。

这里做一下runtime的复习

我们知道对oc对象的方法调用,通过发送消息的方式,底层会调用void objc_msgSend(id self, SEL cmd, ...), 去对象的类中方法列表中搜索,如果没找到就去父类的方法列表中去寻找,如果找不到就走消息转发流程

对象在收到未定义的方法的时候,会首先走 +(BOOL)resolveInstanceMethod:(SEL)selector, 这里给你的机会去动态添加一个方法

如果这里未处理,走到-(id)forwardingTargetForSelector:(SEL)selector, 返回一个能处理此消息的对象

如果仍未处理走到最终的-(void)forwardInvocation:(NSInvocation*)invocation, forwardInvocation叫做完整的消息转发,之所以叫完整消息转发, 它包含一次调用的所有信息,调用对象,调用方法名,实现,参数,返回值。

Jspatch 和 Aspects 为了自定义一个方法调用,直接将方法调用,转发给forwardInvocation,在forwardInvocation中做一些复杂的工作。

如果你override一个forwardInvocation方法在一个类中,你发现你的方法并不会进入,这是因为这个方法在类中可以找到,不出触发消息转发机制,上面所讲。那如何强制进入forwardInvocation呢

Jspatch 和 Aspects 都会找到这样一个关键字,_objc_msgForward,在Demo中搜索一下

拿Jspatch举例,Jspatch会最终对类的某个selector调用class_replaceMethodclass_replaceMethod(cls, selector, msgForwardIMP, typeDescription);,Aspects也同样有此操作。

这样这个在调用这个类对象的selector,就直接进入forwardInvocation了。用图表示一下,工作原理

Jspatch 和 Aspects 为了保留原来的实现,添加了自己forwardInvocation,并replace了原来的forwardInvocation,保留了原版,可恢复。

现象分析之步骤三

现在可以思考下,步骤三,当替换了同一个方法时,方法首先被Jspatch强制转发给forwardInvocation,并替换了forwaInvocation的实现,然后后来Aspects,也替换了forwardInvocation的实现,就是把jspatch的实现给替换走了。

因此当替换同一个方法时,与顺序有关。

现象分析之步骤四

回顾步骤四,替换不同方法(Jspatch替换了viewWillAppear,Aspects替换viewDidAppear),Aspects替换类的实例方法。

到这里可能有疑惑,如果按照现象分析之步骤三的图, Jspatch的实现被Aspects替换,那么Jspatch应该没有启动作用才对,怎么会都奏效呢。

这就要进入Aspects的forwardInvocation看看它是怎么处理的。Aspects重写的forwardInvocation的函数是__ASPECTS_ARE_BEING_CALLED__

关注这个函数的最下面的代码



断点断住的地方,就是去执行Jspatch的地方。respondsToAlias代表,Aspects hook了这个selector(Aspects内部会记录下自己hook的所有selector)。这部分代码意思是,当Aspects没hook这个selector的时候,就调用原来的forwardInvocation实现。大家注意我用红圈圈起来的地方,就知道进来的方法就是Jspatch hook的viewWillAppear。

那么originalForwardInvocationSEL, 肯定保留了Jspatch的实现,找一下源头。搜索AspectsForwardInvocationSelectorName就可以找到了。

很明显,Aspects在替换原来forwardInvocation的时候,把原来的实现保存在AspectsForwardInvocationSelectorName,这是一个新增方法。用图表示下

现象分析之步骤五

看样子,Aspects处理还是很完美,但是往往是看上去完美的地方,还是会出现问题,该来的终究回来。

最后来解释那个assert。步骤五与步骤四不同的地方在于,Aspects替换的是具体对象的方法。

经过调试,发现在获取的Jspatch原有实现是空! 注意红框里的。AspectsForwardInvocationSelectorName也不会添加成功。

问题出在class_replaceMethod这个方法上

文档上表明,这个函数的返回值是,一个类定义了的方法的实现,它不会去找super class。然而这个class_replaceMethod的第一个参数klass是Aspects动态创建的一个Viewcontroller的子类(为何动态创建子类也是为方便的回复原状)。你控制台上po klass,会输出ViewController_Aspects_。然而定义forwardInvocation的类是Viewcontroller( Jspatch有重新实现过Viewcontroller的forwardInvocation)

回到那个assert

由于origin forwardInvoca 为nil,AspectsForwardInvocationSelectorName这方法也就不复存在了。因此走到了那个assert。

解决方案

到目前为止,原因已经全部查明,接下来看看有什么办法来解决步骤五遇到的问题,也就是我们项目中遇到的问题。

我们首先任务就是解决,获取的Jspatch原有实现为nil的问题,既然class_replaceMethod不能够上诉super class去获取实现,我们使用另一个获取实现的方法class_getInstanceMethod, class_getInstanceMethod文档描述可以获取父类实现。

修改代码

static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:";
 static void aspect_swizzleForwardInvocation(Class klass) {
     NSCParameterAssert(klass);
-    // If there is no method, replace will act like class_addMethod.
-    IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
-    if (originalImplementation) {
-        class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
+    // get origin forwardInvocation impl, include superClass impl,not NSObject impl, and class method to kClass
+    Method originalMethod = class_getInstanceMethod(klass, @selector(forwardInvocation:));
+    if (originalMethod !=  class_getInstanceMethod([NSObject class], @selector(forwardInvocation:))) {
+        IMP originalImplementation = method_getImplementation(originalMethod);
+        if (originalImplementation) {
+            // If there is no method, replace will act like class_addMethod.
+            class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
+        }
     }
+    class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");

获取原有实现,排除[NSObject forwardInvocation], 因为搜索父类,会搜到[NSObject forwardInvocation],这个我们不需要addMethod,我们只关心,我们重写的。

这样Jspatch的实现就会找到,看图中红框,很明显名字看出来那就是Jspatch的实现,我们找到了

不过当再次运行的时候,assert还是会跳到,警报还没有完全解除。

respondsToSelector仍不能认识AspectsForwardInvocationSelectorName这个方法。

查文档发现
respondsToSelector实质会去调用class获取类是Viewcontroller(因此子类的class方法被Aspects重写为基类的,为了动态创建子类对用户是透明的),然后查看这个类的方法列表。然而由于Aspects的动态创建子类,AspectsForwardInvocationSelectorName添加到了Aspects的动态创建的子类ViewController_Aspects_上!难怪找不到了!

我们仍然使用class_getInstanceMethod

 if (!respondsToAlias) {
     invocation.selector = originalSelector;
     SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
-        if ([self respondsToSelector:originalForwardInvocationSEL]) {
-            ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
-        }else {
+        Method method = class_getInstanceMethod(object_getClass(self), originalForwardInvocationSEL);
+        if (method) {
+            typedef void (*FuncType)(id, SEL, NSInvocation *);
+            FuncType imp = (FuncType)method_getImplementation(method);
+            imp(self, selector, invocation);
+        } else {
         [self doesNotRecognizeSelector:invocation.selector];
     }

运行...

冲突解决了!

总结

Jspatch和Aspects冲突问题和解决方案调查完毕,如果你有遇到和我们一样的问题,可以考虑我的修复方案。Jspatch和Aspects都是hack的方案,大家在开发过程中,还是尽量少用,我们目前项目中hack了很多系统控件,完成需求,这使得系统升级的时候,我们变得非常被动,因为我们的hack不知道会出现什么问题。

注意

我还尝试过在jspatch的js里,写aspects修改方法,这样是不起作用的,jspatch会自己修改block参数个数,Aspects发现参数不一致, 是不会执行的,因此不要试图这样做

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容