什么?Android-Multidex热更新你都不会(1),奈学大数据

本文深入探讨了Android应用的Dex热修复技术,通过理解ClassLoader的加载机制,尤其是双亲委托机制,解析了如何hook ClassLoader以实现修复bug的dex文件优先加载。文章详细介绍了不同Android版本的hook策略,并提供了具体的Java代码示例,展示了如何在PathClassLoader中插入修复的Dex元素,从而在不更新apk的情况下修复应用中的问题。
摘要由CSDN通过智能技术生成
    Method makePathElementsMethod = ReflectionUtil.getMethod(//获得 DexPathList 的 makePathElements 方法
            dexPathListObj, "makePathElements", List.class, File.class, List.class);
    assert makePathElementsMethod != null;
    Object[] newElements = (Object[]) makePathElementsMethod.invoke(//这个方法是静态方法,所以不需要传实例,直接invoke;这里取得的返回值就是 我们外部的dex文件构建成的 Element数组
            null, files, optimizedDirectory, ioExceptions);//构建出一个新的Element数组
    //下面把新数组和旧数组合并,注意新数组放前面
    Object[] dexElements = null;
    if (newElements != null && newElements.length > 0) {
        dexElements = (Object[]) Array.newInstance(oldElements.getClass().getComponentType(), oldElements.length + newElements.length);//先建一个新容器
        //这5个参数解释一下, 如果是将A,B 你找AB的顺序组合成数组C,那么参数的含义,依次是 A对象,A数组开始复制的位置,C对象,C对象的开始存放的位置,数组A中要复制的元素个数
        System.arraycopy(
                newElements, 0, dexElements, 0, newElements.length);//新来的数组放前面
        System.arraycopy(
                oldElements, 0, dexElements, newElements.length, oldElements.length);
    }
    //最后把合并之后的数组设置到 dexElements里面
    dexElementsField.set(dexPathListObj, dexElements);
}

public static void hookV19(ClassLoader classLoader, File outDexFilePath, File optimizedDirectory) throws IllegalAccessException, InvocationTargetException {
    Field pathList = ReflectionUtil.getField(classLoader, "pathList");//1、获得DexPathList pathList 属性
    Object dexPathListObj = pathList.get(classLoader); //2、获得DexPathList pathList对象
    Field dexElementsField = ReflectionUtil.getField(dexPathListObj, "dexElements");//3、获得DexPathList的dexElements属性
    Object[] oldElements = (Object[]) dexElementsField.get(dexPathListObj);//4、获得pathList对象中 dexElements 的属性值

    List<File> files = new ArrayList<>();//开始构建makeDexElements的实参
    files.add(outDexFilePath);
    List<IOException> ioExceptions = new ArrayList<>();
    Method makePathElementsMethod = ReflectionUtil.getMethod(//获得 DexPathList 的 makeDexElements 方法
            dexPathListObj, "makeDexElements", ArrayList.class, File.class, ArrayList.class);//别忘了后面的参数列表
    Object[] newElements = (Object[]) makePathElementsMethod.invoke(
            null, files, optimizedDirectory, ioExceptions);//这个方法是静态方法,所以不需要传实例,直接invoke;这里取得的返回值就是 我们外部的dex文件构建成的 Element数组

    //下面把新数组和旧数组合并,注意新数组放前面
    Object[] dexElements = null;
    if (newElements != null && newElements.length > 0) {
        dexElements = (Object[]) Array.newInstance(oldElements.getClass().getComponentType(), oldElements.length + newElements.length);//先建一个新容器
        //这5个参数解释一下, 如果是将A,B 你找AB的顺序组合成数组C,那么参数的含义,依次是 A对象,A数组开始复制的位置,C对象,C对象的开始存放的位置,数组A中要复制的元素个数
        System.arraycopy(
                newElements, 0, dexElements, 0, newElements.length);//新来的数组放前面
        System.arraycopy(
                oldElements, 0, dexElements, newElements.length, oldElements.length);
    }

    //最后把合并之后的数组设置到 dexElements里面
    dexElementsField.set(dexPathListObj, dexElements);
}

//14和19的区别,是这个方法 makeDexElements(ArrayList<File> files,File optimizedDirectory)···它又少了一个参数
public static void hookV14(ClassLoader classLoader, File outDexFilePath, File optimizedDirectory) throws IllegalAccessException, InvocationTargetException {
    Field pathList = ReflectionUtil.getField(classLoader, "pathList");//1、获得DexPathList pathList 属性
    Object dexPathListObj = pathList.get(classLoader); //2、获得DexPathList pathList对象
    Field dexElementsField = ReflectionUtil.getField(dexPathListObj, "dexElements");//3、获得DexPathList的dexElements属性
    Object[] oldElements = (Object[]) dexElementsField.get(dexPathListObj);//4、获得pathList对象中 dexElements 的属性值

    List<File> files = new ArrayList<>();//开始构建makeDexElements的实参
    files.add(outDexFilePath);
    List<IOException> ioExceptions = new ArrayList<>();
    Method makePathElementsMethod = ReflectionUtil.getMethod(//获得 DexPathList 的 makeDexElements 方法
            dexPathListObj, "makeDexElements", ArrayList.class, File.class);//别忘了后面的参数列表
    Object[] newElements = (Object[]) makePathElementsMethod.invoke(
            null, files, optimizedDirectory, ioExceptions);//这个方法是静态方法,所以不需要传实例,直接invoke;这里取得的返回值就是 我们外部的dex文件构建成的 Element数组

    //下面把新数组和旧数组合并,注意新数组放前面
    Object[] dexElements = null;
    if (newElements != null && newElements.length > 0) {
        dexElements = (Object[]) Array.newInstance(oldElements.getClass().getComponentType(), oldElements.length + newElements.length);//先建一个新容器
        //这5个参数解释一下, 如果是将A,B 你找AB的顺序组合成数组C,那么参数的含义,依次是 A对象,A数组开始复制的位置,C对象,C对象的开始存放的位置,数组A中要复制的元素个数
        System.arraycopy(
                newElements, 0, dexElements, 0, newElements.length);//新来的数组放前面
        System.arraycopy(
                oldElements, 0, dexElements, newElements.length, oldElements.length);
    }

    //最后把合并之后的数组设置到 dexElements里面
    dexElementsField.set(dexPathListObj, dexElements);
}

}

```java
import android.app.Application;
import android.content.Context;
import android.os.Build;
import android.util.Log;

import com.example.administrator.myapplication.utils.AssetsFileUtil;
import com.example.administrator.myapplication.utils.ClassLoaderHookHelper;

import java.io.File;


public class MyApp extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        Log.d("BugTag2", "" + getClassLoader());//PathClassLoader
        Log.d("BugTag2", "" + getClassLoader().getParent());//BootClassLoader

        String fixPath = "fix.dex";
        try {
            String path = AssetsFileUtil.copyAssetToCache(this, fixPath);
            File fixFile = new File(path);
            if (Build.VERSION.SDK_INT >= 23) {
                ClassLoaderHookHelper.hookV23(getClassLoader(), fixFile, getCacheDir());
            } else if (Build.VERSION.SDK_INT >= 19) {
                ClassLoaderHookHelper.hookV19(getClassLoader(), fixFile, getCacheDir());
            } else if (Build.VERSION.SDK_INT >= 14) {
                ClassLoaderHookHelper.hookV14(getClassLoader(), fixFile, getCacheDir());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

Multidex热修复核心技术

其实 热修复的核心技术,就一句话,Hook ClassLoader ,但是要深入了解它,需要相当多的基础知识,下面列举出必须要知道的一些东西。

基础知识预备

1.Dex文件是什么?

我们写安卓,目前还是用java比较多,就算是用kotlin,它最终也是要转换成java来运行。java文件,被编译成class之后,多个class文件,会被打包成classes.dex,被放到apk中,安卓设备拿到apk,去安装解析(预编译balabala...),当我们运行app时,app的程序逻辑全都是在classes.dex中。
所以,dex文件是什么?
一句话
dex文件是 android app的源代码的最终打包

2.Dex文件如何生成?
androidStudio 打包apk的时候会生成Dex,其实它使用的是SDKdx命令,我们可以用dx命令自己去打包想要打包的 class.
命令格式为: dx --dex --output=output.dex xxxx.class
将上面的output 和 xxxx换成你想要的文件名即可。
注:dx.bat在 安卓SDK的目录下:比如我的 C:\XXXXX\AndroidStudioAbout\sdk1\build-tools\28.0.3\dx.bat

3.ClassLoader是什么?
ClassLoader来自jdk,翻译为 : 类加载器,用于将class文件中的类,加载到内存中,生成class对象。只有存在了Class对象,我们才可以创建我们想要的对象。
android SDK继承了JDK的classLoader,创造出了新的ClassLoader子类。下图表示了android9.0-28 所有的ClassLoader直接或者间接子类.

 android28所有的ClassLoader子类
用的比较多的是 BaseDexClassLoaderDexClassLoader , PathClassLoader, 其他这些,应该是谷歌大佬 创造出来新的 类加载器子类吧,还没研究过。

注:关于DexClassLoaderPathClassLoader ,网上资料有个误区,应该不少人都认为,PathClassLoader 用于加载app内部的dex文件,DexClassLoader用于加载外部的dex文件,但是其实只要看一眼 这两个类的关系,就会发现,它们都是继承自 BaseDexClassLoader,他们的构造函数内部都会去执行父类的构造函数。 他们只有一个差别,那就是 PathClssLoader不用传 optimizedDirectory这个参数,但是DexClassLoader必须传。这个参数的作用是,传入一个 dex优化之后的存放目录。而事实上,虽然PathClassLoader不要求传这个optimizedDirectory,但是它实际上是给了一个默认值。 emmmm…所以不要再认为PathClassLoader不能加载外部的dex了,它只是没有让你传 optimizedDirectory而已。
另外:
BootClassLoader 用于加载Android Framework层class文件(SDK中没有这个BootClassLoader,也是很奇怪)
PathClassLoader 是用于Android应用程序类的加载器,可以加载指定的dex,以及jarzipapk中的classes.dex
DexClassLoader 可以加载指定的dex,以及jarzipapk中的classes.dex

4.ClassLoader的双亲委托机制是什么?
android里面ClassLoader的作用,是将dex文件中的类,加载到内存中,生成Class对象,供我们使用
(举个例子:我写了一个A类,app运行起来之后,当我需要new 一个AClassLoader首先会帮我查找AClass对象是否存在,如果存在,就直接给我Class对象,让我拿去new A,如果不存在,就会出创建这个AClass对象。)
这个查找的过程,就遵循双亲委托机制
一句话解释 双亲委托机制:某个类加载器在加载某个的时候,首先会将 这件事委托给 parent类加载器,依次递归,如果parent类加载器可以完成加载,就会直接返回Class对象。如果parent找不到或者没有父了,就会自己加载。

下图是 安卓源码ClassLoader.java:

这里红字注解,很容易读懂ClassLoaderload一个class的过程.

hook思路

OK,现在可以来解读我是如何去hook ClassLoader的了.
解读之前,先弄清楚,我为何 要hook ClassLoader,为什么hook了它之后,我的fix.dex就能发挥作用?
先解决这个疑问,既然是hook,那么自然要读懂源码,因为hook就是在理解源码思维的前提下,更改源码逻辑。

一张图解决你的疑问:
类的加载过程

按照上面图,去追踪源码,会发现,ClassLoader最终会从DexFile对象中去获得一个Class对象。并且在DexPathList类中findClass的时候,存在一个Element数组的遍历。
这就意味着,如果存在多个dex文件,多个dex文件中都存在同样一个class,那么它会从第一个开始找,如果找到了,就会立即返回。如果没找到,就往下一个dex去找。

也就是说,如果我们可以在 这个数组中插入我们自己的修复bug的 fix.dex,那我们就可以让我们已经修复bug的补丁类发挥作用,让类加载器优先读取我们的补丁类.

OK,理解了源码的逻辑,那我们可以动手了。来解析SDK 23的hook ClassLoader过程吧!

确定思路,我们要改变app启动之后,自带的ClassLoader对象(具体实现类是PathClassLoader )中 DexPathList 中 Element[] element 的实际值。

那么,步骤:
***1.取得PathClassLoader的pathList的属性
2.取得PathClassLoader的pathList的属性真实值(得到一个DexPathList对象)
3.获得DexPathList中的dexElements 属性

最后

按照上面的过程,4个月的时间刚刚好。当然Java的体系是很庞大的,还有很多更高级的技能需要掌握,但不要着急,这些完全可以放到以后工作中边用别学。

学习编程就是一个由混沌到有序的过程,所以你在学习过程中,如果一时碰到理解不了的知识点,大可不必沮丧,更不要气馁,这都是正常的不能再正常的事情了,不过是“人同此心,心同此理”的暂时而已。

道路是曲折的,前途是光明的!”

过程,所以你在学习过程中,如果一时碰到理解不了的知识点,大可不必沮丧,更不要气馁,这都是正常的不能再正常的事情了,不过是“人同此心,心同此理”的暂时而已。

道路是曲折的,前途是光明的!”

[外链图片转存中…(img-AIPtO1CK-1628610710477)]

[外链图片转存中…(img-tcmP3Yyf-1628610710478)]

更多Java核心笔记、真实面经、学习笔记等知识干货可以点击这里免费领取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值