本篇继续来看热修复框架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位网友表示赞同!
《Android热修复Robust框架原理解析(下篇)》不仅解答了我心中的困惑,还开拓了我的技术视野,是值得每一个开发者学习的内容。
有20位网友表示赞同!