六、【热修复二】Java层类加载热修复的简单实现

使用到的技术点

1、Java类加载机制;
2、Android加载dex文件;
3、反射;

原理:

用修复好的类替换有问题的类。在App重新启动后让Classloader去加载新的类。因为在App运行到一半的时候,所有需要发生变更的类已经被加载过了,在Android上是无法对一个类进行卸载的。如果不重启,原来的类还在虚拟机中,就无法加载新类。因此,只有在下次重启的时候,在还没有走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会Resolve为新类。从而达到热修复的目的。

关键问题是:

如何替换一个类?
这里没有直接替换一个类,而是通过类加载机制,在加载问题类之前,先加载补丁类的方案达到替换的目的。具体操作的依据,首先因为同一个类加载器在尝试加载一个类的时候,会先判断这个类是否已经加载,如果已经加载则不会再次去加载;其次,Android加载dex时,会按照先后顺序依次加载的

一、Java加载机制

ClassLoader#loadClass()方法

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                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.
                    c = findClass(name);
                }
            }
            return c;
    }

(1)、从当前类加载器的已加载类缓存中根据类的全路径名查询是否存在该类,如果存在则直接返回。
(2)、如果当前类存在父类加载器,则调用父类加载器的loadClass(name,false)方法对其进行加载。
(3)、如果当前类加载器不存在父类加载器,则直接调用启动类加载器对该类进行加载。
(4)、如果当前类的所有父类加载器都没有成功加载class,则尝试调用当前类加载器的findClass方法对其进行加载,该方法就是我们自定义加载器需要重写的方法
(5)、最后如果类被成功加载,则做一些性能数据的统计。
(6)、由于loadClass指定了resolve为false,所以不会进行连接阶段的继续执行,这也就解释了为什么通过类加载器加载类并不会导致类的初始化。

二、Android类加载的机制

Android中相关的类加载器PathClassLoaderDexClassLoader

(1)、DexClassLoader:能够加载自定义的jar/apk/dex
(2)、PathClassLoader:只能加载系统中已经安装过的apk
所以Android系统默认的类加载器为PathClassLoader,而DexClassLoader可以像JVM的ClassLoader一样提供动态加载。

Android类加载器的继承关系.png

PathClassLoader和DexClassLoader加载Class的相关源码:

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

DexPathList #findClass()

final class DexPathList {
    /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private 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;
    }

}

DexFile#loadClassBinaryName()

    public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
        return defineClass(name, loader, mCookie, this, suppressed);
    }

    private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                     DexFile dexFile, List<Throwable> suppressed) {
        Class result = null;
        try {
            result = defineClassNative(name, loader, cookie, dexFile);
        } catch (NoClassDefFoundError e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (ClassNotFoundException e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        }
        return result;
    }

从上面的源码可以得知,当Android中类加载器尝试加载类的时候,会调用DexPathListfindClass()方法,通过遍历dexElements中的Element,得到DexFile,再通过DexFile调用loadClassBinaryName()方法加载类。类加载成功后,就直接返回。因此,可以通过将修复好的dex插入到dexElements的集合(出现bug的xxx.class所在的dex的前面)的位置,就可以达到间接替换bug类的目的
最本质的实现原理:类加载器去加载某个类的时候,是去dexElements里面从头往下查找的。fixed.dex,classes1.dex,classes2.dex,classes3.dex

三、具体实现

Class  BaseDexClassLoader{
    private DexPathList pathList;
}

Class  DexPathList{
    private Element[] dexElements;
}

先获取到已安装App中原有的dexElements(appDexElements)和通过DexClassLoader加载已经修复好的dex得到的dexElements(fixedDexElements)。然后再将appDexElements和fixedDexElements合并成一个新的dexElements(newDexElements)。

类加载修复的流程.png

核心类代码
FixDexUtils

public class FixDexUtils {
    private static HashSet<File> sLoadedDexFiles = new HashSet();

    static {
        sLoadedDexFiles.clear();
    }

    public static void loadFixedDex(Context context){
        File fileDir = context.getDir(Constants.DEX_DIR, Context.MODE_PRIVATE);
        File[] dexFiles = fileDir.listFiles();
        for(File file:dexFiles){
            if(file.getName().startsWith("classes") && file.getName().endsWith(".dex")){
                sLoadedDexFiles.add(file);
            }
        }

        doDexInject(context, fileDir, sLoadedDexFiles);
    }

    private static void doDexInject(final Context context, File filesDir, HashSet<File> loadedDexs){
        String optimizedDir = filesDir.getAbsolutePath() + File.separator + Constants.OPT_DEX;
        File fopt = new File(optimizedDir);
        if(!fopt.exists()){
            fopt.mkdirs();
        }
        try {
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            for(File dex:loadedDexs){
                DexClassLoader dexClassLoader = new DexClassLoader(dex.getAbsolutePath(), fopt.getAbsolutePath(), null, pathClassLoader);

                Object dexObj = getPathList(dexClassLoader);
                Object pathObj = getPathList(pathClassLoader);

                Object dexElementsList = getDexElements(dexObj);
                Object pathDexElementsList = getDexElements(pathObj);

                //合并dexElements
                Object dexElements = combineArray(dexElementsList, pathDexElementsList);

                //重新给PathList里面的Element[] dexElements赋值
                Object pathList = getPathList(pathClassLoader);
                setFiled(pathList, pathList.getClass(), "dexElements", dexElements);
                Object finalElementsList = getDexElements(pathList);
                System.out.println("--------"+finalElementsList);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void setFiled(Object obj, Class<?> clazz, String fieldName, Object value) throws Exception {
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    private static Object getField(Object obj, Class<?> clazz, String fieldName) throws NoSuchFieldException, IllegalAccessException {
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(obj);
    }

    private static Object getPathList(Object baseDexClassLoader) throws Exception {
        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

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

    private static Object combineArray(Object arrLhs, Object arrRhs){
        Class<?> clazz = arrLhs.getClass().getComponentType();
        int i = Array.getLength(arrLhs);
        int len = i + Array.getLength(arrRhs);
        Object arrResult = Array.newInstance(clazz, len);
        for(int k = 0; k < len; k++){
            if(k < i){
                Array.set(arrResult, k, Array.get(arrLhs, k));
            }else{
                Array.set(arrResult, k, Array.get(arrRhs, k - i));
            }
        }
        return arrResult;
    }

}

FMApplication

public class FMApplication extends Application {

    @Override
    public void onCreate() {
        ClassLoader classLoader = getClassLoader();
        System.out.println("--------onCreate---ClassLoader:"+getClassLoader());
        super.onCreate();
    }

    @Override
    protected void attachBaseContext(Context base) {
        System.out.println("--------attachBaseContext");
        MultiDex.install(base);
        FixDexUtils.loadFixedDex(base);
        super.attachBaseContext(base);

    }


}

app#build.gradle的核心配置
android {
    compileSdkVersion 27
    defaultConfig {
        ...
        multiDexEnabled true
        ...
    }

    buildTypes {
        release {
            //指定单独打到一个dex的类
            multiDexKeepFile file('dex.keep')
            def myFile = file('dex.keep')
            println("isFileExists:"+myFile.exists())
            println "dex keep"

            minifyEnabled false
          
        }
    }

}

dependencies {
   ...
    implementation 'com.android.support:multidex:1.0.1'
  ...
}

dex.keep的配置和目录.png

四、关于FixedClass.class打包成classes.dex

dx.bat所在目录:

sdk\build-tools\27.0.3

dx --dex --output=D:\Users\x\Desktop\dex\classes2.dex D:\Users\x\Desktop\dex

命令解释:
--output=D:\Users\x\Desktop\dex\classes2.dex 指定输出路径
D:\Users\x\Desktop\dex 最后指定去打包哪个目录下面的class字节文件(注意要包括全路径的文件夹,也可以有多个class)


class_2_dex.png
class_2_dex命令.png
class_2_dex结果.png

五、Java反射机制可以动态修改实例中final修饰的成员变量吗?

回答是分两种情况的。

  1. 当final修饰的成员变量在定义的时候就初始化了值,那么java反射机制就已经不能动态修改它的值了。
  2. 当final修饰的成员变量在定义的时候并没有初始化值的话,那么就还能通过java反射机制来动态修改它的值。

【相关源码】HotFixJava

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355

推荐阅读更多精彩内容