Android 热修复原理与实现方案学习

这篇文章主要是对 QQ空间团队 安卓App热补丁动态修复技术介绍 的原理和实现方案学习。

基于 安卓App热补丁动态修复技术介绍 github上开源了很多热修复的框架,大致有:

https://github.com/dodola/HotFix
https://github.com/jasonross/Nuwa
https://github.com/bunnyblue/DroidFix

##原理

  • 在app打包的时候,阻止相关类被打上CLASS_ISPREVERIFIED标志;
  • 将存在bug的类打成一个patch.jar包;
  • 通过DexClassLoader从sdcard加载patch.jar包中的class.dex文件;
  • 通过Java的反射机制,将patch.jar中的classes.dex文件,动态添加到BaseDexClassLoader对象间接引用的dexElements数组中。使BaseDexClassLoader在进行类加载时,先加载patch.jar中的classes.dex,从而完成bug的修复。

##CLASS_ISPREVERIFIED问题

关于CLASS_ISPREVERIFIED的问题 安卓App热补丁动态修复技术介绍 有详细的介绍。

根据上面的文章,在虚拟机启动的时候,若verify选项被打开,相关类的static方法、private方法、构造函数等直接引用(第一层关系)到的类都在同一个dex文件中,那么该类就会被打上CLASS_ISPREVERIFIED标志。

若使用热修复则要阻止该类打上CLASS_ISPREVERIFIED标志。

注意是阻止引用者的类,也就是说,假设app里面有个类叫做LoadBugClass,在其内部引用了BugClass。发布后发现BugClass有编写错误,那么想要发布一个新的BugClass类,那么就要阻止LoadBugClass这个类打上CLASS_ISPREVERIFIED的标志。

也就是说,在生成apk之前,就需要阻止相关类打上CLASS_ISPREVERIFIED的标志了。对于如何阻止,上面的文章说的很清楚,让LoadBugClass在构造方法中,去引用别的dex文件,比如:hack.dex中的某个类即可。

其实就是两件事:

  • 1、在app打包的时候,阻止相关类去打上CLASS_ISPREVERIFIED标志。
  • 2、动态改变BaseDexClassLoader对象间接引用的dexElements;

##DexClassLoader与PathClassLoader

DexClassLoader
DexClassLoader的源码注释中这样写到:

A class loader that loads classes from {@code .jar} and {@code .apk} files containing a {@code classes.dex} entry. This can be used to execute code not installed as part of an application.
可以看出,DexClassLoader可以用来从.jar和.apk类型的文件内部加载classes.dex文件。可以用来执行非安装的程序代码。

热修复正是利用DexClassLoader可以从外部文件中加载classes.dex文件这一特性。

DexClassLoader.java

package dalvik.system;
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

PathClassLoader

PathClassLoader的源码注释中这样写到:

Provides a simple {@link ClassLoader} implementation that operates on a list of files and directories in the local file system, but does not attempt to load classes from the network. Android uses this class for its system class loader and for its application class loader(s).

可以看出,Android使用PathClassLoader作为其系统类和应用类的加载器。并且这个类,只能去加载已经安装到Android系统中的apk文件。

Android系统正是使用PathClassLoader作为其类加载器,从apk中进行类加载的。

PathClassLoader.java

package dalvik.system;
public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

到这里,我们了解到,可以通过DexClassLoader从sdcard的patch.jar中加载classes.dex:

  • Android系统使用PathClassLoader作为其类加载器,从apk中进行类的加载;
  • DexClassLoader可以从.jar和.apk类型的外部文件中加载classes.dex。

##BaseDexClassLoader

前边说道:通过Java的反射机制,将patch.jar中的classes.dex文件,动态添加到BaseDexClassLoader对象间接引用的dexElements数组中。使BaseDexClassLoader在进行类加载时,先加载patch.jar中的classes.dex,从而完成bug的修复。

这里来看一下,如何反射到BaseDexClassLoader类间接引用的dexElements数组中。

BaseDexClassLoader.java源码

package dalvik.system;
public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;
	...
	@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
	...
}

BaseDexClassLoader通过findClass进行类的加载,通过pathList属性,又调用了DexPathList类的findClass方法进行类的加载,下边看一下DexPathList类的findClass方法。

DexPathList.java

package dalvik.system;
final class DexPathList {
    private final Element[] dexElements;
	...
    public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }
	...
}

这里找到了dexElements数组。
通过反射机制,将patch.jar包中的class.dex文件添加到dexElements数组的最前边,即完成bug热修复。
BaseDexClassLoader在加载类时会循环dexElements数组,先加载dexElements数组靠前的dex文件,若在dexElements数组中靠前的dex文件中找到对应的类则不再加载后边的dex文件,从而完成bug修复。

##阻止相关类被打上CLASS_ISPREVERIFIED的代码实现

通过https://github.com/dodola/HotFix的代码学习。

大致的流程是:
在dx工具执行之前,对LoadBugClass.class文件进行修改,再其构造中添加System.out.println(dodola.hackdex.AntilazyLoad.class),然后继续打包的流程。注:AntilazyLoad.class这个类独立在hack.dex中。

这里有2个问题:

  • 如何去修改一个.class文件(通过javassist注入代码)
  • 如何让.class文件的修改发生在dx之前

###如何去修改一个类的class文件(通过javassist注入代码)

新建以下几个类:

被引用类AntilazyLoad.java(hack.jar中包含的类)

package dodola.hackdex;
public class AntilazyLoad {}

要修复的类BugClass.java

package dodola.hotfix;
public class BugClass
{
    public String bug()
    {
        return "bug class";
    }
}

引用者类LoadBugClass.java

package dodola.hotfix;
public class LoadBugClass
{
    public String getBugString()
    {
        BugClass bugClass = new BugClass();
        return bugClass.bug();
    }
}

这里要做的是,在LoadBugClass.java正常编译产生class文件LoadBugClass.class构造方法中,添加:

System.out.println(dodola.hackdex.AntilazyLoad.class)

可以通过javassist来实现,关于javassist

JAVAssist
Javassist学习总结

对于向LoadBugClass.class构造方法中注入代码,下边有一个Test.java
这里写图片描述

Test.java

package dodola.hotfix;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
public class Test {
    public static void main(String[] args) {
        try {
            //根据文件路径找到.class文件
            // class文件路径
            String path = "D:\\work_space\\ws\\Android_Test\\app\\build\\intermediates\\classes\\debug\\dodola\\hotfix";
            ClassPool classes = ClassPool.getDefault();
            classes.appendClassPath(path);
            // 找到LoadBugClass.class
            CtClass c = classes.get("dodola.hotfix.LoadBugClass");
            // 构造方法中插入代码
            CtConstructor ctConstructor = c.getConstructors()[0];
            ctConstructor.insertBefore("System.out.println(dodola.hackdex.AntilazyLoad.class);");
            // 输出文件
            c.writeFile(path + "/output");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注:build.gradle中导入了

compile 'org.javassist:javassist:3.18.2-GA'

run运行Test.java,向LoadBugClass.class中注入代码。
下边是注入前与注入后的LoadBugClass.class对比
这里写图片描述
这里写图片描述

###如何让.class文件的修改发生在dx之前

打开https://github.com/dodola/HotFix工程app/build.gradle目录

apply plugin: 'com.android.application'

// 构造方法注入代码
task('processWithJavassist') << {
    // 项目编译后.class所在目录
    String classPath = file('build/intermediates/classes/debug')
    // 调用PatchClass.java的process方法,在.class文件构造方法注入代码
    dodola.patch.PatchClass.process(
            // 项目编译后.class所在目录
            classPath,
            // hackdex的.class所在目录
            project(':hackdex').buildDir.absolutePath + '/intermediates/classes/debug')

}
android {
	...
    // 在执行dx命令之前将代码注入到.class中
    applicationVariants.all { variant ->
        variant.dex.dependsOn << processWithJavassist 
    }
}
dependencies {
	...
}

##将AntilazyLoad打成一个hack_dex.jar包
在完成"System.out.println(dodola.hackdex.AntilazyLoad.class);"的代码注入后,还要将AntilazyLoad.class打成一个hack_dex.jar包,并通过DexClassLoader加载到程序中,否则LoadBugClass等被注入代码的类,在运行时会抛出ClassNotFoundException异常。

hack_dex.jar包生成
这里写图片描述
这里写图片描述
对应的命令行为:

jar cvf hack.jar dodola/hackdex/*
dx --dex --output hack_dex.jar hack.jar 

##加载hack_dex.jar中的.dex文件

HotfixApplication.java

package dodola.hotfix;
import java.io.File;
import android.app.Application;
import android.content.Context;
import dodola.hotfixlib.HotFix;
//1、dexClassLoader加载hackdex.jar(dodola.hackdex.AntilazyLoad)
//2、hack_dex中的.dex文件,动态添加到BaseDexClassLoader对象间接引用的dexElements数组中
public class HotfixApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        //
        File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
        // assets下的hackdex_dex.jar文件拷贝到/data/data/packagename/dex/hackdex_dex.jar 路径下
        Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar");
        // 将hack_dex 动态添加到BaseDexClassLoader对象间接引用的dexElements数组中
        HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
        // 加载dodola.hackdex.AntilazyLoad
        try {
            this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

HotFix.java文件中patch方法

//将patch.dex 动态添加到BaseDexClassLoader对象间接引用的dexElements数组中
public static void patch(Context context, String patchDexFile, String patchClassName) {
    // dex文件存在
    if (patchDexFile != null && new File(patchDexFile).exists()) {
        try {
            // 阿里设备
            if (hasLexClassLoader()) {
                injectInAliyunOs(context, patchDexFile, patchClassName);
            }
            // 非阿里设备
            else if (hasDexClassLoader()) {
                injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);
            } else {
                injectBelowApiLevel14(context, patchDexFile, patchClassName);
            }
        } catch (Throwable th) {
        }
    }
}

HotFix.java文件中injectAboveEqualApiLevel14方法

private static void injectAboveEqualApiLevel14(Context context, String str, String str2)
        throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
    // PathClassLoader
    PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
    // ----------合并数组--------
    // 合并数组
    Object a = combineArray(
            // PathClassLoader中的dexElements数组
            getDexElements(getPathList(pathClassLoader)),
            // DexClassLoader 加载本地dex文件后,dexElements数组
            getDexElements(getPathList(new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader())))
    );
    //--------dexElements数组赋值---------
    // 获取pathList
    Object a2 = getPathList(pathClassLoader);
    setField(a2, a2.getClass(), "dexElements", a);
    //--------加载str2类---------
    pathClassLoader.loadClass(str2);
}

参考:
https://zhuanlan.zhihu.com/p/20308548?columnSlug=magilu
http://blog.csdn.net/lmj623565791/article/details/49883661

========== THE END ==========

您对“我的文章”有任何疑问,可用微信扫描以下“二维码”向我提问!

在这里插入图片描述

如果文章对您有帮助,请扫描以下二维码支持我!

在这里插入图片描述

发布了250 篇原创文章 · 获赞 143 · 访问量 27万+

猜你喜欢

转载自blog.csdn.net/aiwusheng/article/details/70153828