Android热修复动手实现

一、前言

       最近看了很多第三方的热修复框架的实现,比如阿里的AndFix,对于我们在自己的app里面接入SDK很是方便,至于内部的实现基本不需要我们怎么关注都可以。如此,我们就真的变成搬砖的码农,所以,不行,我们得尝试自己手动来实现一遍安卓的热修复,究其是如何实现的。

二、实现原理

       在动手前,我们对其原理得有个大概的了解。我们知道,Java的虚拟机JVM运行代码时,加载的是.class字节码文件,而Android的Dalvik/ART虚拟机加载的是Dex文件,不过他们的工作机制是一样的,都经过ClassLoader这个类加载器,只不过,Android重新定义了两个类 DexClassLoader和PathClassLoader去解析类,他们是继承BaseDexClassLoader类的,关于这两个类的介绍:
1)、PathClassLoader:官方文档解析 Android uses  this class for its system class loader and for its application   class loader(s),使用该类作为系统类和应用类的加载器,也即只能加载已经安装到Android系统的apk文件
2)、 DexClassLoader:可加载jar、apk和dex文件,可以从存储外部加载。
本文我们通过DexClassLoader来来加载dex文件,然后通过Java类反射的原理来实现热修复的功能,先来看一段DexClassLoader 使用的代码:
/**
 * 第一个参数:是dex压缩文件的路径
 * 第二个参数:是旧dex解压优化后生成新dex存放的目录(注意:API文档说明,要求我们不能把dex存放在外部存储器中)
 * 第三个参数:本地依赖库目录,可以为null
 * 第四个参数:是上一级的类加载器
*/
DexClassLoader dexClassLoader = new DexClassLoader(dexPath,dexOutputDirs,null,getClassLoader());
Class<?> clazz = null;
try {
   clazz = dexClassLoader.loadClass("com.lsy.hotfix.Bugs");
   Bugs bugs = (Bugs) clazz.newInstance();
} catch (Exception e) {
    e.printStackTrace();
}
上面这段代码可以看到通过DexClassLoader类,我们可以从一个Dex文件中提取一个简单类出来。

三、实现步骤

1、首先修改我们要修复的的类,这里很简单,就toast出来一句提示
public class Bugs {
    public static void showToast(Context context){
        Toast.makeText(context, "已修复", Toast.LENGTH_SHORT).show();
    }  
}
2、把修改后的类,打包成dex文件
把.class字节码文件打包成dex文件,我们需要用到Android工具包里面的dx打包工具,该工具在我们sdk安装目录下的build-tools/22.0.1(对应一个版本的)的文件夹里面。
 
这个时候,我们cmd在windows的命令行窗口输入dx命令还是不认的,因为如果我们希望全局快捷使用,还得把这个目录配置到我们的环境变量中去,配置完环境变量之后,再到窗口输入dx命令,就可以看到

看dx工具的使用还挺多的参数,那么我们只需要知道其基本的用法就够了
dx --dex [--output=<file>] [<file>.class | <file>.{zip,jar,apk} | <directory>]
这里,我使用下面的命令进行生成,特别注意,要用绝对路径



  
  
 patch.dex文件已经生成。
三、补丁类替换
 
补丁生成后,我们这里把它放到手机外部存储的根目录,(通常如果结合后台服务器的话,我们可以把dex文件放到远程服务器,然后下载下来。)
接下来就是进行类的替换,这应该是本篇的重点所在。我们所有的类被调用都是通过ClassLoader类加载器的findClass()方法来进行查找的,在Android系统中类被加载顺序是:
ClassLoader ——> BaseDexClassLoader ——> PathClassLoader ——> DexClassLoader
其中,根据上面,PathClassLoader 和 DexClassLoader 是Android系统继承BaseDexClassLoader实现的,而已被安装的类会在PathClassLoader找到,所以,我们要把补丁的类替代有问题的类,那么就要把从DexClassLoader加载的到合拼到PathClassLoader的类前面去,这样,就会优先执行修复的类。具体点,我们看下源码:
1)、BaseDexClassLoader的源码:
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
2)、DexPathList核心代码
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
/**
     * 加载dex或资源文件,存放在一个element数组返回
     */
    private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                             ArrayList<IOException> suppressedExceptions) {
        ArrayList<Element> elements = new ArrayList<Element>();
        /*
         * Open all files and load the (direct or contained) dex files
         * up front.
         */
        for (File file : files) {
            File zip = null;
            DexFile dex = null;
            String name = file.getName();

            if (name.endsWith(DEX_SUFFIX)) {
                // Raw dex file (not inside a zip/jar).
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {
                zip = file;

                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) {
                    /*
                     * IOException might get thrown "legitimately" by the DexFile constructor if the
                     * zip file turns out to be resource-only (that is, no classes.dex file in it).
                     * Let dex == null and hang on to the exception to add to the tea-leaves for
                     * when findClass returns null.
                     */
                    suppressedExceptions.add(suppressed);
                }
            } else if (file.isDirectory()) {
                // We support directories for looking up resources.
                // This is only useful for running libcore tests.
                elements.add(new Element(file, true, null, null));
            } else {
                System.logW("Unknown file type for: " + file);
            }

            if ((zip != null) || (dex != null)) {
                elements.add(new Element(file, false, zip, dex));
            }
        }

        return elements.toArray(new Element[elements.size()]);
    }

3)、PathClassLoader继承 BaseDexClassLoader,其 构造器 源码:
 public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
 }

4)、DexClassLoader同样也是继承BaseDexClassLoader,其构造器源码:
public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
从分析上面这几个加载器的源码,我们要替代类的做法,就是将DexClassLoader中的 dexElements  与PathClassLoader中的 dexElements  进行合拼,并且把补丁dex的类放到新 dexElements最前面,这样,加载的Bugs类就是最新补丁的类了。
下面是替换类的详细实现代码:
package com.lsy.hotfix;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import android.annotation.TargetApi;
import android.content.Context;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
public final class HotFix {
    
    /**
     * 修复指定的类
     * @param context 上下文对象
     * @param patchDexFile dex文件
     * @param patchClassName 被修复类名
     */
    public static void patch(Context context, String patchDexFile, String patchClassName) {
        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) {
            }
        }
    }
    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;
        }
    }
    private static void injectInAliyunOs(Context context, String patchDexFile, String patchClassName)
        throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException,
        InstantiationException, NoSuchFieldException {
        PathClassLoader obj = (PathClassLoader) context.getClassLoader();
        String replaceAll = new File(patchDexFile).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[] {context.getDir("dex", 0).getAbsolutePath() + File.separator + replaceAll,
                    context.getDir("dex", 0).getAbsolutePath(), patchDexFile, obj});
        cls.getMethod("loadClass", new Class[] {String.class}).invoke(newInstance, new Object[] {patchClassName});
        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")));
    }
    @TargetApi(14)
    private static void injectBelowApiLevel14(Context context, String str, String str2)
        throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        PathClassLoader obj = (PathClassLoader) context.getClassLoader();
        DexClassLoader dexClassLoader =
            new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader());
        dexClassLoader.loadClass(str2);
        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(str2);
    }
    private static void injectAboveEqualApiLevel14(Context context, String str, String str2)
        throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
        Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
            getDexElements(getPathList(
                new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));
        Object a2 = getPathList(pathClassLoader);
        //新的dexElements对象重新设置回去
        setField(a2, a2.getClass(), "dexElements", a);
        pathClassLoader.loadClass(str2);
    }
    /**
     * 通过反射先获取到pathList对象
     * @param obj
     * @return
     * @throws ClassNotFoundException
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
        IllegalAccessException {
        return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }
    /**
     * 从上面获取到的PathList对象中,进一步反射获得dexElements对象
     * @param obj
     * @return
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
        return getField(obj, obj.getClass(), "dexElements");
    }
    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);
    }
    //合拼dexElements
    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 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;
    }
}

四、APP调用Patch
public void onClick(View v){
        if(v.getId() == R.id.btn_show){
            Bugs.showToast(this);
        }else if(v.getId() == R.id.btn_fix){
            String dexPath = Environment.getExternalStorageDirectory()+"/patch.dex";
            HotFix.patch(this, dexPath,"com.lsy.hotfix.Bugs");
            System.out.println("已修复"+dexPath);
        }
}  
到此结束!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值