美团热修复 Robust 原理解析(二)

版权声明:本文为博主原创文章,转载请注明出处: 小嵩的博客 https://blog.csdn.net/qq_22393017/article/details/82224656

声明:本文为博主原创文章,转载请注明出处:小嵩的博客

本系列传送门:
美团热修复 Robust 方案接入(一)
美团热修复 Robust 原理解析(二)

前言

我们知道InstantRun 对应三种更新机制:
冷插拔,我们称之为重启更新机制;
温插拔,我们称之为重启Activity更新机制;
热插拔,我们称之为热更新机制。

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

实现原理

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

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

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

sample1

(如State.java的getIndex方法)

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

旧代码:

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 100;
    }

新代码添加注解:

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

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();

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 的方法支持不太好。
  • 没有安全校验,需要开发者在加载补丁之前自己做验证。
  • 可能会出现深度方法内联导致的不可预知的错误(几率很小可以忽略)。

猜你喜欢

转载自blog.csdn.net/qq_22393017/article/details/82224656