热修复之仿Robust实现

1,390 阅读5分钟

概述

关于Robust,原理是简约精巧的,而细节却是无尽繁杂的。
这一篇文章,通过手写基础版RobustDemo,稍稍打开下Robust的大门。
可以在此Demo上,丰富Robust的细节。
Demo在github中

过程介绍

截屏2020-12-27 下午4.23.02.png

1.原始方法注入Hook代码

使用Javassist+Gradle插件,在原始类的所有方法中,注入hook代码。 原始类如下:

public class M {
    private int apple = 3;

    public String a(int appleNum) {
        this.apple = appleNum;
        StringBuilder sb = new StringBuilder();
        sb.append("M aaa ,apple:");
        sb.append(this.apple);
        return sb.toString();
    }

    public static int b() {
        System.out.println("method b ");
        return 1;
    }
}

Hook

插件在RobustInjectTransform.groovy中,通过javassist实现。经过插件处理,插入如下代码

截屏2020-12-27 下午4.31.15.png

ChangeQuickRedirect

ChangeQuickRedirect实际是一个接口类,在补丁包生成中,对于发生补丁修改行为的XXclass,都会生成对应的XXPatchControlClass,用于补丁方法的识别与运行。

package com.a.robust.patch;

public interface ChangeQuickRedirect {

     /**
     * 如果此方法支持补丁,则分发执行补丁内容,次接口实现中,调用补丁类的补丁方法
     * @param methodParams  被注入方法的参数
     * @param originObj   被注入方法所在类
     * @param isStatic    是否是静态方法
     * @param methodNumber  方法对应序列号,是我们注入过程中,递增生成的。 为后续生成自动补丁和运行补丁代码做准备
     * @return
     */
    Object accessDispatch(Object[] methodParams, Object originObj, boolean isStatic, int methodNumber);

    /**
     * 次方法是否支持补丁
     * @param methodParams
     * @param originObj
     * @param isStatic
     * @param methodNumber
     * @return
     */
    boolean isSupport(Object[] methodParams, Object originObj, boolean isStatic, int methodNumber);
}

Hook MethodMap

补丁包运行过程中,会将changeQuickRedirect属性设置为XXPatchControlClass实例。 isSupport同时需要去识别哪些方法打了补丁。因此,插件在插入hook代码同时,生成类方法的记录methodMap.txt,用于生成XXPatchControl以及后续运行补丁时的比较。

com.a.robust.data.M.a(int):1
com.a.robust.data.M.b():2

2.修改原始代码

这里我们可以看到,我们

  • 修改了a()方法中"M aaa ,apple:"--> "M aaa fix,apple:"
  • 同时给a()方法加上@Modify注解,表示修改了。用于后续插件失败被修改的方法。
package com.a.robust.data;


import com.a.robust.patch.annotation.Modify;

public class M {
    private int apple = 3;

//    public String a(int appleNum) {
//        this.apple = appleNum;
//        StringBuilder sb = new StringBuilder();
//        sb.append("M aaa ,apple:");
//        sb.append(this.apple);
//        return sb.toString();
//    }

    //仿照robust,给修改过的方法加上自定义注解,后续自动生成补丁会需要到。
    @Modify
    public String a(int appleNum) {
        this.apple = appleNum + 1;
        StringBuilder sb = new StringBuilder();
        sb.append("M aaa fix,apple:");
        sb.append(this.apple);
        return sb.toString();
    }
}

自定义的注解如下

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.CLASS)
@Documented
public @interface Modify {
    String value() default "";
}

自动生成补丁插件

插件在RobustAutoPatchTransform.groovy
生成两个类:

  • MPatch:补丁实体类,代理原始类M,运行修改过的方法a()。
  • MPatchControl:实现ChangeQuickRedirect接口,
    • 识别是否需要补丁方法
    • 分发补丁方法。

生成MPatch.class过程

使用javassist+gradle插件。
截屏2020-12-27 下午5.15.45.png
补丁插件自动生成的class。 插件参考RobustAutoPathFactory. createPatchClass()
下面是其反编译成的Java代码

package com.a.robust.data;

import android.util.Log;
import com.a.robust.patch.EnhancedRobustUtils;

public class MPatch {
    M originClass;

    public MPatch(Object obj) {
        MPatch mPatch = this;
        this.originClass = (M) obj;
    }

    /**
    * 这段代码对应我们Modify注解的方法。
    * 把方法语句,一行一行翻译成反射方式使用
    * 同时把代码中的this从MPatch替换成originClass,即M。
    */
    public String a(int i) {
        EnhancedRobustUtils.setFieldValue("apple", this instanceof MPatch ? r3.originClass : r3, i + 1, M.class);
        int d = Log.d("robust", "set value is apple  No:  1");
        Object obj = null;
        Object obj2 = null;
        StringBuilder sb = (StringBuilder) EnhancedRobustUtils.invokeReflectConstruct("java.lang.StringBuilder", new Object[0], null);
        StringBuilder stringBuilder = sb;
        Object obj3 = null;
        M m = stringBuilder == this ? ((MPatch) stringBuilder).originClass : stringBuilder;
        Object[] objArr = new Object[1];
        Object[] objArr2 = objArr;
        objArr[0] = "M aaa fix,apple:";
        Class[] clsArr = new Class[1];
        Class[] clsArr2 = clsArr;
        clsArr[0] = String.class;
        StringBuilder stringBuilder2 = (StringBuilder) EnhancedRobustUtils.invokeReflectMethod("append", m, objArr2, clsArr2, StringBuilder.class);
        d = Log.d("robust", "invoke  method is   No:  2 append");
        StringBuilder stringBuilder3 = stringBuilder2;
        stringBuilder3 = sb;
        obj2 = null;
        int intValue = ((Integer) EnhancedRobustUtils.getFieldValue("apple", this instanceof MPatch ? r3.originClass : r3, M.class)).intValue();
        int d2 = Log.d("robust", "get value is apple    No:  3");
        stringBuilder = stringBuilder3;
        obj3 = null;
        m = stringBuilder == this ? ((MPatch) stringBuilder).originClass : stringBuilder;
        objArr = new Object[1];
        objArr2 = objArr;
        Object[] objArr3 = objArr;
        Integer num = r16;
        Integer num2 = new Integer(intValue);
        objArr3[0] = num;
        clsArr = new Class[1];
        clsArr2 = clsArr;
        clsArr[0] = Integer.TYPE;
        stringBuilder2 = (StringBuilder) EnhancedRobustUtils.invokeReflectMethod("append", m, objArr2, clsArr2, StringBuilder.class);
        d = Log.d("robust", "invoke  method is   No:  4 append");
        stringBuilder3 = stringBuilder2;
        stringBuilder = sb;
        obj2 = null;
        String str = (String) EnhancedRobustUtils.invokeReflectMethod("toString", stringBuilder == this ? ((MPatch) stringBuilder).originClass : stringBuilder, new Object[0], null, StringBuilder.class);
        d = Log.d("robust", "invoke  method is   No:  5 toString");
        return str;
    }
}

Modify MethodMap

同时得到Modify映射关系,

com.a.robust.data.M.a(int):1

最终的映射字符串可能如下。就是仅仅包含Modify的方法。

com.a.robust.data.M-->   1,3
com.a.robust.data.N-->   4,5

生成MPatchControl.class过程

使用javassist+gradle插件,根据 Modify MethodMap和Hook MethodMap生成。
插件参考RobustAutoPathFactory. createPatchControlClass()
下面是实际生成的内容

package com.a.robust.data;

import com.a.robust.patch.ChangeQuickRedirect;

public class MPatchControl implements ChangeQuickRedirect {

    public Object accessDispatch(Object[] methodParams, Object originObj, boolean isStatic, int methodNumber) {
        MPatch mPatch;
        if (isStatic) {
            mPatch = new MPatch(null);
        } else {
            mPatch = new MPatch(originObj);
        }
        if (1 == methodNumber) {
            return mPatch.a(((Integer) methodParams[0]).intValue());
        }
        return null;
    }

    public boolean isSupport(Object[] methodParams, Object originObj, boolean isStatic, int methodNumber) {
        return ":1:".contains(new StringBuffer.append(":").append(methodNumber).append(":").toString());
    }
}
}

All Patch Class To Dex

MPatch.class,MPatchControl.class
通过 dx --dex指令生成dex,我们命名为robust_patch.dex

Patch Class Map

classMap.txt 记录原始类与其ChangeQuickRedirect实现

com.a.robust.data.M:com.a.robust.data.MPatchControl

加载补丁

我们把上面生成的文件放到assets中,

  • robust_patch.dex
  • classMap.txt
package com.a.robust.patchexecute;

import android.content.Context;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.RequiresApi;

import com.a.robust.patch.EnhancedRobustUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.Map;

import dalvik.system.DexClassLoader;

public class PatchExecutor {
    /**
     * 真正执行补丁加载,代码也是超级简单
     * @param context
     */
    public static void doPatch(Context context) {
        try {

            File classMapFile = copyAssetsToExternalCache(context, "classMap.txt");
            File patchFile = copyAssetsToExternalCache(context, "robust_patch.dex");
            Map<String, String> classMap = generatePatchClassMap(context, classMapFile);
            ClassLoader classLoader = generateClassLoader(context, patchFile);

            /**
             * classMap--> [com.a.robust.data.M:com.a.robust.data.MPatchControl]
             */
            for (Map.Entry<String, String> entry : classMap.entrySet()) {
                Class originClass = classLoader.loadClass(entry.getKey());
                Class patchControlClass = classLoader.loadClass(entry.getValue());
                /**
                 * 反射调用,MPatchControl对象设置到M.changeQuickRedirect静态变量
                 */
                EnhancedRobustUtils.setStaticFieldValue(
                    "changeQuickRedirect",originClass,patchControlClass.newInstance());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * patch dex的classLoader,parent为context.getClassLoader()
     * @param context
     * @param patchFile
     * @return
     */
    private static ClassLoader generateClassLoader(Context context, File patchFile) {
        return new DexClassLoader(
                patchFile.getAbsolutePath(),
                patchFile.getParentFile().getAbsolutePath(),
                null, context.getClassLoader());
    }

    /**
     * 从Assets 复制到外部缓存,模拟补丁下载
     * @param context
     * @param assetFileName
     * @return
     */
    private static File copyAssetsToExternalCache(Context context, String assetFileName);

    /**
     * 把classMap.txt 还原成Map--> [com.a.robust.data.M:com.a.robust.data.MPatchControl]
     * 与robust不同,robust是生成类方式,存放map
     * @param context
     * @param classMapFile
     * @return
     */
    public static Map<String, String> generatePatchClassMap(Context context, File classMapFile);
}

MainActivity

public class MainActivity extends AppCompatActivity {
    private Context context;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_app_robust);
        context	= MainActivity.this.getApplicationContext();
        showText();
        findViewById(R.id.btnHotfix).setOnClickListener(v -> {
            PatchExecutor.doPatch(context);
            showText();

        });
    }

    private void showText() {
        String str = new M().a(100) + "," + M.b();
        ((TextView) findViewById(R.id.tvText)).setText(str);
    }
}

不足

如果想理解这个过程,上面代码就够了。
但实际方法内联,调用super方法,lambda表达式,桥方法都没有考虑。目前demo也都是不支持的。
甚至还有AndroidN的混合编译,也不支持。