资讯专栏INFORMATION COLUMN

通过预安装给MultiDex加速

Lemon_95 / 2726人阅读

摘要:压缩过程会有明显的耗时,经过测试,如果不进行压缩,直接从里解压文件,则过程会有大约的加速效果。

在Android Kikat及以前的Android系统上,构建或安装Apk会出现“65535方法数超标”以及“INSTALL_FAILED_DEXOPT”问题,MultiDex是Google为了解决这个问题问题而开发的一个Support库。MultiDex出现的具体背景、使用方式可以参考给App启用 MultiDex功能,而MultiDex Support库的工作机制、源码分析可以参考MultiDex工作原理分析和优化方案。

MultiDex的使用虽然很简单便捷,但是有个比较蛋疼的问题,就是在App第一次冷启动的时候会产生明显的卡顿现象。经过测试和统计,根据Apk包的大小、Android系统版本的不同,这个卡顿时间一般是2000到5000毫秒左右,极端的情况下甚至可以到20000+毫秒。通过之前的分析,我们知道具体的卡顿产生在MultiDex解压、优化dex这两个过程,而且只在第一次冷启动的时候才会触发这两个过程。那么优化的方式也很简单,在安装Apk前先对新版本的Apk做好解压和优化工作,就能在安装后第一次冷启动的时候避开这两个耗时的过程了。

MultiDex是如何判断是否需要重新解压和优化dex的

在之前的章节里面讲到,MultiDex在第一次做完解压和优化dex之后,会保留当前Apk的一些信息,下一次启动时候后读取这些配置信息再判断是否需要重新解压和优化dex文件。

这个判断主要是在MultiDexExtractor#load(Context, ApplicationInfo, File, boolean)方法里进行。

    static List load(Context context, ApplicationInfo applicationInfo, File dexDir,
            boolean forceReload) throws IOException {
       
        try {
            ...
            if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
                try {
                    files = loadExistingExtractions(context, sourceApk, dexDir);
                } catch (IOException ioe) {
                    ...
                    files = performExtractions(sourceApk, dexDir);
                    putStoredApkInfo(context,
                            getTimeStamp(sourceApk), currentCrc, files.size() + 1);
                }
            } else {
                ...
                files = performExtractions(sourceApk, dexDir);
                putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
            }
        }
        ...
        return files;
    }

第一次调用这个方法的时候,forceReload为false,则不需要强制重新解压dex。然后调用了isModified这个方法判断当前App的Apk包是否被修改过。

    private static boolean isModified(Context context, File archive, long currentCrc) {
        SharedPreferences prefs = getMultiDexPreferences(context);
        return (prefs.getLong(KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive))
                || (prefs.getLong(KEY_CRC, NO_VALUE) != currentCrc);
    }

isModified方法主要是判断当前App的Apk包的CRC值是否和上一次解压dex时记录的Apk包CRC一样(CRC值可以认为是一个稀疏的MD5算法,它的时间复杂度低很多,但是计算结果容易产生冲突),以及Apk文件的修改时间(文件的Last Modified Time)是否一致。如果这两项都一致的话就认为Apk文件没有产生变化(没有覆盖安装过),因此上一次解压和优化dex得到的缓存文件可以复用。

当然,光Apk包没有修改过这一项条件还不够,接下来调用了这个判断主要是在MultiDexExtractor#loadExistingExtractions(Context, File, File)。

    private static List loadExistingExtractions(Context context, File sourceApk, File dexDir)
            throws IOException {
            
        final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
        int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
        final List files = new ArrayList(totalDexNumber);
        for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
            String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
            File extractedFile = new File(dexDir, fileName);
            if (extractedFile.isFile()) {
                files.add(extractedFile);
                if (!verifyZipFile(extractedFile)) {
                    throw new IOException("Invalid ZIP file.");
                }
            } else {
                throw new IOException("Missing extracted secondary dex file "" +
                        extractedFile.getPath() + """);
            }
        }
        return files;
    }

这里先通过SharePreference读取上一次MultiDex保存的Apk包的dex数量totalDexNumber,然后挨个加载预定的文件路径上的dex文件,加载文件的的同时还通过verifyZipFile方法判断dex文件的合法性。如果这个过程出现异常就认为获取上一次缓存的dex文件失败,需要重新解压。

    static boolean verifyZipFile(File file) {
        try {
            ZipFile zipFile = new ZipFile(file);
            try {
                zipFile.close();
                return true;
            } catch (IOException e) {
                Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath());
            }
        } catch (ZipException ex) {
            Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex);
        } catch (IOException ex) {
            Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex);
        }
        return false;
    }

verifyZipFile这个方法非常简单,解压dex文件的时候,解压出来的文件被保存成Zip包,这个方法这是检查缓存的dex文件是否是Zip包。感觉不靠谱,虽然检查MD5值比较耗时不适合这种情景,不过好歹也像检查Apk包的CRC值和修改时间一样,检查dex缓存文件的CRC和修改时间啊。不过读取SharePreference配置是一个IO操作,如果保存的数值太多的话,也是有增加耗时和IO异常的风险的。

到这里我们的方案就清晰了:

在安装新Apk前,先做好dex的解压和优化,得到dex压缩包(.zip)列表和dexopt后的odex文件(.dex)列表。

把dex/odex文件保存到一个内部存储路径PATH_A,同时使用SP记录新版本Apk的CRC、dex数量,以及解压出来的每一个dex的CRC值。

安装新版本Apk后,启动时在执行MultiDex前,把PATH_A路径上的缓存文件移动(rename)到MultiDex的缓存路径PATH_B上,同时保存当前Apk的CRC、修改时间以及dex数量到MultiDex对应的SP配置上。

执行原有MultiDex逻辑,让MultiDex以为之前已经做过解压和优化dex工作,从而绕开第一次MultiDex时候的耗时。

第一次成功启动新Apk后,对dex进行校验工作,如果校验失败则清除dex缓存,强制让App在下一次启动的时候再执行一遍MultiDex。

预解压(PreMultiDex)详细的流程图

注:

流程图的绿色部分为文件锁(FileLock)操作,主要是为了多进程同步。

红色部分为耗时的操作。

Dex路径为MultiDex过程中用于存储解压出来的dex文件的路径(/data/data//code_cache)。

PreDex路径为存储预解压得到的缓存文件的内部路径(/data/data//code_cache_pre)。

MultiDex从Apk包解压出来的dex文件会被压缩成Zip包(.zip),而执行dexopt操作后生成的odex文件文件名为.dex,这两个容易搞混。

安装新Apk前先解压和优化dex

这个环节必须在升级Apk前,由旧版本的Apk进行,也就是要求App拥有自主更新的逻辑。

第一次运行新Apk时,移动预先安装好的dex文件

从旧版的Apk覆盖安装新的Apk后,第一次运行App时MultiDex主要的耗时过程。这时需要把在旧版本Apk预安装得到的dex缓存文件移动到MultiDex使用的存储路径上。

第一次运行新Apk后,检查dex文件是否正确

原有的MultiDex,dex文件时同步从Apk包里解压出来的,所以不存在dex文件和Apk版本对不上的问题。而PreMultiDex的方案的一个问题ui是,解压dex文件和使用dex文件这两个过程是分开的,无论版本控制做得再精确,理论上也存在版本出错的问题(比如从A版本解压得到了dex文件,而用户却选择覆盖安装了B版本,这时候由于代码逻辑的不严谨导致B版本的Apk使用了A版本解压出来的dex文件)。如果想要确保dex文件的正确性,需要对Apk包里面的dex文件和解压出来的dex文件做一下MD5值校验,而这个过程比较耗时,不适合在App启动的时候做,不然PreMultiDex就失去了意义。因此,需要在第一次运行新Apk后,启动dex的校验工作,在Worker线程对dex进行校验,如果校验失败则清除dex缓存,强制让App在下一次启动的时候再执行一遍MultiDex。

恢复MultiDex

在MultiDex校验失败后,需要清空MultiDex的缓存文件,禁用PreMultiDex功能,并且强制让App在下一次启动的时候再执行一遍MultiDex。

一些小细节 dex文件、odex文件?

dex文件是Android虚拟机使用的可执行文件(从Java类编译得到),相当于JVM虚拟机用的class文件。但是与class文件不同,Android系统并不能直接使用dex文件,需要先使用dexopt工具对dex文件进行一次优化工作(Optimize),优化得到的odex文件才能被虚拟机加载。不同的Android设备需要不同格式的odex文件,所以这个过程只能在Android设备上进行,而不能在构建Apk的时候就处理好。

dex文件在Apk包里的文件后缀名是.dex,MultiDex从Apk包里解压出dex文件后会压缩成Zip包,文件后缀名是.zip。对dex文件进行dexopt操作后,会生成相同文件名的odex文件,后缀名是.dex,odex文件会比dex文件大许多,不要搞混这些文件。

至于为什么MultiDex解压dex文件时会进行压缩工作,可能是因为压缩后的压缩包会占用比较小的内部存储空间,因为MultiDex本来就是给旧版本的Android系统使用,一些早期的Android设备拥有的内部存储空间非常有限,而这些dex文件对于App的运行时必须的,所以才需要尽量压缩dex的体积。压缩过程会有明显的耗时,经过测试,如果不进行压缩,直接从Apk里解压dex文件,则MultiDex过程会有大约1/3的加速效果。

dexopt缓存

MultiDex其实并没有刻意保留dexopt后的缓存,如果只保留dex文件,而不保留odex文件,那么下一次启动执行MultiDex的时候,不需要重新解压dex文件,但是依然需要dexopt并产生odex文件,这个过程大概会占用MultiDex总耗时的一般左右。如果odex文件存在,但是已经损坏了,或者是一个非法的odex文件,依然会触发dexopt工作。也就是说,加载dex文件并创建DexFile对象的时候,Android系统会判断odex的缓存,以及缓存文件是否正确,具体过程在dalvik_system_DexFile.cpp里实现,有兴趣的同学可以找找dex文件结构分析的文章,这里就不挖坑了。

关于dex文件校验

其实,如果dex文件和Apk的版本对不上的话,一般在启动App的时候就会出现ClassNotFound异常而导致App崩溃,接着再次启动由于没有重新MultiDex也会继续崩溃。而崩溃的时候,可能App崩溃上报系统还没来得及初始化,所以没有办法发现崩溃的问题。

为了防止这种问题,可以开发一个恢复模式或者安全模式的功能,当App出现连续的崩溃的时候,会进入恢复模式的状态,清空一些可能导致异常的数据(比如PreMultiDex的缓存),这样就能避免App因为连续崩溃而不能使用。至于怎么实现恢复,这已经是另一个领域的功能了,这里不再展开。

参考链接:
Google Multidex

著作信息:
本文章出自 Kaede 的博客,原创文章若无特别说明,均遵循 CC BY-NC 4.0 知识共享许可协议4.0(署名-非商用-相同方式共享),可以随意摘抄转载,但必须标明署名及原地址。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/13403.html

相关文章

  • 【腾讯Bugly干货分享】Android Patch 方案与持续交付

    摘要:在安卓系统上,可以通过的思路来达到这一目的下发补丁文件,更新版本。只是一个加载器既然做安卓方案,最好的结果就是能支持更新所有的代码和资源。安卓系统的实现是包装了一层真正的上下文,真正使用到的就是这个。 本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57a31... Android 不仅系统版本众多,机型众多,而且各...

    Karuru 评论0 收藏0
  • 一触即发 App启动优化最佳实践

    摘要:一触即发启动优化最佳实践文中的很多图都是性能优化指南第六季中的一些截图给出的优化指南来镇楼闪屏定义官方的性能优化典范,从第六季开始,发起了一系列针对启动的优化实践,地址如下可想而知,的启动性能是非常重要的。是继承于并处理异步请求的一 一触即发 App启动优化最佳实践 文中的很多图都是Google性能优化指南第六季中的一些截图 Google给出的优化指南来镇楼https://develo...

    Carbs 评论0 收藏0
  • MultiDex工作原理分析和优化方案

    摘要:最后的工作在这个方法里面。在中,提取出来的文件被压缩成文件,又优化后的文件则被保存为文件。目前,优化方案能想到的有两种。 动态加载技术(插件化)系列已经坑了有一段时间了,不过UP主我并没有放弃治疗哈,相信在不就的未来就可以看到系统Api Hook模式和插件化框架Frontia的更新了。今天要讲的是动态加载技术的亲戚 —— MultiDex。他们的核心原理之一都是dex文件的加载。 Mu...

    everfly 评论0 收藏0
  • Android 在 Multidex 下使用 Instant Run

    摘要:使用多个的方式,需要先在里配置另外需要导入的依赖在进行分包可以使用两种方式让分包。第一种,使用使用作为即可。下面看下如何使用。 Instant Run是Android studio 2.0新的逆天功能,在Run和Debug的时候,只有在第一次build会花费大量的时间,之后再次Run或者Debug的时候会直接把变化的代码更新到手机,再上面生成一个新的APK直接运行,这样就能很快的显示出...

    liuhh 评论0 收藏0
  • Android DexIndexOverflow错误解析和解决方案

    摘要:错误剖析前些天进行应用开发时遇到一个错误。这个错误是应用的方法总数限制造成的。具体错误如下解决方案采用提供的分片库,解决原理是将一个文件分拆成多个文件。具体解决方案在文件中引入。当方法数量过多导致超出缓冲区大小时,会造成崩溃。 1. 错误剖析 前些天进行Android应用开发时遇到一个错误: DexIndexOverflow。后面经过努力解决,这里做个记录,也为其它遇到这个问题的人做...

    张利勇 评论0 收藏0

发表评论

0条评论

Lemon_95

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<