凡铁游戏提供最新游戏下载和手游攻略!

Android应用热修复技术Robust框架深度解析(续篇)

发布时间:2024-10-16浏览:89

本篇继续来看热修复框架Robust原理,在之前的一篇文章中已经详细讲解了Robust框架原理,因为这个框架不是开源的,所以通过官方给出的原理介绍,咋们自己模拟了案例和框架逻辑的简单实践。最后在通过反编译美团app进行验证咋们的逻辑实现是否大致不差。最终确定实践的逻辑大同小异。但是在上一篇文章末尾多次强调了,这个框架吸引我研究的不是他热修复技术,而是他有一个技术点,就是如何在编译期给每个类每个方法都加上修复功能代码,对于上层开发代码是透明的。因为从之前案例可以看到,如果方法没有修复功能代码,那么此方法就丧失了修复功能,再来看一下这个框架的原理图,包括编译期动态插入代码和加载修复包逻辑:

二、自动插入原理分析

为了演示和填坑方便,咋们最好开始使用一个简单的案例来,因为第一次谁都保证不了能一帆风顺插入成功。所以这里就用一个简单的类文件进行即可。这里定义一个简单的类Person,内部定义多个不同类型的方法,包括方法的返回值,参数,类型等。这也是为了后续检测我们插入代码的各种情形是否都能成功。我们的目的也只有一个,就是如何动态给Person这个类中每个方法插入之前提到的动态代码:

if(changeQuickRedirect != null){ if(PatchProxy.isSupport(new Object[]{xxx,xxx,...}, this, changeQuickRedirect, false)){ return ((XXX) PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)); } }

在类中插入一个静态变量:

public static ChangeQuickRedirect changeQuickRedirect;

咋们定义的Person类如下:

这个类非常简单,定义了很多不同类型格式的方法,下面我们就要来编写代码,自动给每个方法注入那段修复代码以及给这个类添加一个静态变量。有了之前的那篇文章:Android中动态插入代码工具icodetools,我们操作就很简单了,这里依然需要借助asm包和Eclipse的插件Bytecode,咋们直接利用Bytecode插件查看那段代码的asm对应的代码,不过这里需要注意,每个方法插入的代码不同,先来看修复代码的两个重要方法:isSupport和accessDispatch,这两个方法都有四个参数:

第一个参数:Object数组,存放的是这个方法的所有参数值,看到如果是基本类型需要做封箱转化。

第二个参数:当前方法所属的对象,如果方法是static类型就是null,如果方法是非static的就是this。

第三个参数:修复接口类型,也就是我们需要插入的静态变量changeQuickRedirect。

第四个参数:方法是否为static类型。

所以从上面这四个参数,就知道我们在插入代码时需要做如下处理,主要包括以下几点:

1、每个方法的参数不同,因为我们看到插入的修复代码的isSupport和accessDispatch方法的第一个参数都是一个Object数组,也就是这个方法的所有参数。

3、方法的返回值不同,对于方法是否有返回值需要做特殊处理,以及方法返回值类型不同也要做处理。

主要是这三点,但是实际操作还有很多小的细节问题,比如参数如果是基本类型,咋们还得做封箱操作,将其变成对象类型。返回值如果是基本类型,还得做拆箱操作,把对象类型变成基本类型。

三、自动插入案例

上面分析完了基本原理,下面直接来操作,开始我们用一个简单的方法做案例,然后手动的先插入一段修复代码,在借助Bytecode插件查看这段代码对应的asm代码:

通过asm代码,我们需要注意的就是参数数组构建,和返回值转化:

下面我们可以把这段asm代码直接拷贝到Java代码中,在这个过程中,我们需要对那个参数数组构建做处理了,因为现在方法的参数个数是不确定的,所以咋们得编写动态构建代码:

这段代码就是完成了修复代码的动态插入,逻辑和顺序很清晰,首先得构造出方法的四个参数,其中最重要的就是第一个参数Object数组了。

第一个参数:构建方法参数数组

在这里还得区分,一个方法是否为有参数和无参数的情况。做特殊处理,然后最核心的地方就是创建多个参数数组类型的代码了:

上面代码,就开始创建一个方法的所有参数类型数组,需要做以下几个特殊处理:

1、因为字节码指令中常量值指令是Opcode.ICONST_0到Opcode.ICONST_5的,所以如果一个数组大小超过这个指令范围了,就得借助Opcode.BIPUSH进行操作了。

2、判断当前方法是否为static类型的,因为这个类型关系到后面取方法局部参数的索引值,我们知道非static类型的方法有一个隐含的参数this,所以这里要做一次局部参数索引值判断。static类型从0开始,非static类型从1开始。

3、在进行数组数据填充的时候,因为需要通过索引值访问,这里依然要做特殊处理,超过5通过Opcode.BIPUSH指令进行操作了。

4、对于参数处理需要区分基本类型和对象类型,因为他们采用的LOAD指令不同,一般基本类型中long是LLOAD,double是DLOAD,float是FLOAD,其他基本类型都是ILOAD;对于对象类型都是ALOAD。

5、对于参数中,如果一个参数的前面一个参数是long,double类型,要对参数索引做特殊处理,这里猜想可能和这两种类型占用的字节数有关,毕竟他们都是占用8个字节。而其他类型都是在4个字节以内的。当遇到是这种两种类型,参数索引值就得加一。

看到这里有这么多个坑,可以想到我在填坑的时候多么痛苦,但是填坑方法也是很简单的,可以先模拟定义这样的方法,然后查看他对应的asm代码即可:

这个方法就包含多个参数,而且所有特殊情况都包含了,查看asm代码即可:

这样咋们就把坑给填完了。继续看上面的代码,在处理特殊的基本类型,因为上面提到基本类型除了LOAD指令不一样,还有就是需要进行对象封箱操作,从asm代码中也可以看到,看看具体方法:

对于不同基本类型做了特殊处理,下面看一下boolean类型的处理:

其他基本类型都大致相同了,这里不再解释了。

第二个参数:当前方法所属的类对象

到这里就看完了,修复方法的第一个参数:对象数组构建,也是整个过程中的核心,也是最复杂的。咋们在回过头继续看,第二个参数:方法当前所属的对象

这里需要做判断就是方法是否为static类型,如果是static类型直接传入null即可,如果是非static类型就要直接传入隐含的第一个参数this了。

第三个参数:静态变量

这个参数就简单了,直接用类的静态变量changeQuickRedirect即可:

第四个参数:方法是否为static类型

有了上面四个参数之后,下面就可以开始调用了修复的两个方法了,一个是isSupport:

这个方法返回值是boolean类型,也就是在if语句中执行,可以用IFEQ指令即可。不过这里还有一个坑:就是如果是Bytecode插件直接得到的asm代码,方法的参数签名第一个是Ljava/lang/Object;,这个明显不对的,因为我们知道第一个参数是数组类型,所以需要手动改成[Ljava/lang/Object;,这个坑找了好久才填成功了。

然后就是accessDispatch方法调用,在调用这个方法之前,我们依然需要构造四个参数,不过这个构造过程和之前是一模一样的。直接抄过来就可以了,主要是执行完这个方法之后的事,又有好多坑:

这里看到,我们又得像上面构造那个复杂的方法参数数组一样填坑了。这里需要做这几个特殊处理:

1、方法是否有返回值,如果没有返回值,直接调用Opcode.RETURN指令即可。

2、方法返回值类型如果是基本类型需要特殊处理。

3、方法返回值类型是对象类型,需要做类型签名处理,如果是数组类型不做处理,如果是非数组类型需要去除前面的L字符,以及后面的分号字符,不然后面在使用dx命令转化jar的时候报错。

下面来看看如果返回值是基本类型,我们需要进行拆箱操作,即把对象类型变成基本类型:

代码也很简单,直接拷贝asm代码即可,对每个基本类型做判断即可。最后就是返回指令,因为不同基本类型和对象类型采用的不一样,基本类型中float类型是FRETURN,long类型是LRETURN,double类型是DRETURN,其他类型都是IRETURN,如果是对象类型直接是ARETURN即可:

四、遇到的问题

到这里我们就完成了动态代码注入的编写,整个过程可以看到有很多地方需要处理,也就是填坑,在无数次实验中遇到问题解决问题,因为如果开始把asm对应的代码拷贝过来会遇到一些问题的。不过每次遇到问题的时候解决办法也很简单,借助jd-gui工具,查看我们每次处理之后的class文件,比如这里:

这里看到,这个方法处理就报错了,其实这个就是之前遇到的坑,如果一个参数前面一个参数是long,double类型没有做特殊处理的结果。这时候发现有问题,我们可以先手动编写修复代码,然后借助Eclipse的Bytecode插件查看其对应的asm代码,和我们生成代码逻辑作比较即可。

还要一种方法,可以使用javap命令生成两个class的字节码,然后对比也可以:

然后对比这两个class文件的字节码:

不一样的地方,再继续修改指令即可。

五、踩过的坑

到这里我们就把动态插入代码的逻辑编写完毕了,总结一下我们遇到的坑:

第一、处理构造方法参数数组

1、参数个数,字节码指令常数值是ICONST_0到ICONST_5,过了这个范围,就得用BIPUSH指令。

2、基本类型需要进行封箱操作。

3、参数前面一个参数是long和double类型,需要做特殊处理。

4、基本类型和对象类型在存放值的时候用的LOAD指令不同。

第二、方法返回值处理

1、方法有无值返回。

2、返回值是基本类型需要做拆箱处理。

3、对于返回值是数组和非数组类型处理。

4、基本类型和对象类型返回指令不同。

六、包装成小工具

下面还没完,因为上面我们看到只是编写完了插入代码的工具类方法,回头可以看到,这个方法需要传入几个参数:

下面来说明这几个参数的含义:

第一个参数:操作方法的类MethodVisitor

第二个参数:方法所属类的全称名称

第三个参数:方法参数签名字符串列表

第四个参数:方法返回值类型签名

第五个参数:方法是否为static类型

下面咋们需要借助asm包中的api来处理class文件了,在之前介绍Android中动态插入代码工具icodetools 的时候,说过一句,操作类使用ClassVisitor,操作方法使用MethodVisitor即可,直接看代码:

这里可以通过方法的描述字段desc,通过Type类得到方法的参数类型和返回值类型

在这里,可以通过access字段获取方法是否为static类型,而且需要给每个类添加一个静态变量changeQuickRedirect

然后就需要借助ClassReader类,这里传入的是需要处理的类的字节数组,然后可以获取到类名。处理之后在返回类的字节数组即可。外部在封装一个方法,读写文件,所这里为了后面方便使用,编写了两个简单小工具,一个是用于单独class文件处理,一个是为了jar文件处理,只要输入源文件,输出就是处理之后的结果了:

这个项目中具体代码就不多解释了,后面会给出项目的下载地址,可以自己弄下来慢慢解读。但是这里需要注意一点:就是这里的ChangeQuickRedirect和PatchProxy这两个类必须和应用工程中的名称包名保持一致,不然插入是失败的。下面就简单用处理单个的class文件工具处理一下Person类:

好了,到这里,咋们就处理完了Robust框架动态插入代码的逻辑了。提供了两个工具,一个是处理jar文件,一个是处理单独class文件。那么有同学可能会困惑?美团项目应该还有其他操作吧。的确如此。

七、项目中如何使用工具

有了这两个工具,我们可以将其导出成jar文件,在项目编译期间开始操作,先不管项目用ant脚本,还是gradle脚本了,不了解用脚本编译Android应用的同学,可以查看这里:Android中使用脚本编译应用;用脚本编译项目都是需要经历这么几个阶段的:

1、使用Android SDK提供的aapt.exe生成R.java类文件

2、使用Android SDK提供的aidl.exe把.aidl转成.java文件(如果没有aidl,则跳过这一步)

3、使用JDK提供的javac.exe编译.java类文件生成class文件

4、使用Android SDK提供的dx.bat命令行脚本生成classes.dex文件

5、使用Android SDK提供的aapt.exe生成资源包文件(包括res、assets、androidmanifest.xml等)

6、使用Android SDK提供的apkbuilder.bat生成未签名的apk安装文件

7、使用jdk的jarsigner.exe对未签名的包进行apk签名

那么脚本是我们自己控制的,所以可以在两个阶段选择处理,也就有两个方案:

第一个方案:只需要在将java文件用javac命令编译成class文件之后,利用上面的那个可以处理单个class文件工具进行处理即可。这样对于开发人员其实是无感知的。在编译阶段自动完成了。

第二个方案:在编译所有文件得到class文件之后,将其打包成jar文件,然后在借助上面提到的处理jar文件工具进行处理即可。然后在使用dx命令将处理之后的jar文件变成dex文件即可。

八、优化工作

到这里我们就算把美团的Robust框架中动态插入修复代码的逻辑讲解完了,但是这里还有一些细节问题需要处理:

1、添加黑名单规则,我们可以看到,这个动态插入代码段是为了修复作用,那么一个apk中所有类是否都有必要插入呢?明显不需要,比如我们用到了v4包中的类,那么这里的类肯定不需要插入的。当然还有一些我们自己定义的类的一些方法也不想插入的。所以这里就要有一个插入时的黑名单,这个需要在上面插入工具里做处理,比较简单,因为我们知道处理的方法名和类名了,只是做一个简单过滤即可。

2、做插入前判断,从上面看到每个类需要有一个changeQuickRedirect变量,这个变量名是唯一的,但是又不能保证在开发过程中,每个开发人员都会使用这个名字,如果有人使用了,而我们又自动插入了,那么编译肯定会报错的。所以我们在插入代码之前需要做一些判断逻辑。如果有这个变量就不插入了。并且给与一些信息提示。

九、框架优缺点

优点:在之前一篇文章中已经知道他的加载逻辑非常简单,直接使用DexClassLoader类加载修复包即可。所以可以看到这个修复框架的兼容性非常好。因为直接使用系统提供的api,不会有很高的崩溃率,不像AndFix框架借助底层,会有系统限制需要做兼容操作的。

项目下载地址:

https://github.com/fourbrother/RobustInsertCodeTools

十、总结

手机看文章有点费劲,可以进入网页版:http://www.wjdiankong.cn

用户评论

炙年

这款《Android热修复 Robust框架》的下篇解析让我大开眼界,深入浅出地讲解了热修复在Flutter项目中的实践

    有8位网友表示赞同!

墨染天下

作为一个热衷于深入研究技术细节的开发者,《下篇》的内容十分丰富,提供了很多实操性的见解和建议。

    有18位网友表示赞同!

苏樱凉

看了《Android热修复Robust框架原理解析(下篇)》之后,我对Robust框架背后的机制有了更深刻的理解,准备应用于我的项目中。

    有5位网友表示赞同!

墨染殇雪

对于那些渴望优化应用启动速度、提升用户体验的开发者,《下篇》是一本必读的技术文章。

    有15位网友表示赞同!

景忧丶枫涩帘淞幕雨

《Android热修复 Robust框架原理解析(下篇)》中的实验案例让我对热修复技术的应用有了全新的视角,特别是如何在生产环境中灵活应对。

    有5位网友表示赞同!

莫飞霜

想要掌握Android中热修复的高级用法和最佳实践?这本《下篇》是你的不二之选,提供了详细的代码样例和优化策略。

    有14位网友表示赞同!

代价是折磨╳

看完《Android热修复Robust框架原理解析(下篇)》,感觉自己对Flutter框架的理解更深了一层,特别是如何实现高效的远程调试和热更新。

    有12位网友表示赞同!

夏日倾情

对于那些寻求提升项目稳定性和上线效率的开发人员,《下篇》中提到的一些调优技巧让我眼前一亮。

    有7位网友表示赞同!

花容月貌

《Android热修复 Robust框架原理解析(下篇)》不仅内容充实,而且案例丰富,是解决实际问题时的好帮手。

    有14位网友表示赞同!

一别经年

从理论到实战,《下篇》彻底解答了我关于Android热修复技术的所有疑问,对于优化移动应用至关重要。

    有5位网友表示赞同!

花菲

作为一名 Flutter 开发者,看完《Android热修复 Robust框架原理解析(下篇)》,我发现自己在项目管理方面还有很大的进步空间。

    有5位网友表示赞同!

别留遗憾

强烈推荐给所有想要深入理解和掌握 Android 热修复技术的开发者,《下篇》提供了最新的框架特性与实践指南。

    有17位网友表示赞同!

暖瞳

读了《Android热修复 Robust框架原理解析(下篇)》,我对如何选择和配置热修复机制有了更清晰的认识,非常实用。

    有7位网友表示赞同!

青衫故人

对于寻求更高效开发流程的团队来说,《下篇》中关于优化跨平台应用的技术分享令人受益匪浅。

    有10位网友表示赞同!

虚伪了的真心

《Android热修复 Robust框架原理解析(下篇)》让我认识到在构建稳定可靠的移动应用时,热修复是一种不可或缺的技术。

    有5位网友表示赞同!

病态的妖孽

看完《下篇》,我对如何通过热修复提升用户体验、减少用户等待时间有了新的思考和策略规划。

    有20位网友表示赞同!

我的黑色迷你裙

对于追求极致性能的开发者,《Android热修复 Robust框架原理解析(下篇)》是探索Flutter深层潜力的重要资源。

    有7位网友表示赞同!

良人凉人

作为游戏开发的一部分,《下篇》中关于热更新对游戏流畅性的提升提供了具体案例,让我倍感启发。

    有14位网友表示赞同!

此刻不是了i

《Android热修复Robust框架原理解析(下篇)》不仅解答了我心中的困惑,还开拓了我的技术视野,是值得每一个开发者学习的内容。

    有20位网友表示赞同!

热点资讯