微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Android Robust热修复方案实现原理浅析

前言

本文旨在通过分析源码一步步分析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文件时会经过的一些主要步骤:

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为我们提供的可实现的方法

表格中可见,getNamgetInputTypes方法均为Transform的配置项,最后的transform方法需要重点关注,我们想要实现上面的拦截.class文件并进行代码插桩操作就需要在此方法中实现。其中着重看一下方法的型参inputs,通过inputs可以拿到所有的class文件inputs包括directoryInputsjarInputsdirectoryInputs文件夹中的class文件,而jarInputsjar包中的class文件

B.加载补丁

DexClassLoader

关于Java中的ClassLoader,大家熟知的基础概念就是通过一个类的全名加载得到这个类的class对象,进而可以得到其实例对象。 在Android中的ClassLoader基本运作远离与Java中类似,不过开发者无法自己实现ClassLoader进行自定义操作,官方的api为我们提供了两个ClassLoader的子类,PathClassLoaderDexClassLoader。虽然两者继承于BaseDexClassLoaderBaseDexClassLoader继承于ClassLoader,但是前者只能加载已安装的Apk里面的dex文件,后者则支持加载apkdex以及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的集合;

CtClassJavassist框架中的类,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 中,该mapvalue值为该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 对象的集合,此对象中包含本次需要修复的类信息以及修复使用的补丁类信息,分别对应变量 sourceClasspatchClass

首先加载 sourceClass 类,遍历该类中所有的字段,拿到前文中代码插桩环节中注入的 ChangeQuickRedirect 变量,再加载 patchClass ,将该对象赋值给 ChangeQuickRedirect 变量;

至此,加载补丁逻辑结束,需修复的补丁类中 ChangeQuickRedirect 被赋值,方法体内的插桩逻辑将会执行并修复原有逻辑。

结尾

谢谢大家看完,如有不恰当、不充分的地方,欢迎大家指正。针对这块知识点我在学习过程中,进行了详细的整理梳理了一些学习笔记,@H_468_2502@有需要参考学习的小伙伴可以 点击这里查看获取方式 传送门直达 !!!

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐