前言

本文旨在通过分析源码一步步分析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], this, changeQuickRedirect, 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, Object[] paramArrayOfObject) {
        String[] signature = methodSignature.split(":");
        if (TextUtils.equals(signature[1], "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。
image.png
Transform具体操作的节点如上图所示,对于打包生成dex文件前的.class文件进行拦截,我们来看一看Transform为我们提供的可实现的方法:
方法名    含义
getInputTypes    Transform需要处理的类型
getName    Transform名称
getScopes    Transform的作用域
isIncremental    增量编译开关
transform    Transform处理文件的核心逻辑代码,此回调参数中可拿到要处理的.class文件集合

表格中可见,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 的构造方法:
参数    含义
dexPath    包含dex文件的jar包或apk文件路径
optimizedDirectory    释放目录,可以理解为缓存目录,必须为应用私有目录,不能为空
librarySearchPath    native库的路径(so文件),可为空
parent    父类加载器(ClassLoader为双亲委派的加载方式,所以需要一个父级的ClassLoader)
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, ctClass.getName().replaceAll("\\.", "/") + ".class");
        } else {
            zipFile(ctClass.toBytecode(), outStream, ctClass.getName().replaceAll("\\.", "/") + ".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 被赋值,方法体内的插桩逻辑将会执行并修复原有逻辑。

我们知道InstantRun 对应三种更新机制:

冷插拔,我们称之为重启更新机制;
温插拔,我们称之为重启Activity更新机制;
热插拔,我们称之为热更新机制。

Robust属于 Java 方案,它是基于Instant Run 热插拔实现的。
如果不熟悉 InstantRun,可参考这篇文章:《从Instant run谈Android替换Application和动态加载机制》
实现原理

有一点和Qzone类似的是,Robust也利用了Javassist库来插桩(新版本默认用ASM来插桩)。不过它的原理和Qzone不一样,Qzone是通过插桩避免类被打上CLASS_ISPREVERIFYED标志,而Robust是插桩插入代码,通过插入的changeQuickRedirect 静态变量对象判断是否绕过旧方法的执行逻辑。大致如下:

打基础包时进行代码插桩,在每个方法前插入一段类型为 ChangeQuickRedirect 静态变量的逻辑。
    打补丁包时,通过patch插件运行gradle脚本时,利用注解获取改动代码并将信息保存到patch.jar
    加载补丁时,从patch补丁包中读取要替换的类及具体替换的方法实现, 新建 ClassLoader 去加载补丁dex。

接下来通过分析两个例子,让大家更为直观地了解Robust:
sample1

(如State.java的getIndex方法)

在编译打包期间,为每个方法插入了一段补丁逻辑的代码,如下所示:

旧代码:

public long getIndex() {
        return 100;
    }

新代码添加注解:

@Modify
public long getIndex() {
        return 200;
    }

会被处理成如下的实现:

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], this, changeQuickRedirect, false)).longValue();
            }
        }
        return 100;
    }

sample2

(开源项目demo的SecondActivity)

打基础包时,Robust 为每个类新增了一个类型为 ChangeQuickRedirect 的静态变量,并且在每个方法前,增加判断该变量是否为空的逻辑,如果不为空,走打基础包时插桩的逻辑,否则走正常逻辑。打完apk之后,我们反编译出基础包中的代码如下:

//SecondActivity
public static ChangeQuickRedirect u;
protected void onCreate(Bundle bundle) {
        if (u != null) {
            if (PatchProxy.isSupport(new Object[]{bundle}, this, u, false, 78)) {
                PatchProxy.accessDispatchVoid(new Object[]{bundle}, this, u, false, 78);
                return;
            }
        }
        super.onCreate(bundle);
        ...
    }

主要类文件

对应于补丁,有三个文件比较重要
1. PatchesInfoImpl 类

用于记录修改的类,及其对应的 ChangeQuickRedirect 接口的实现,我们反编译补丁包得出以下结果,其中的类名是混淆后的。

public class PatchesInfoImpl implements PatchesInfo {
    public List getPatchedClassesInfo() {
        List arrayList = new ArrayList();
        arrayList.add(new PatchedClassInfo("com.meituan.sample.robusttest.l", "com.meituan.robust.patch.SampleClassPatchControl"));
        arrayList.add(new PatchedClassInfo("com.meituan.sample.robusttest.p", "com.meituan.robust.patch.SuperPatchControl"));
        arrayList.add(new PatchedClassInfo("com.meituan.sample.SecondActivity", "com.meituan.robust.patch.SecondActivityPatchControl"));
        EnhancedRobustUtils.isThrowable = false;
        return arrayList;
    }
}

2. xxxPatchControl 类

它是 ChangeQuickRedirect 接口的具体实现,是一个代理,具体的替换方法是在 xxxPatch 类中:

public class SecondActivityPatchControl implements ChangeQuickRedirect {
...
    public boolean isSupport(String methodName, Object[] paramArrayOfObject) {
        return "78:79:90:".contains(methodName.split(":")[3]);
    }
    public Object accessDispatch(String methodName, Object[] paramArrayOfObject) {
        try {
            SecondActivityPatch secondActivityPatch;
            ...
            Object obj = methodName.split(":")[3];
            if ("78".equals(obj)) {
                secondActivityPatch.onCreate((Bundle) paramArrayOfObject[0]);
            }
            if ("79".equals(obj)) {
                return secondActivityPatch.getTextInfo((String) paramArrayOfObject[0]);
            }
            if ("90".equals(obj)) {
                secondActivityPatch.RobustPubliclambda$onCreate$0((View) paramArrayOfObject[0]);
            }
            return null;
        } catch (Throwable th) {
            th.printStackTrace();
        }
    }
}

通过代码不难看出,最终会调用 accessDispatch 方法,该方法会根据传递过来的方法签名,然后调用xxxPatch的修改过的方法。
3. xxxPatch 类

xxxPatch 具体的替换实现类,代码就不贴了。

过程可以简单描述为:
1. 下发补丁包后,新建 DexClassLoader 加载补丁 dex 文件。
2. 反射得到 PatchesInfoImpl.class,并创建其对象。
3. 调用 getPatchedClassesInfo() 方法得到哪些修改的类(比如 SecondActivity)。
4. 然后再通过反射,循环拿到每个修改类在当前环境中的的class,将其中类型为 ChangeQuickRedirect 的静态变量反射修改为 xxxPatchControl.java 这个类 new 出来的对象。

原理图:

image
补丁加载过程分析

1.调用入口:

new PatchExecutor(getApplicationContext(), new PatchManipulateImp(),  new Callback()).start();

1

2.PatchExecutor 是个 Thread,我们看源代码:

public class PatchExecutor extends Thread {
    @Override
    public void run() { //开启一个子线程
        ...
        applyPatchList(patches);
        ...
    }
    /**
     * 应用补丁列表
     */
    protected void applyPatchList(List<Patch> patches) {  // 补丁patch文件可以有多个,for循环读patch文件的jar包。
        ...
        for (Patch p : patches) {
            ...
            currentPatchResult = patch(context, p);
            ...
            }
    }
    protected boolean patch(Context context, Patch patch) {
        ...
        DexClassLoader classLoader = new DexClassLoader(patch.getTempPath(), context.getCacheDir().getAbsolutePath(),
                null, PatchExecutor.class.getClassLoader());  //每个patch文件对应一个 DexClassLoader 去加载
        patch.delete(patch.getTempPath());
        ...
        try {
            patchsInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName()); //每个patch文件中存在PatchInfoImp,反射获取它。
            patchesInfo = (PatchesInfo) patchsInfoClass.newInstance();
            } catch (Throwable t) {
             ...
        }
        ...
        for (PatchedClassInfo patchedClassInfo : patchedClasses) { //遍历类信息, 进而反射修改其中 ChangeQuickRedirect 对象的值。
            ...
            try {
                oldClass = classLoader.loadClass(patchedClassName.trim());
                Field[] fields = oldClass.getDeclaredFields();
                for (Field field : fields) {
                    if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), oldClass.getCanonicalName())) {
                        changeQuickRedirectField = field;
                        break;
                    }
                }
                ...
                try {
                    patchClass = classLoader.loadClass(patchClassName);
                    Object patchObject = patchClass.newInstance();
                    changeQuickRedirectField.setAccessible(true);
                    changeQuickRedirectField.set(null, patchObject);
                    } catch (Throwable t) {
                    ...
                }
            } catch (Throwable t) {
                 ...
            }
        }
        return true;
    }
}

流程大致可以描述为:
1. 首先开启一个子线程,指定的路径去读patch文件的jar包;
2. patch文件可以为多个,for循环读取;
3. 每个patch文件对应一个 DexClassLoader 去加载;
4. 每个patch文件中存在PatchInfoImp,反射获取它;
5. 通过遍历其中的类信息进而反射修改其中 ChangeQuickRedirect 对象的值。
基础包插桩过程分析

基于 InstantRun , Robust 也是使用 Transform API 修改字节码文件,该 API 允许第三方插件在 .class 文件打包为 dex 文件之前操作编译好的 .class 字节码文件。Robust 中的 Gradle-Plugin 就是操作字节码的名为 robust 的 gradle 插件项目。我们来简单看下实现:

class RobustTransform extends Transform implements Plugin<Project> {
    ...
    @Override
    void apply(Project target) {
            //解析项目下robust.xml配置文件
            robust = new XmlSlurper().parse(new File("${project.projectDir}/${Constants.ROBUST_XML}"))
            ...
            project.android.registerTransform(this)
            project.afterEvaluate(new RobustApkHashAction())
        }

@Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
    ...
    ClassPool classPool = new ClassPool()
    project.android.bootClasspath.each {
        logger.debug "android.bootClasspath   " + (String) it.absolutePath
        classPool.appendClassPath((String) it.absolutePath)
    }
    ...
    def box = ConvertUtils.toCtClasses(inputs, classPool)
    insertRobustCode(box, jarFile)
    writeMap2File(methodMap, Constants.METHOD_MAP_OUT_PATH)
    ...
    }
}

首先去读取 robust.xml 配置文件并初始化,可配置选项包括:
一些开关选项。
需要热补丁的包名或者类名,这些包名下的所有类都被会插入代码。
不需要热补的包名或者类名,可以在需要热补的包中剔除指定的类或者包。

然后通过 Transform API 调用 transform() 方法,扫描所有类加入到 classPool 中,调用 insertRobustCode() 方法,源代码:

def insertRobustCode(List<CtClass> box, File jarFile) {
    ZipOutputStream outStream=new JarOutputStream(new FileOutputStream(jarFile));
    new ForkJoinPool().submit {
        box.each { ctClass ->
            if (isNeedInsertClass(ctClass.getName())) {
               //将class设置为public ctClass.setModifiers(AccessFlag.setPublic(ctClass.getModifiers()))
                boolean addIncrementalChange = false;
                ctClass.declaredBehaviors.findAll {
                //规避接口和无方法类
                    if (ctClass.isInterface() || ctClass.declaredMethods.length < 1) {
                        return false;
                    }
                    if (!addIncrementalChange) {
                    //插入 public static ChangeQuickRedirect changeQuickRedirect;
                        addIncrementalChange = true;
                        ClassPool classPool = it.declaringClass.classPool
                        CtClass type = classPool.getOrNull(Constants.INTERFACE_NAME);
                        CtField ctField = new CtField(type, Constants.INSERT_FIELD_NAME, ctClass);
                        ctField.setModifiers(AccessFlag.PUBLIC | AccessFlag.STATIC)
                        ctClass.addField(ctField)
                        logger.debug "ctClass: " + ctClass.getName();
                    }
                    if (it.getMethodInfo().isStaticInitializer()) {
                        return false
                    }
                    // synthetic 方法暂时不aop 比如AsyncTask 会生成一些同名 synthetic方法,对synthetic 以及private的方法也插入的代码,主要是针对lambda表达式
                    if ((it.getModifiers() & AccessFlag.SYNTHETIC) != 0 && !AccessFlag.isPrivate(it.getModifiers())) {
                        return false
                    }
                    //不支持构造方法
                    if (it.getMethodInfo().isConstructor()) {
                        return false
                    }
                    //规避抽象方法
                    if ((it.getModifiers() & AccessFlag.ABSTRACT) != 0) {
                        return false
                    }
                    //规避NATIVE方法
                    if ((it.getModifiers() & AccessFlag.NATIVE) != 0) {
                        return false
                    }
                    //规避接口
                    if ((it.getModifiers() & AccessFlag.INTERFACE) != 0) {
                        return false
                    }
                    if (it.getMethodInfo().isMethod()) {
                        if (AccessFlag.isPackage(it.modifiers)) {
                            it.setModifiers(AccessFlag.setPublic(it.modifiers))
                        }
                        //判断是否有方法调用,返回是否插庄
                        boolean flag = modifyMethodCodeFilter(it)
                        if (!flag) {
                            return false
                        }
                    }
                    //方法过滤
                    if (isExceptMethodLevel && exceptMethodList != null) {
                        for (String exceptMethod : exceptMethodList) {
                            if (it.name.matches(exceptMethod)) {
                                return false
                            }
                        }
                    }
                    if (isHotfixMethodLevel && hotfixMethodList != null) {
                        for (String name : hotfixMethodList) {
                            if (it.name.matches(name)) {
                                return true
                            }
                        }
                    }
                    return !isHotfixMethodLevel
                }.each { ctBehavior ->
                    // methodMap must be put here
                    methodMap.put(ctBehavior.longName, insertMethodCount.incrementAndGet());
                    try {
                    if (ctBehavior.getMethodInfo().isMethod()) {
                            boolean isStatic = ctBehavior.getModifiers() & AccessFlag.STATIC;
                            CtClass returnType = ctBehavior.getReturnType0();
                            String returnTypeString = returnType.getName();
                            def body = "if (${Constants.INSERT_FIELD_NAME} != null) {"
                            body += "Object argThis = null;"
                            if (!isStatic) {
                                body += "argThis = \$0;"
                            }
                            body += "   if (com.meituan.robust.PatchProxy.isSupport(\$args, argThis, ${Constants.INSERT_FIELD_NAME}, $isStatic, " + methodMap.get(ctBehavior.longName) + ")) {"
                            body += getReturnStatement(returnTypeString, isStatic, methodMap.get(ctBehavior.longName));
                            body += "   }"
                            body += "}"
                            ctBehavior.insertBefore(body);
                        }
                    } catch (Throwable t ) {
                        logger.error "ctClass: " + ctClass.getName() + " error: " + t.toString();
                    }
                }
                }
            zipFile(ctClass.toBytecode(),outStream,ctClass.name.replaceAll("\\.","/")+".class");
        }
    }.get()
    outStream.close();
    logger.debug "robust insertMethodCount: " + insertMethodCount.get()
}

代码大致流程:
1. 将class设置为public;
2. 规避 接口;
3. 规避 无方法类;
4. 规避 构造方法;
5. 规避 抽象方法;
6. 规避 native方法;
7. 规避 synthetic方法;
8. 过滤配置文件中不需要修复的类;
9. 通过 javassist 在类中插入 public static ChangeQuickRedirect
10. changeQuickRedirect;
11. 通过 javassist 在方法中插入逻辑代码段;
12. 通过 zipFile() 方法写回class文件;
13. 最后调用 writeMap2File() 将插桩的方法信息写入 robust/methodsMap.robust 文件中,此文件和混淆的mapping文件需要备份。
Robust方案总结
优点:

兼容性和稳定性更好,由于使用多ClassLoader方案(补丁中无新增Activity,所以不算激进类型的动态加载,无需hook system),不存在preverify的问题。
    由于采用 InstantRun 的热更新机制,所以可以即时生效,不需要重启。
    支持Android2.3-8.X版本 。
    对性能影响较小,不需要合成patch。
    支持方法级别的修复,支持静态方法。
    支持新增方法和类。
    支持ProGuard的混淆、内联、编译器优化(解决了桥方法、lambda、内部类等优化后- 引起的问题)等操作。
    Robust补丁自动化。plugin为Robust自动生成补丁,使用者只需要提交修改完bug后的代码,运行和线上apk打包同样的gradle命令即可,会在项目的app/build/outputs/robust目录下生成补丁。

不足:

暂时不支持新增字段,但可以通过新增类解决。
    会增加apk的体积。因为每个函数都插入了一段逻辑,为每个class插入了ChangeQuickRedirect的字段以及补丁判断代码。
    暂时不支持修复构造方法,不过貌似已经在内测。
    暂时不支持资源和 so 修复,不过这个问题不大,因为独立于 dex 补丁,已经有很成熟的方案了,就看怎么打到补丁包中以及 diff 方案。
    对于返回值是 this 的方法支持不太好。
    没有安全校验,需要开发者在加载补丁之前自己做验证。
    可能会出现深度方法内联导致的不可预知的错误(几率很小可以忽略)。

Robust热修复方案实现原理相关推荐

  1. 常用热修复方案以及原理

    常用热修复方案以及原理 bsdiff.exe 比对文件不同 bspatch.exe 生成增量包 增量级别更新,应用很广泛,抖音微信都有使用增量更新 所有的热修复都是使用 反射和类加载机制完成热修复,和 ...

  2. JAndFix: 基于Java实现的Android实时热修复方案

    简述 JAndFix是一种基于Java实现的Android实时热修复方案,它并不需要重新启动就能生效.JAndFix是在AndFix的基础上改进实现,AndFix主要是通过jni实现对method(A ...

  3. android 热修复 需要重启应用嘛?_Android热修复方案盘点

    前言 上一个大的系列文章叫 "手把手讲解", 历时10个月,出产博文二十余篇,讲解细致,几乎每一篇都提供了详实的原理讲解,提供了可运行 githubDemo,并且针对Demo中的关 ...

  4. Android热修复实现及原理

    前言 说起热修复,已经是目前Android开发必备技能.我所了解的一种实现方式就是类加载方案,即 dex 插桩,这种思路在插件化中也会用到.除此之外,还有底层替换方案,即修改替换 ArtMethod. ...

  5. Android热更新五:四大热修复方案对比分析

    很早之前就想深入的研究和学习一下热修复,由于时间的原因一直拖着,现在才执笔弄起来. Android而更新系列: Android热更新一:JAVA的类加载机制 Android热更新二:理解Java反射 ...

  6. android热修复方案

    热补丁方案有很多,其中比较出名的有腾讯Tinker.阿里的AndFix.美团的Robust以及QZone的超级补丁方案.他们的优劣如下: 一.Tinker 热修复 Tinker通过 Dexdiff 算 ...

  7. 干货满满,Android热修复方案介绍

    摘要:在云栖社区技术直播中,阿里云客户端工程师李亚洲(毕言)从技术原理层面解析和比较了业界几大热修复方案,揭开了Qxxx方案.Instant Run以及阿里Sophix等热修复方案的神秘面纱,帮助大家 ...

  8. Android 热修复方案分析

    绝大部分的APP项目其实都需要一个动态化方案,来应对线上紧急bug修复发新版本的高成本.之前有利用加壳,分拆两个dex结合DexClassLoader实现了一套全量更新的热更方案.实现原理在Andro ...

  9. QQ浏览器HD iOS 动态化/热修复方案QBDF(0) 【简书被冻结-搬运】

    原作时间:2019-2021年 此次共享,腾讯内网 / 外网同步发布. 内部代码地址:https://git.code.oa.com/fatboyli/QBDF 外部代码地址:GitHub - ven ...

最新文章

  1. 如何设置SOLR的高亮 (highlight)?
  2. AttributeError: module ‘seaborn‘ has no attribute ‘tsplot‘
  3. java实现计算机图形学中点画线算法
  4. tomcat runing on daemon with apr and ssl mode
  5. php对象json,php 把对象转化为json
  6. 构造函数、原型、继承原来这么简单?来吧,深入浅出
  7. css跑道_如何不超出跑道:计划种子的简单方法
  8. 单例设计模式-懒汉式(线程安全)
  9. MySQL is running but PID file could not be found(在macOS系统下解决方法)
  10. windows系统清除电脑地址栏文件(夹)路径
  11. python 共享内存 c_python共享内存 - DK's Blog - 博客园
  12. Numpy中reshape的用法
  13. jsp+ssm计算机毕业设计中青年健康管理监测系统【附源码】
  14. LeetCode 41
  15. Translation插件
  16. [论文笔记]Seed,Expend and Constrain
  17. 汇编语言随笔(4)-数据段和栈段与mov,add,sub,div,mul、adc、sbb指令
  18. android 修改键盘换行按键
  19. 5.UI线程和非UI线程的交互方式
  20. aspectj weaver记录

热门文章

  1. 一种防山火在线监测装置
  2. hihocoder 闰秒
  3. Java中super详解
  4. Kotlin常见知识点和踩坑指南
  5. lcg_magic算法笔记:插入排序
  6. 手机便签设了私密怎么查看
  7. 什么副业来钱快?有什么靠谱的副业可以做?
  8. linux卸载命令pkg,[转]软件包管理的命令:pkginfo、pkgadd和pkgrm
  9. 机器学习画图工具(1)
  10. 深度学习-Resolution-robust Large Mask Inpainting with Fourier Convolutions基于傅里叶卷积的对分辨率鲁棒的掩模修复