IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android 进阶——MultiDex分包与动态加载原理剖析 -> 正文阅读

[移动开发]Android 进阶——MultiDex分包与动态加载原理剖析

一、为什么要进行Dex分包

Android下单个Dex文件存在65535函数数量的限制,而一个功能稍微复杂点的APP很容易超过这个限制,为此,我们引入了Dex分包,将一个App所有class分别打包为classes1.dex, classes2.dex, classes3.dex…

二、Dex加载时机

首先,从原理上分析,多个Dex的加载必须要在class被load进内存之前,否则会导致loadClass过程中出现ClassNotFoundException,而Dex的加载是通过MultiDex.install(context)方法进行的,这个方法的内部实现细节我们稍后分析。可以看到这个方法是需要context参数的,而在APP启动过程中,最早可以拿到context的回调就是Application.attachBaseContext(),对于App启动流程不清楚的可以点这里:App启动流程,所以Dex的加载我们会放在App的Application中的attachBaseContext()回调中进行:

// APP自定义的Application
public class MyApplication extends Application {
  @Override
  protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);
  }
}

三、Dex加载原理

我们先来看看MultiDex.install()的源码

public static void install(Context context) {
    Log.i(TAG, "install");
    //判断Android系统是否已经支持了MultiDex,如果支持了就不需要再去安装了,直接返回
    if (IS_VM_MULTIDEX_CAPABLE) {
        Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
        return;
    }

    // 如果Android系统低于MultiDex最低支持的版本就抛出异常
    if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
        throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
                + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
    }
    try {
        // 获取应用信息
        ApplicationInfo applicationInfo = getApplicationInfo(context);
        // 如果应用信息为空就返回,比如说运行在一个测试的Context下。
        if (applicationInfo == null) {
            // Looks like running on a test Context, so just return without patching.
            return;
        }
        // 同步方法
        synchronized (installedApk) {
            // 获取已经安装的APK的全路径
            String apkPath = applicationInfo.sourceDir;
            if (installedApk.contains(apkPath)) {
                return;
            }
            // 把路径添加到已经安装的APK路径中
            installedApk.add(apkPath);
            // 如果编译版本大于最大支持版本,报一个警告
            if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
                Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "
                        + Build.VERSION.SDK_INT + ": SDK version higher than "
                        + MAX_SUPPORTED_SDK_VERSION + " should be backed by "
                        + "runtime with built-in multidex capabilty but it's not the "
                        + "case here: java.vm.version=\""
                        + System.getProperty("java.vm.version") + "\"");
            }
            /* The patched class loader is expected to be a descendant of
             * dalvik.system.BaseDexClassLoader. We modify its
             * dalvik.system.DexPathList pathList field to append additional DEX
             * file entries.
             */
            ClassLoader loader;
            try {
                // 获取ClassLoader,实际上是PathClassLoader
                loader = context.getClassLoader();
            } catch (RuntimeException e) {
                /* Ignore those exceptions so that we don't break tests relying on Context like
                 * a android.test.mock.MockContext or a android.content.ContextWrapper with a
                 * null base Context.
                 */
                Log.w(TAG, "Failure while trying to obtain Context class loader. " +
                        "Must be running in test mode. Skip patching.", e);
                return;
            }
            // 在某些测试环境下ClassLoader为null
            if (loader == null) {
                // Note, the context class loader is null when running Robolectric tests.
                Log.e(TAG,
                        "Context class loader is null. Must be running in test mode. "
                                + "Skip patching.");
                return;
            }
            try {
                // 清除老的缓存的Dex目录,来源的缓存目录是"/data/user/0/${packageName}/files/secondary-dexes"
                clearOldDexDir(context);
            } catch (Throwable t) {
                Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
                        + "continuing without cleaning.", t);
            }
            // 新建一个存放dex的目录,路径是"/data/user/0/${packageName}/code_cache/secondary-dexes",用来存放优化后的dex文件
            File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
            // 使用MultiDexExtractor这个工具类把APK中的dex抽取到dexDir目录中,返回的files集合有可能为空,表示没有secondaryDex
            // 不强制重新加载,也就是说如果已经抽取过了,可以直接从缓存目录中拿来使用,这么做速度比较快
            List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
            if (checkValidZipFiles(files)) {
            	// 1这个是核心的加载dex的地方
                // 如果抽取的文件是有效的,就安装secondaryDex
                installSecondaryDexes(loader, dexDir, files);
            } else {
                Log.w(TAG, "Files were not valid zip files. Forcing a reload.");
                // Try again, but this time force a reload of the zip file.
                // 如果抽取出的文件是无效的,那么就强制重新加载,这么做的话速度就慢了一点,有一些IO开销
                files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
                if (checkValidZipFiles(files)) {
                    // 强制加载后,如果文件有效就安装,否则就抛出异常
                    installSecondaryDexes(loader, dexDir, files);
                } else {
                    // Second time didn't work, give up
                    throw new RuntimeException("Zip files were not valid.");
                }
            }
        }
    } catch (Exception e) {
        Log.e(TAG, "Multidex installation failure", e);
        throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
    }
    Log.i(TAG, "install done");
}

直接看我这里标的1部分,前面的主要是一些抽取dex文件列表和校验的逻辑,下面是installSecondaryDexes(loader, dexDir, files)的源码

private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)
        throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
        InvocationTargetException, NoSuchMethodException, IOException {
    if (!files.isEmpty()) {
    	// 这里根据不同SDK_VISION走了不同的方法
        if (Build.VERSION.SDK_INT >= 19) {
            V19.install(loader, files, dexDir);
        } else if (Build.VERSION.SDK_INT >= 14) {
            V14.install(loader, files, dexDir);
        } else {
            V4.install(loader, files);
        }
    }
}

开启套娃模式,再来看看V19.install的源码:

/**
 * Installer for platform versions 19.
 */
private static final class V19 {
    private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
            File optimizedDirectory)
                    throws IllegalArgumentException, IllegalAccessException,
                    NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
        /* The patched class loader is expected to be a descendant of
         * dalvik.system.BaseDexClassLoader. We modify its
         * dalvik.system.DexPathList pathList field to append additional DEX
         * file entries.
         */
        // 传递的loader是PathClassLoader,findFidld()方法是遍历loader及其父类找到pathList字段
        // 实际上就是找到BaseClassLoader中的DexPathList
        Field pathListField = findField(loader, "pathList");
        // 获取PathClassLoader绑定的DexPathList对象
        Object dexPathList = pathListField.get(loader);
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // 扩展DexPathList对象的Element数组,数组名是dexElements
        // makeDexElements()方法的作用就是调用DexPathList的makeDexElements()方法来创建dex元素
        expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
                new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
                suppressedExceptions));
        // 后面就是添加一些IO异常信息,因为调用DexPathList的makeDexElements会有一些IO操作,相应的可能就会有一些异常情况
        if (suppressedExceptions.size() > 0) {
            for (IOException e : suppressedExceptions) {
                Log.w(TAG, "Exception in makeDexElement", e);
            }
            Field suppressedExceptionsField =
                    findField(loader, "dexElementsSuppressedExceptions");
            IOException[] dexElementsSuppressedExceptions =
                    (IOException[]) suppressedExceptionsField.get(loader);
            if (dexElementsSuppressedExceptions == null) {
                dexElementsSuppressedExceptions =
                        suppressedExceptions.toArray(
                                new IOException[suppressedExceptions.size()]);
            } else {
                IOException[] combined =
                        new IOException[suppressedExceptions.size() +
                                        dexElementsSuppressedExceptions.length];
                suppressedExceptions.toArray(combined);
                System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
                        suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
                dexElementsSuppressedExceptions = combined;
            }
            suppressedExceptionsField.set(loader, dexElementsSuppressedExceptions);
        }
    }
    /**
     * A wrapper around
     * {@code private static final dalvik.system.DexPathList#makeDexElements}.
     */
    // 通过反射的方式调用DexPathList#makeDexElements()方法
    // dexPathList 就是一个DexPathList对象
    private static Object[] makeDexElements(
            Object dexPathList, ArrayList<File> files, File optimizedDirectory,
            ArrayList<IOException> suppressedExceptions)
                    throws IllegalAccessException, InvocationTargetException,
                    NoSuchMethodException {
        // 获取DexPathList的makeDexElements()方法
        Method makeDexElements =
                findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
                        ArrayList.class);
        // 调用makeDexElements()方法,根据外界传递的包含dex文件的源文件和优化后的缓存目录返回一个Element[]数组
        return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
                suppressedExceptions);
    }
}

四、MultiDex的一些问题与解决方案

1. MultiDex的性能问题

多dex在初始化时,通过刚才的代码分析我们知道,实际上在Application.attachBaseContext()中执行的MultiDex.install()方法内部会进行,包括dex读取、验证、合并、插入等一系列操作,首次启动还会做dex——>odex的转换,而默认情况下这些操作都是在UI线程进行的,且由于dex初始化非常靠前(实际上是在Activity初始化之前就完成了),所以如果分包过多实际上会被APP启动速度产生一定的负向影响。根据 Carlos Sessa的测试,启用multidex后,4.4或以下的设备,app的启动时间平均会增加15%,更严重的情况,甚至在启动时候会出现黑屏。

解决思路:

目前业界主流的解决思路主要有两种:
① 单独开启线程对MultiDex进行初始化,在通常在闪屏页开启,初始化完成后进去主页面;
② 单独开启进程并创建临时文件,初始化完成后删除临时文件并finish辅助进程,主进程会轮询临时文件是否存在,删除则进入主页面。(这个也是头条的方案,实际上头条还对MultiDex中多余的一次zip压缩进行了优化),详细方案可自行检索。

2. Multidex分包后主包方法数依旧超出65535问题

这种主要发生在一些大型APP,尤其是存在很多第三方依赖的APP,即时进行了初步分包,其主包方法数往往还是超过65535。

解决思路:

首先要明白分包的核心思路是主页面及其所依赖的类必须要在主包中,其他的可以放在子dex中启动后加载即可。使用SAX自行解析AndroidMainfest.xml,抽取出组件信息,将原始的Manifest_keep.txt内容替换掉,去除启动不需要的Activity组件,保证启动加载的类最小。
在gradle中添加multiDexExt扩展块,通过指定类名或通配符来设置必须编译在MainDex中类,在扩展块中指定的类都会被添加到maindexlist.txt文件汇中。详细可参考网易的优化思路:传送门

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-04-18 17:54:07  更:2022-04-18 17:57:52 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 21:31:59-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码