前言
本文旨在通过分析源码一步步分析Robust热修复的实现原理,前半部分首先分析一下Robust思路中运用到的技术方案;后半部分多为源码部分,即Robust对于技术方案的实现与运用。
1、关于Robust
Robust is an Android HotFix solution with high compatibility and high stability. Robust can fix bugs immediately without a reboot.
2、简述Android APK生成原理
首先我们来看一下生成.apk文件时会经过的一些主要步骤:
- 资源文件打包,并生成对应的R.java文件(用于项目中对于资源文件的映射)
- 将aidl编译生成对应的java文件(Android中对于跨进程交互的一种方式)
- 将上述两类和我们编写的源码.java文件通过javac编译成.class文件
- 通过dx脚本将所有的.class文件打包成为一个.dex文件(dex文件为Android中虚拟机所需加载的文件格式)
- 通过apkbuilder将dex文件打包生成.apk文件
3、热修复基本实现思路
- source code中对每一个方法体内进行插桩
- 加载补丁包时,查找到对应方法体及类,使用
DexClassLoader
加载补丁类实现代码修复
原始代码
public long getIndex() {
return 100;
}
插桩后方法体
public static ChangeQuickRedirect changeQuickRedirect;
public long getIndex() {
if(changeQuickRedirect != null) {
//PatchProxy中封装了获取当前className和methodName的逻辑,并在其内部最终调用了changeQuickRedirect的对应函数
if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {
return ((Long)PatchProxy.accessDispatch(new Object[0], false)).longValue();
}
}
return 100L;
}
Patch类
public class StatePatch implements ChangeQuickRedirect {
@Override
public Object accessDispatch(String methodSignature,Object[] paramArrayOfObject) {
String[] signature = methodSignature.split(":");
if (TextUtils.equals(signature[1],"a")) {//long getIndex() -> a
return 106;
}
return null;
}
@Override
public boolean isSupport(String methodSignature,"a")) {//long getIndex() -> a
return true;
}
return false;
}
}
A.代码插桩
ASM技术
首先,我们需要了解一个概念,ASM。ASM是一个Java字节码层面的代码分析及修改工具,它有一套非常易用的API,通过它可以实现对现有class文件的操纵,从而实现动态生成类,或者基于现有的类进行功能扩展。在Android的编译过程中,首先会将java文件编译为class文件,之后会将编译后的class文件打包为dex文件,我们可以利用class被打包为 dex 前的间隙,插入ASM相关的逻辑对class文件进行操纵。
Groovy Transform
Google在Gradle 1.5.0后提供了一个叫Transform
的API,它的出现使得第三方的Gradle Plugin可以在打包dex之前对class文件进行进行一些操纵。我们本次就是要利用Transform API
来实现这样一个Gradle Plugin
。
Transform具体操作的节点如上图所示,对于打包生成dex文件前的.class文件进行拦截,我们来看一看Transform为我们提供的可实现的方法:
表格中可见,getNam
、getInputTypes
等方法均为Transform
的配置项,最后的transform
方法需要重点关注,我们想要实现上面的拦截.class文件并进行代码插桩操作就需要在此方法中实现。其中着重看一下方法的型参inputs
,通过inputs
可以拿到所有的class文件。inputs
中包括directoryInputs
和jarInputs
,directoryInputs
为文件夹中的class文件,而jarInputs
为jar包中的class文件。
B.加载补丁
DexClassLoader
关于Java中的ClassLoader
,大家熟知的基础概念就是通过一个类的全名加载得到这个类的class对象,进而可以得到其实例对象。 在Android中的ClassLoader
基本运作远离与Java中类似,不过开发者无法自己实现ClassLoader
进行自定义操作,官方的api为我们提供了两个ClassLoader
的子类,PathClassLoader
与 DexClassLoader
。虽然两者继承于BaseDexClassLoader
,BaseDexClassLoader
继承于ClassLoader
,但是前者只能加载已安装的Apk里面的dex
文件,后者则支持加载apk
、dex
以及jar
,也可以从SD卡里面加载。 从上述的概念我们可以得知,想要实现一个热修复的方案,就需要依赖外部的dex文件,那么就需要使用 DexClassLoader
来帮助实现。 我们来看一下 DexClassLoader
的构造方法:
4、Robust的实现
A.代码插桩
这一节中,我们来看一下Robust的具体实现方案以及一些大致的接入流程。 上一节讲述了Robust热修复方案中需要运用到的技术,接下来我们来看一看Robust的具体代码逻辑,如何将上述的思路融汇。 首先看一下Robust为接入用户提供的一个配置相关的文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<switch>
<!--true代表打开Robust,请注意即使这个值为true,Robust也默认只在Release模式下开启-->
<!--false代表关闭Robust,无论是Debug还是Release模式都不会运行robust-->
<turnOnRobust>false</turnOnRobust>
<!--是否开启手动模式,手动模式会去寻找配置项patchPackname包名下的所有类,自动的处理混淆,然后把patchPackname包名下的所有类制作成补丁-->
<!--这个开关只是把配置项patchPackname包名下的所有类制作成补丁,适用于特殊情况,一般不会遇到-->
<!--<manual>true</manual>-->
<manual>false</manual>
<!--是否强制插入插入代码,Robust默认在debug模式下是关闭的,开启这个选项为true会在debug下插入代码-->
<!--但是当配置项turnOnRobust是false时,这个配置项不会生效-->
<forceInsert>true</forceInsert>
<!--<forceInsert>false</forceInsert>-->
<!--是否捕获补丁中所有异常,建议上线的时候这个开关的值为true,测试的时候为false-->
<catchReflectException>true</catchReflectException>
<!--<catchReflectException>false</catchReflectException>-->
<!--是否在补丁加上log,建议上线的时候这个开关的值为false,测试的时候为true-->
<patchLog>true</patchLog>
<!--<patchLog>false</patchLog>-->
<!--项目是否支持progaurd-->
<!--<proguard>true</proguard>-->
<proguard>false</proguard>
<!--项目是否支持ASM进行插桩,默认使用ASM,推荐使用ASM,Javaassist在容易和其他字节码工具相互干扰-->
<useAsm>true</useAsm>
<!--<useAsm>false</useAsm>-->
</switch>
<!--需要热补的包名或者类名,这些包名下的所有类都被会插入代码-->
<!--这个配置项是各个APP需要自行配置,就是你们App里面你们自己代码的包名,
这些包名下的类会被Robust插入代码,没有被Robust插入代码的类Robust是无法修复的-->
<packname name="hotfixPackage">
<name>operation.enmonster.com.gsoperation</name>
<name>com.enmonster.lib.shop</name>
</packname>
<!--不需要Robust插入代码的包名,Robust库不需要插入代码,如下的配置项请保留,还可以根据各个APP的情况执行添加-->
<exceptPackname name="exceptPackage">
</exceptPackname>
<!--补丁的包名,请保持和类PatchManipulateImp中fetchPatchList方法中设置的补丁类名保持一致( setPatchesInfoImplClassFullName("com.meituan.robust.patch.PatchesInfoImpl")),
各个App可以独立定制,需要确保的是setPatchesInfoImplClassFullName设置的包名是如下的配置项,类名必须是:PatchesInfoImpl-->
<patchPackname name="patchPackname">
<name>com.meituan.robust.patch</name>
</patchPackname>
<!--自动化补丁中,不需要反射处理的类,这个配置项慎重选择-->
<noNeedReflectClass name="classes no need to reflect">
</noNeedReflectClass>
</resources>
接下来看一下Robust自己实现的Transform API
内做了什么
class RobustTransform extends Transform implements Plugin<Project> {
def robust
InsertcodeStrategy insertcodeStrategy;
@Override
void apply(Project target) {
...
initConfig()
project.android.registerTransform(this)
}
def initConfig() {
...
/*对文件进行解析*/
for (name in robust.packname.name) {
hotfixPackageList.add(name.text());
}
for (name in robust.exceptPackname.name) {
exceptPackageList.add(name.text());
}
for (name in robust.hotfixMethod.name) {
hotfixMethodList.add(name.text());
}
for (name in robust.exceptMethod.name) {
exceptMethodList.add(name.text());
}
...
}
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
...
def box = ConvertUtils.toCtClasses(inputs, classPool)
...
insertcodeStrategy.insertCode(box, jarFile);
writeMap2File(insertcodeStrategy.methodMap, Constants.METHOD_MAP_OUT_PATH)
...
}
private void writeMap2File(Map map, String path) {
...
}
}
在初始化方法apply
中,进行了配置文件的解析,就是上述.xml文件中关于Robust的一些配置项; 在transform
中,通过inputs
方法过滤筛选得到一个box
变量,是一个类型为CtClass
的集合;
CtClass
是Javassist
框架中的类,Javassist
是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。Java字节码存储在名叫class file
的二进制文件里。每个class文件包含一个Java类或者接口。Javassit.CtClass
是一个class
文件的抽象表示。一个CtClass(compile-time class)
对象可以用来处理一个class
文件。
而后的代码插桩逻辑都放在了 insertcodeStrategy
方法内进行; 在代码插桩结束后,将名为 methodMap
的集合写入到文件 methodsMap.robust
中; methodsMap
文件用于后续生成patch.jar文件时,查找方法映射。
@Override
protected void insertCode(List<CtClass> box, File jarFile) throws IOException, CannotCompileException {
ZipOutputStream outStream = new JarOutputStream(new FileOutputStream(jarFile));
//get every class in the box,ready to insert code
for (CtClass ctClass : box) {
//change modifier to public,so all the class in the apk will be public,you will be able to access it in the patch
ctClass.setModifiers(AccessFlag.setPublic(ctClass.getModifiers()));
if (isNeedInsertClass(ctClass.getName()) && !(ctClass.isInterface() || ctClass.getDeclaredMethods().length < 1)) {
//only insert code into specific classes
zipFile(transformCode(ctClass.toBytecode(), ctClass.getName().replaceAll("\\.", "/")), outStream, "/") + ".class");
} else {
zipFile(ctClass.toBytecode(), "/") + ".class");
}
ctClass.defrost();
}
outStream.close();
}
public byte[] transformCode(byte[] b1, String className) throws IOException {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassReader cr = new ClassReader(b1);
ClassNode classNode = new ClassNode();
Map<String, Boolean> methodInstructionTypeMap = new HashMap<>();
cr.accept(classNode, 0);
final List<MethodNode> methods = classNode.methods;
for (MethodNode m : methods) {
InsnList inList = m.instructions;
boolean isMethodInvoke = false;
for (int i = 0; i < inList.size(); i++) {
if (inList.get(i).getType() == AbstractInsnNode.METHOD_INSN) {
isMethodInvoke = true;
}
}
methodInstructionTypeMap.put(m.name + m.desc, isMethodInvoke);
}
InsertMethodBodyAdapter insertMethodBodyAdapter = new InsertMethodBodyAdapter(cw, className, methodInstructionTypeMap);
cr.accept(insertMethodBodyAdapter, ClassReader.EXPAND_FRAMES);
return cw.toByteArray();
}
该方法内遍历了上述操作中过滤得到的CtClass
对象集合,首先通过 isNeedInsertClass
方法判断是否需要进行代码插桩,若需要则进入 transformCode
方法;
该方法内其中首先遍历方法节点集合,遍历过滤指令集类型,将method
类型的指令集存入 methodInstructionTypeMap
中,该map
中value
值为该method
是否有被调用,如果没有被调用的方法,value
则为false
。接下来的代码插桩操作具体实现在 InsertMethodBodyAdapter
中进行;
在InsertMethodBodyAdapter
中主要执行了两部操作:
- 在class对象中,注入一个名为
ChangeQuickRedirect
的成员变量 - 在过滤后的方法体中,注入一段代码逻辑
至此,Robust的代码插桩实现完毕。
B.加载补丁
Robust关于DexClassLoader的具体运用在PatchExecutor类中
public class PatchExecutor extends Thread {
@Override
public void run() {
//...
//拉取补丁列表
List<Patch> patches = fetchPatchList();
//应用补丁列表
applyPatchList(patches);
}
protected void applyPatchList(List<Patch> patches) {
...
for (Patch p : patches) {
if (p.isAppliedSuccess()) {
continue;
}
if (patchManipulate.ensurePatchExist(p)) {
boolean currentPatchResult = false;
try {
currentPatchResult = patch(context, p);
} catch (Throwable t) {
robustCallBack.exceptionNotify(t, "class:PatchExecutor method:applyPatchList line:69");
}
}
}
}
protected boolean patch(Context context, Patch patch) {
//...
ClassLoader classLoader = new DexClassLoader(patch.getTempPath(), dexOutputDir.getAbsolutePath(),
null, PatchExecutor.class.getClassLoader());
//加载dex文件中的PatchesInfoImpl类文件
Class patchesInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());
PatchesInfo patchesInfo = (PatchesInfo) patchesInfoClass.newInstance();
//拿到其中的本次需要的补丁类集合
List<PatchedClassInfo> patchedClasses = patchesInfo.getPatchedClassesInfo();
//...
boolean isClassNotFoundException = false;
Class patchClass, sourceClass;
for (PatchedClassInfo patchedClassInfo : patchedClasses) {
//目标类className
String patchedClassName = patchedClassInfo.patchedClassName;
//补丁类className
String patchClassName = patchedClassInfo.patchClassName;
try {
sourceClass = classLoader.loadClass(patchedClassName.trim());
} catch (ClassNotFoundException e) {
isClassNotFoundException = true;
continue;
}
Field[] fields = sourceClass.getDeclaredFields();
//遍历目标类,找到其中的ChangeQuickRedirect字段
Field changeQuickRedirectField = null;
for (Field field : fields) {
if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), sourceClass.getCanonicalName())) {
changeQuickRedirectField = field;
break;
}
}
if (changeQuickRedirectField == null) {
robustCallBack.logNotify("changeQuickRedirectField is null,patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:147");
continue;
}
//通过反射,对ChangeQuickRedirect字段进行赋值
patchClass = classLoader.loadClass(patchClassName);
Object patchObject = patchClass.newInstance();
changeQuickRedirectField.setAccessible(true);
changeQuickRedirectField.set(null, patchObject);
}
if (isClassNotFoundException) {
return false;
}
return true;
}
}
上述代码中可以看到PatchExecutor
继承了Thread
接口,内部操作都是异步执行的;
入口方法中首先获取到开发者所配置的补丁文件位置等信息集合,接下来在 applyPatchList
方法中遍历补丁文件,依次执行 patch
方法;
该方法中首先通过补丁中的 getPatchesInfoImplClassFullName
获取到 PatchsInfoImpl
的类全名,并使用 DexClassLoader
加载并实例化;
在该类中通过 getPatchedClassesInfo
方法中可以拿到 PatchedClassInfo
对象的集合,此对象中包含本次需要修复的类信息以及修复使用的补丁类信息,分别对应变量 sourceClass
和 patchClass
;
首先加载 sourceClass
类,遍历该类中所有的字段,拿到前文中代码插桩环节中注入的 ChangeQuickRedirect
变量,再加载 patchClass
,将该对象赋值给 ChangeQuickRedirect
变量;
至此,加载补丁逻辑结束,需修复的补丁类中 ChangeQuickRedirect
被赋值,方法体内的插桩逻辑将会执行并修复原有逻辑。
结尾
谢谢大家看完,如有不恰当、不充分的地方,欢迎大家指正。针对这块知识点我在学习过程中,进行了详细的整理梳理了一些学习笔记,有需要参考学习的小伙伴可以 点击这里查看获取方式 传送门直达 !!!
原文地址:https://blog.csdn.net/weixin_61845324
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。