Android中热修复框架Robust原理解析+并将框架代码从”闭源”变成”开源”(上篇)

Android技术篇 尼古拉斯.赵四 10368℃ 0评论

一、前言

Android中热修复框架比较多,每家公司都有对应的方案和框架,比如阿里的AndFix框架,关于这个框架在之前的文章已经详细讲解了,不了解的同学可以点击这里:AndFix热修复框架原理分析 。本文继续来看另外一个热修复框架,也就是美团团队开发的Robust框架。关于这个框架网上已经有详细解释了,具体用法也有。不过他没有开源,所以本文就先简单介绍他的原理,用一个案例来演示这个框架的作用,但是重点是咋们自己编码将其框架机制实现,让其”闭源”变成”开源”。

 

二、原理解析

关于热修复技术点,其实虽然每家都有对应的框架,但是核心点都离不开动态加载机制。有了动态加载机制,然后就是具体修复方案问题了,对于Robust修复方案也是比较简单的。下面来简单看一下他的大致原理:

Robust插件对每个产品代码的每个函数都在编译打包阶段自动的插入了一段代码,插入过程对业务开发是完全透明。如State.java的getIndex函数:

  1. public long getIndex() {
  2.     return 100;
  3. }

被处理成如下的实现:

  1. public static ChangeQuickRedirect changeQuickRedirect;
  2.     public long getIndex() {
  3.         if(changeQuickRedirect != null) {
  4.             //PatchProxy中封装了获取当前className和methodName的逻辑,并在其内部最终调用了changeQuickRedirect的对应函数
  5.             if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {
  6.                 return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();
  7.             }
  8.         }
  9.     return 100L;
  10. }

可以看到Robust为每个class增加了个类型为ChangeQuickRedirect的静态成员,而在每个方法前都插入了使用changeQuickRedirect相关的逻辑,当changeQuickRedirect不为null时,可能会执行到accessDispatch从而替换掉之前老的逻辑,达到fix的目的。如果需将getIndex函数的返回值改为return 106,那么对应生成的patch,主要包含两个class:PatchesInfoImpl.java和StatePatch.java。

PatchesInfoImpl.java:

  1. public class PatchesInfoImpl implements PatchesInfo {
  2.     public List<PatchedClassInfo> getPatchedClassesInfo() {
  3.         List<PatchedClassInfo> patchedClassesInfos = new ArrayList<PatchedClassInfo>();
  4.         PatchedClassInfo patchedClass = new PatchedClassInfo(“com.meituan.sample.d”, StatePatch.class.getCanonicalName());
  5.         patchedClassesInfos.add(patchedClass);
  6.         return patchedClassesInfos;
  7.     }
  8. }

 

StatePatch.java:

  1. public class StatePatch implements ChangeQuickRedirect {
  2.     @Override
  3.     public Object accessDispatch(String methodSignature, Object[] paramArrayOfObject) {
  4.         String[] signature = methodSignature.split(“:”);
  5.         if (TextUtils.equals(signature[1], “a”)) {//long getIndex() -> a
  6.             return 106;
  7.         }
  8.         return null;
  9.     }
  10.     @Override
  11.     public boolean isSupport(String methodSignature, Object[] paramArrayOfObject) {
  12.         String[] signature = methodSignature.split(“:”);
  13.         if (TextUtils.equals(signature[1], “a”)) {//long getIndex() -> a
  14.             return true;
  15.         }
  16.         return false;
  17.     }
  18. }

客户端拿到含有PatchesInfoImpl.java和StatePatch.java的patch.dex后,用DexClassLoader加载patch.dex,反射拿到PatchesInfoImpl.java这个class。拿到后,创建这个class的一个对象。然后通过这个对象的getPatchedClassesInfo函数,知道需要patch的class为com.meituan.sample.d(com.meituan.sample.State混淆后的名字),再反射得到当前运行环境中的com.meituan.sample.d class,将其中的changeQuickRedirect字段赋值为用patch.dex中的StatePatch.java这个class new出来的对象。这就是打patch的主要过程。通过原理分析,其实Robust只是在正常的使用DexClassLoader,所以可以说这套框架是没有兼容性问题的。

下面直接上一张图来看看人家的原理结构(点击下载查看高清大图):

再来看一下美团team公开的原理图:

原理真的很简单:直接用DexClassLoader加载修复包,然后用loadClass方法加载修复类,new出新对象,在用反射把这新的修复对象设置到指定类的changeQuickRedirect静态变量中即可。其实这种修复类似于Java中的设计模式之静态代理。

 

三、案例实践

知道了大致原理,下面就用一个简单的案例来运行,看到效果,这里我们需要新建三个项目,直接拷贝之前开发插件的那个项目即可,可以看这篇文章:Android中插件开发原理解析,三个项目图结构如下:

本文依然采用这种结构图如下:

咋们这里主要看RobustHost,RobustPatch,RobustPatchImpl这三个工程,RobustInsertCodeTools是个Java工程用于后面会重点介绍的动态插入代码工具。

第一、修复包接口工程RobustPatch

这个工程比较简单,大部分都是接口定义,主要有这三个类和接口:

1、ChangeQuickRedirect接口

这个接口主要是后面修复包中每个修复类必须实现的一个接口,内部有两个方法:一个是判断当前方法是否支持修复,一个方法是具体的修复逻辑了。

两个方法的参数都是一样的:

第一个参数是方法的签名,这个签名的格式很简单:方法所属类全称:方法名:方法是否为static类型,注意中间使用冒号进行连接的。

第二个参数是方法的参数信息,而对于这个参数后面分析动态插入代码逻辑的时候会发现操作非常麻烦,才把这个参数弄到手的。

2、PatchesInfo接口

这个接口比较简单,只有一个方法,用户存放一个修复包中所有待修复类的具体信息,因为一个修复包中可能有多个需要修复的类。这个接口也是后面宿主工程中动态加载需要用到的。通过这个接口获取到指定修复的类和旧类信息。

3、PatchedClassInfo类

这个类比较简单,主要就是存放两个字段:一个是本次修复类信息,一个是需要修复旧类信息,让这个类会被上面的接口用到。

第二、修复包工程

到这里咋们就看完了修复包接口工程。下面继续来看一下修复包工程:

修复包工程比较简单,直接增加修复类即可,这里可以看到,我们需要修复宿主工程中的一个MoneyBean类,这个类必须实现上面的修复包接口工程中的ChangeQuickRedirect接口,然后就是需要保存修复类信息和待修复旧类信息,在PatchesInfoImpl中进行保存返回的,而这个类实现了修复包接口工程中的PatchesInfo接口。

1、MoneyBeanStatePatch修复类

这个类看到了,实现了修复接口ChangeQuickRedirect,然后实现具体两个方法即可。

1》在isSupport方法中,会通过方法的签名信息得到方法名,然后判断支持修复的方法。这里看到的是待修复类中的getMoneyValue和desc这两个方法需要进行修复。

2》accessDispatch方法中,主要进行了具体修复方案实行了,开始也是先通过方法签名信息,得到方法名,然后在判断哪些方法需要进行修复,这里把getMoneyValue方法的返回值修复成了10000,把desc方法的返回值修复成了”Patch Desc”。

第三、宿主工程RobustHost

宿主工程最大的工作就是需要加载修复包,这个不用多解释了,放在Application中即可。加载逻辑没什么可说的。这里我们有一个MoneyBean类:

这个类的每个方法之前都加上了具备修复功能的代码段。比如现在如果线上的包中这个类的方法返回值出了问题,这时候就可以借助热修复功能。把第二个修复包工程打包好发布出去即可。

再来看一下这里有一个PatchProxy类:

这个类其实是对修复类进行了包装了,内部有一个重要逻辑就是获取当前执行方法的信息,包括类名和方法名,这里主要是通过方法栈信息得到的:

然后就是宿主工程中的加载修复包逻辑了:

使用DexClassLoader进行加载修复包文件,不多解释了。有了加载器就开始后续的加载逻辑了。首先咋们得获取到PatchesInfoImpl类信息,因为这个类中保存了修复类信息。这里是new出一个新对象。然后就调用其方法得到修复类的相信信息,依次遍历修复类信息列表,得到修复类和待修复旧类信息,依然使用反射new出新对象,再把修复类对象设置到待修复旧类的静态变量changeQuickRedirect中即可。

 

四、运行效果

好了,到这里我们就分析完了Robust热修复框架中涉及的工程,主要就是三个工程。下面开始直接运行结果了。不过咋们还是得先弄出修复包,这个比较简单,直接运行修复包RobustPatchImpl工程,得到apk文件即可,本文中加载的是dex文件,所以咋们把这个apk中的classes.dex文件弄出来,然后在改成patch.dex放到设备的sd卡目录下即可。运行的时候可能会报错:

关于这个错误这里,不在多解释了,因为咋们把修复包接口工程的代码也加到修复包dex中了。具体错误原因和解决办法,一定要去这里找:Android中插件开发原理。记得是一定哦。

到这里假设我们已经解决了所有问题,下面运行看看效果:

咋们再看看没有修复之前的效果:

从这里可以看到,咋们就修复方法成功了。

 

五、分析美团的实践案例

不过咋们还没有结束工作,因为美团的这个框架没有开源,所以上面的案例并不能具备说服性。所以为了验证我们的实现逻辑是没有错的,咋们需要反编译美团的app了,看他的热修复逻辑是什么样的。这里直接使用Jadx工具打开美团app即可:

咋们打开app之后,可以直接全局搜索PatchProxy类,记得要勾选上Class,不然内容太多了。然后找到了这个类的定义:

看到这个类的实现了,其实到这里我想告诉大家,我的案例工程中的PatchProxy类就是把这个类导出来的,因为我懒得写代码。正好这个类又没有混淆。

咋们继续来看他的加载代码逻辑,这里有个小技巧了:在查看一个应用中动态加载逻辑的时候,可以全局搜索信息:DexClassLoader,如下结果:

其实就是美团的PatchExecutor类,点进去直接查看:

看到这里的核心加载逻辑,不多解释,代码逻辑很简单,大致和我们实现的差不多了。最后咋们再来看一下app中的类中的每个方法是否插入了修复代码段:

随便打开一个类,类中的每个方法之前都有这段修复代码。哈哈,到这里我们就非常确定了,我们的案例实现和美团的修复框架实现逻辑大致不差了。

 

六、自动插入修复代码

其实到这里还不算结束,因为这篇文章其实不是我想介绍Robust框架的用意,因为大家看完上面的原理之后,发现其实修复原理没多大难度的。而我的用意是如何让每个类中的每个方法都插入一段修复代码。

因为从上面我们可以看到,这个框架的最大难度是在每个类每个方法之前都必须有修复代码段。如果没有那么方法就丧失了修复功能。但是问题在于,一个庞大的项目比如美团,如何能够保证每个开发人员在编写一个方法的时候都需要手动加上这段修复代码呢?这个是不可能约束成功的。所以这个就需要借助自动化操作了,其实在美团官方说明这个框架的时候提到依据:“插入代码是在编译期自动进行的,对于开发者是透明的”。所以这句话对我的诱惑性最大,因为这句话我研究了这个框架。所以限于篇幅原因,不能在一篇文章中讲解完了。从这里可以看到,Robust框架的核心点不在这篇,而是在下一篇文章,我会带大家手动编写如何在编译期中自动插入修复代码,无需开发人员操心。等那篇文章介绍完之后,会在详细介绍一下美团这个热修复框架Robust的优缺点。

特此说明

关于美团的这个Robust热修复框架等下一篇介绍完自动插入代码功能之后,会统一总结一下这个框架的优缺点。还是那句话,这个框架的重点其实是自动插入代码模块,或许这个就是美团没有开源的一个原因,当然也有其他原因。关于本文中实践的案例是大致上实现了Robust框架原理,但是还是有很多细节问题需要优化的。从我的三个项目代码也可以看到,写的比较粗糙。很多情况并没有考虑到。所以如果想自己完善优化的话,可以自行操作即可。

项目下载地址:https://github.com/fourbrother/Robust

 

七、总结

本文主要介绍了美团的热修复框架Robust原理,因为他没有开源,所以咋们就通过官方给出的原理解析,大致实现了简单案例,为了验证我们的案例和美团app的框架逻辑类似,咋们通过反编译美团app,核对之后确定了咋们的实现逻辑就是和Robust实现大致相同,还有具体细节问题就不说了。但是我们在实现案例的时候会发现这个框架有一个很大的约束,就是每个类的每个方法之前必须加上一段修复代码,如果没有添加该方法就失去了修复功能。当然这段代码是不可能约束每个开发人员在开发过程中手动添加的。官方给出的解释是:在项目编译阶段自动添加,对于开发人员是透明的。所以这个就是我这次解析这个框架的重点,也是下一篇的重点,如何编写工具实现自动插入修复代码。说到这里我感觉挺内疚的,本来是想在这篇文章一起都讲解完了,可是没想到文章写着写着就内容多了,真心不能在一篇中介绍了,只能分篇介绍了。不过这个也不影响咋们后面的重点篇,希望大家能够看完本篇之后保持着高度热情期待下一篇内容。小编周末吸着雾霾给大家写文章,还是希望大家能够多多分享,要有打赏就在好不过了。

 

《Android应用安全防护和逆向分析》

点击立即购买:京东  天猫

更多内容:点击这里

关注微信公众号,最新技术干货实时推送

扫一扫加小编微信
添加时注明:“编码美丽”否则不予通过!

转载请注明:尼古拉斯.赵四 » Android中热修复框架Robust原理解析+并将框架代码从”闭源”变成”开源”(上篇)

喜欢 (10)or分享 (0)
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址