android热修复技术 HotFix

功能实现:将代码有bug的类通过热修复技术动态替换的效果

demo下载地址:里面有所以代码以及patch包运行即可

http://download.csdn.net/detail/h291850336/9911383

基本介绍:

android的Dalvik/ART虚拟机虽然与标准Java的JVM虚拟机不一样,ClassLoader具体的加载细节不一样,但是工作机制是类似的,也就是说在Android中同样可以采用类似的动态加载插件的功能

参考博客

http://blog.csdn.net/xyang81/article/details/7292380

https://juejin.im/post/5976af6151882510ca6d0559

https://github.com/dodola/HotFix

https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a&scene=1&srcid=1106Imu9ZgwybID13e7y2nEi#wechat_redirect

java加载机制

Java默认提供的三个ClassLoader

 ClassLoader使用的是双亲委托模型来搜索类的
 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }




//打印类加载器从哪些地方加载了相关的jar或class文件
        System.out.println("-----------appclassloader-----------------------");
        // classloaertest获取类加载器
        Class classtest = ClassLoaderTest.class;
        ClassLoader mainLoader = classtest.getClassLoader();
        System.out.println("main class loader : " + mainLoader.toString()); //sun.misc.Launcher$AppClassLoader@63947c6b

        //打印 appclassloader 的加载器
        URL[] murls = ((URLClassLoader)mainLoader).getURLs();
        for (URL url : murls){
            System.out.println(url);
        }

        System.out.println("---------ExtClassLoader-------------------------");
        //打印classtest 的parent字段
        ClassLoader parentClass = mainLoader.getParent();
        System.out.println("parent class : " + parentClass.toString()); //sun.misc.Launcher$ExtClassLoader@3cd1a2f1
        //打印 appclassloader 的加载器
        URL[] parenturls = ((URLClassLoader)parentClass).getURLs();
        for (URL url : parenturls){
            System.out.println(url);
        }

        System.out.println("-------------extclassloader---------------------");
        //extclassloader 的parent字段
        ClassLoader extclassloaderParent = parentClass.getParent();
        System.out.println("parent class : " + (extclassloaderParent == null ? "null" : extclassloaderParent.toString()));

        System.out.println("-------------bootstrapclassloader--------------------");
        //打印 bootstrapclassloader
        try {
            Class lancherClass = Class.forName("sun.misc.Launcher");
            Method methodGetCLassPath = lancherClass.getDeclaredMethod("getBootstrapClassPath", null);
            if(methodGetCLassPath != null){
                methodGetCLassPath.setAccessible(true);
                Object mObj = methodGetCLassPath.invoke(null, null);
                if(mObj != null) {
                    Method methodUrls = mObj.getClass().getDeclaredMethod("getURLs", null);
                    if(methodUrls != null){
                        methodUrls.setAccessible(true);
                        URL[] murlBoot = (URL[])methodUrls.invoke(mObj, null);
                        for (URL url : murlBoot){
                            System.out.println(url);
                        }
                    }
                }
            }else {

            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
分别输出

-----------appclassloader-----------------------
main class loader : sun.misc.Launcher$AppClassLoader@63947c6b
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/deploy.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/dnsns.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/localedata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/nashorn.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/sunec.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/zipfs.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/javaws.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/jfxswt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/management-agent.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/plugin.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/lib/ant-javafx.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/lib/dt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/lib/javafx-mx.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/lib/jconsole.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/lib/packager.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/lib/sa-jdi.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/lib/tools.jar
file:/Users/sunmeng/Desktop/springmvc/gs-rest-service/aaa/out/production/aaa/
file:/Applications/IntelliJ%20IDEA%2015.app/Contents/lib/idea_rt.jar
---------ExtClassLoader-------------------------
parent class : sun.misc.Launcher$ExtClassLoader@3cd1a2f1
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/dnsns.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/localedata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/nashorn.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/sunec.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/zipfs.jar
file:/System/Library/Java/Extensions/AppleScriptEngine.jar
file:/System/Library/Java/Extensions/dns_sd.jar
file:/System/Library/Java/Extensions/j3daudio.jar
file:/System/Library/Java/Extensions/j3dcore.jar
file:/System/Library/Java/Extensions/j3dutils.jar
file:/System/Library/Java/Extensions/jai_codec.jar
file:/System/Library/Java/Extensions/jai_core.jar
file:/System/Library/Java/Extensions/libAppleScriptEngine.jnilib
file:/System/Library/Java/Extensions/libJ3D.jnilib
file:/System/Library/Java/Extensions/libJ3DAudio.jnilib
file:/System/Library/Java/Extensions/libJ3DUtils.jnilib
file:/System/Library/Java/Extensions/libmlib_jai.jnilib
file:/System/Library/Java/Extensions/mlibwrapper_jai.jar
file:/System/Library/Java/Extensions/MRJToolkit.jar
file:/System/Library/Java/Extensions/vecmath.jar
file:/usr/lib/java/libjdns_sd.jnilib
-------------extclassloader---------------------
parent class : null
-------------bootstrapclassloader--------------------
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/classes

除了Java默认提供的三个ClassLoader之外,用户还可以根据需要定义自已的ClassLoader,而这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类,也包括Java提供的另外二个ClassLoader(Extension ClassLoader和App ClassLoader)在内,但是Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。


功能实现

android类加载器

//加载apk中的文件
        System.out.println("xxxx main class loader(PathClassLoader) : " + classLoader.toString()); // dalvik.system.PathClassLoader
        /* pathclassloader
        DexPathList[[zip file "/data/app/com.mas.sunmeng.hotfixtest-2/base.apk",
         zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_dependencies_apk.apk",
          zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_0_apk.apk",
           zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_1_apk.apk",
            zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_2_apk.apk",
             zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_3_apk.apk",
              zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_4_apk.apk",
               zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_5_apk.apk",
                zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_6_apk.apk",
                 zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_7_apk.apk",
                  zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_8_apk.apk",
                   zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_9_apk.apk"],
                   nativeLibraryDirectories=[/data/app/com.mas.sunmeng.hotfixtest-2/lib/x86, /vendor/lib, /system/lib]]]
         */

        ClassLoader parentClassLoader = classLoader.getParent();
        if(parentClassLoader != null ){
            System.out.println("xxxx main class parent loader (BootClassLoader) : " + parentClassLoader.toString()); // java.lang.BootClassLoader
        }
        //  parentClassLoader.getParent() 为空
        if(parentClassLoader.getParent() != null ){
            System.out.println("xxxx BootClassLoader parent class  loader : " + parentClassLoader.getParent().toString()); // null
        }

可以看见有2个Classloader实例,一个是BootClassLoader(系统启动的时候创建的),另一个是PathClassLoader(应用启动时创建的,用于加载“/data/app/xxx.apk”里面的类)。 由此也可以看出,一个运行的Android应用至少有2个ClassLoader。

动态加载的实现思路

DexClassLoader可以加载.jar和.apk类型的文件内部的classes.dex文件,用来执行非安装的程序,那么我们可以将有bug的类做成jar文件,然后将类动态插入到类加载器中,通过dexclassloader读取jar文件获取对应的dexpathlist中的dexelements,然后将补丁的dexelements插入到有bug的dexpathlist的dexelements之前,让类加载器先加载没有bug的类即可。


例如:

/**
 * Created by sunmeng on 17/7/26.
 */

public class BugClass {

    public String haveBug(){
        return "xxxx there hava a bug !";
    }
}
我们认为此类是有bug的类,需要讲返回的字符串做修改,

然后将修改后的代码打包成jar文件

jar cvf pathbug.jar com/mas/sunmeng/hotfixtest/BugClass.class //必须是全路径
然后将pathbug.jar做成补丁包path_dex.jar,只有通过dex工具打包而成的文件才能被Android虚拟机(dexopt)执行
./dx --dex --output=path_dex.jar pathbug.jar
若在进行class转jar包是不是全路径会报异常
Caused by: com.android.dx.cf.iface.ParseException: class name (com/mas/sunmeng/hotfixtest/BugClass) does not match path (BugClass.class)
打成补丁包后下面进行使用
    private String LoadBugClassName = "com.mas.sunmeng.hotfixtest.BugClass";

    /**
     * jar cvf pathbug.jar com/mas/sunmeng/hotfixtest/BugClass.class //必须是全路径
     * 否则执行./dx --dex --output=path_dex.jar pathbug.jar会报错
     * 错误提示Caused by: com.android.dx.cf.iface.ParseException: class name (com/mas/sunmeng/hotfixtest/BugClass) does not match path (BugClass.class)
     */

    private static final int BUF_SIZE = 2048;
    @Override
    public void onCreate() {
        super.onCreate();
        File dexpath  = new File(getDir("dex", Context.MODE_PRIVATE), "path_dex.jar");

        //将path包复制到私有目录下
        BufferedInputStream bis = null;
        OutputStream dexWriter = null;
        try{
            bis = new BufferedInputStream(getAssets().open("path_dex.jar"));
            dexWriter = new BufferedOutputStream(new FileOutputStream(dexpath));
            byte[] buf = new byte[BUF_SIZE];
            int len;
            while ((len = bis.read(buf, 0, BUF_SIZE)) > 0){
                dexWriter.write(buf, 0, len);
            }
        }catch (Exception e){

        }finally {
            if(bis != null){
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(dexWriter != null){
                try {
                    dexWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }


        //文件复制成功后
        if(dexpath.getAbsolutePath() != null && new File(dexpath.getAbsolutePath()).exists()) {

            //判断是否有loader,根据不同的系统中ClassLoader的类型来做相应的处理
            //LexClassLoader应该是阿里自己的ClassLoader
            // 我们分 API 14以上和以下进行分析
            try{
                if(hasLexClassLoader()){
                    PathClassLoader obj = (PathClassLoader) getClassLoader();
                    String replaceAll = new File(dexpath.getAbsolutePath()).getName().replaceAll("\\.[a-zA-Z0-9]+", ".lex");
                    Class cls = Class.forName("dalvik.system.LexClassLoader");
                    Object newInstance =
                            cls.getConstructor(new Class[] {String.class, String.class, String.class, ClassLoader.class}).newInstance(
                                    new Object[] {getDir("dex", 0).getAbsolutePath() + File.separator + replaceAll,
                                            getDir("dex", 0).getAbsolutePath(), dexpath.getAbsolutePath(), obj});
                    cls.getMethod("loadClass", new Class[] {String.class}).invoke(newInstance, new Object[] {LoadBugClassName});
                    setField(obj, PathClassLoader.class, "mPaths",
                            appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(newInstance, cls, "mRawDexPath")));
                    setField(obj, PathClassLoader.class, "mFiles",
                            combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(newInstance, cls, "mFiles")));
                    setField(obj, PathClassLoader.class, "mZips",
                            combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(newInstance, cls, "mZips")));
                    setField(obj, PathClassLoader.class, "mLexs",
                            combineArray(getField(obj, PathClassLoader.class, "mLexs"), getField(newInstance, cls, "mDexs")));
                }else if(hasDexClassLoader()){
                    //系统版本在14以上的
                    PathClassLoader pathClassLoader = (PathClassLoader) getClassLoader();
                    // 根据 DexPathList类及属性名dexElements获取到我们补丁包对应的有序数组dexElements上面已经得到了两个有序数组dexElements,
                    // 一个存放的的是没有打补丁之前的dex有序数组dexElements,另外一个是我们的补丁包对应的dex有序数组dexElements
                    //  a 就是我们合并后的有序dex数组dexElements
                    Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
                            getDexElements(getPathList(
                                    new DexClassLoader(dexpath.getAbsolutePath(), getDir("dex", 0).getAbsolutePath(), dexpath.getAbsolutePath(), getClassLoader()))));
                    Object a2 = getPathList(pathClassLoader);
                    // 重新设置DexPathList 的有序数组对象dexElements值
                    setField(a2, a2.getClass(), "dexElements", a);
                    pathClassLoader.loadClass(LoadBugClassName);
                }else{
                    injectBelowApiLevel14(this, dexpath.getAbsolutePath(), LoadBugClassName);
                }
            }catch (Exception e){

            }
        }

    }

    @TargetApi(14)
    private static void injectBelowApiLevel14(Context context, String dexpath, String bugclass)
            throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        PathClassLoader obj = (PathClassLoader) context.getClassLoader();
        DexClassLoader dexClassLoader =
                new DexClassLoader(dexpath, context.getDir("dex", 0).getAbsolutePath(), dexpath, context.getClassLoader());
        dexClassLoader.loadClass(bugclass);
        setField(obj, PathClassLoader.class, "mPaths",
                appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(dexClassLoader, DexClassLoader.class,
                        "mRawDexPath")
                ));
        setField(obj, PathClassLoader.class, "mFiles",
                combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(dexClassLoader, DexClassLoader.class,
                        "mFiles")
                ));
        setField(obj, PathClassLoader.class, "mZips",
                combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(dexClassLoader, DexClassLoader.class,
                        "mZips")));
        setField(obj, PathClassLoader.class, "mDexs",
                combineArray(getField(obj, PathClassLoader.class, "mDexs"), getField(dexClassLoader, DexClassLoader.class,
                        "mDexs")));
        obj.loadClass(bugclass);
    }

    private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
            IllegalAccessException {
        return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
        return getField(obj, obj.getClass(), "dexElements");
    }


    private static Object appendArray(Object obj, Object obj2) {
        Class componentType = obj.getClass().getComponentType();
        int length = Array.getLength(obj);
        Object newInstance = Array.newInstance(componentType, length + 1);
        Array.set(newInstance, 0, obj2);
        for (int i = 1; i < length + 1; i++) {
            Array.set(newInstance, i, Array.get(obj, i - 1));
        }
        return newInstance;
    }

    private static Object getField(Object obj, Class cls, String str)
            throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);
        return declaredField.get(obj);
    }

    private static void setField(Object obj, Class cls, String str, Object obj2)
            throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);
        declaredField.set(obj, obj2);
    }

    private static Object combineArray(Object obj, Object obj2) {
        Class componentType = obj2.getClass().getComponentType();
        int length = Array.getLength(obj2);
        int length2 = Array.getLength(obj) + length;
        Object newInstance = Array.newInstance(componentType, length2);
        for (int i = 0; i < length2; i++) {
            if (i < length) {
                Array.set(newInstance, i, Array.get(obj2, i));
            } else {
                Array.set(newInstance, i, Array.get(obj, i - length));
            }
        }
        return newInstance;
    }

    private static boolean hasLexClassLoader() {
        try {
            Class.forName("dalvik.system.LexClassLoader");
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }

    private static boolean hasDexClassLoader() {
        try {
            Class.forName("dalvik.system.BaseDexClassLoader");
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }



猜你喜欢

转载自blog.csdn.net/H291850336/article/details/76147641