引言

在安卓社区中,65k方法数的限制是一个被多次提及的问题。目前解决这个问题的办法就是用multidex。虽然multidex是谷歌给出的一个非常棒的办法,但是我发现了它对app启动性能存在严重的影响,这点还没有在社区引起重视。我这篇文章的就是为那些还没有听说过这个问题(但是想使用multidexing)的开发者以及那些使用了multidexing,但是想观察本文解决办法所能能赢得性能的伙伴而写的。

背景

先为外行做一下科普。安卓app是由被转换成一个.class文件的java写成的。然后这个class文件(以及任何jar依赖)被编译成单个classes.dex文件。然后这个dex文件和一个apk文件(即最终从app商店所下载的东西)所需要的任意资源相组合。

这种编译过程的一个缺陷是一个dex文件系统只允许最多有65k个方法。在安卓的早期,达到65k方法上限的应用解决这个问题的办法就是使用Proguard来减少无用的代码。但是,这个方法有局限,并且只是为生产app拖延了接近65k限制的时间。

为了解决这个问题,谷歌在最近的兼容库中放出了一个针对65k方法限制的解决方案:multidexing。这个方法非常方便并且允许你突破65k方法限制,但是(就如我之前说的),对性能有一个非常严重的影响,可能会减慢app的启动速度。

设置multidex

multidex是一个文档齐全的成熟的解决方案。我强烈推荐遵循安卓开发者网站上的指示来启用multidex。你也可以参考我的github上的项目样例。

NoClassDefFoundError?!

在为项目配置multidexing 的时候,你可能会在运行的时候看到java.lang.NoClassDefFoundError。这意味着app启动的class不在main dex文件中。Android SDK Build Tools 21.1或者更高版本中的Gradle  Android 插件有对multidex 的支持。这个插件使用Proguard 来分析你的项目并在 [buildDir]/intermediates/multi-dex/[buildType]/maindexlist.txt文件中生成一个app启动classes 的列表。但是这个列表并不是100%准确,可能会丢失一些app启动所需的classes 。

YesClassDefFound

为了解决这个问题,你应该在multidex.keep 文件中罗列出那些class,以便让编译器知道在main dex文件中要保持哪些class。

  • 在工程目录中创建一个multidex.keep文件。
  • 把java.lang.NoClassDefFoundError中报告的class列举到multidex.keep文件。(注意: 不要直接修改build目录里的maindexlist.txt ,这个文件每次在编译的时候都会生成)。
  • 添加如下脚本到build.gradle。这个脚本将在编译项目的时候把multidex.keep 和 由Gradle生成的maindexlist.txt 结合在一起。
android.applicationVariants.all { variant ->task "fix${variant.name.capitalize()}MainDexClassList" << {logger.info "Fixing main dex keep file for $variant.name"File keepFile = new File("$buildDir/intermediates/multi-dex/$variant.buildType.name/maindexlist.txt")keepFile.withWriterAppend { w ->// Get a reader for the input filew.append('\n')new File("${projectDir}/multidex.keep").withReader { r ->// And write data from the input into the outputw << r << '\n'}logger.info "Updated main dex keep file for ${keepFile.getAbsolutePath()}\n$keepFile.text"}}
}
tasks.whenTaskAdded { task ->android.applicationVariants.all { variant ->if (task.name == "create${variant.name.capitalize()}MainDexClassList") {task.finalizedBy "fix${variant.name.capitalize()}MainDexClassList"}}
}

  

multidex app启动性能问题

如果你使用multidex,你需要意识到它对app启动性能有影响。我们通过跟踪app的启动时间发现了这个问题-用户点击app图标到所有图片都下载完并显示给用户的这段时间。一旦multidex 启用,在所有运行Kitkat (4.4) 及以下的设备上我们的app启动时间就会大约增加15%。更多信息参考 Carlos Sessa的Lazy Loading Dex files 。

这是因为Android 5.0 以及更高版本使用了一个叫做ART的运行时,它天生就支持从应用的apk文件中加载multiple dex文件。

解决multidex app启动性能问题

在app启动到所有图片显示的间隙,存在着许多没有被Proguard 检测到的class,因此它们也就没有被存进main dex文件中。现在的问题是,我们如何才能知道在app启动期间什么样的calss被加载了呢?

幸运的是,在 ClassLoader中我们有 findLoadedClass 方法。我们的办法就是在app启动结束的时候做一次运行时检查。如果第二个dex 文件中存有任何在app启动期间加载的class,那么就通过添加calss  name 到multidex.keep文件中的方式来把它们移到main dex文件中。我的 项目案例  中有实现的细节,但是你也可以这样做:

  • 在你认为app启动结束的地方运行下面util类中的getLoadedExternalDexClasses
  • 把上面这个方法返回的列表添加到你的 multidex.keep 文件然后重新编译。
public class MultiDexUtils {private static final String EXTRACTED_NAME_EXT = ".classes";private static final String EXTRACTED_SUFFIX = ".zip";private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator +"secondary-dexes";private static final String PREFS_FILE = "multidex.version";private static final String KEY_DEX_NUMBER = "dex.number";private SharedPreferences getMultiDexPreferences(Context context) {return context.getSharedPreferences(PREFS_FILE,Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB? Context.MODE_PRIVATE: Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);}/*** get all the dex path** @param context the application context* @return all the dex path* @throws PackageManager.NameNotFoundException* @throws IOException*/public List<String> getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException {final ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);final File sourceApk = new File(applicationInfo.sourceDir);final File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);final List<String> sourcePaths = new ArrayList<>();sourcePaths.add(applicationInfo.sourceDir); //add the default apk path//the prefix of extracted file, ie: test.classesfinal String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;//the total dex numbersfinal int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {//for each dex file, ie: test.classes2.zip, test.classes3.zip...final String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;final File extractedFile = new File(dexDir, fileName);if (extractedFile.isFile()) {sourcePaths.add(extractedFile.getAbsolutePath());//we ignore the verify zip part} else {throw new IOException("Missing extracted secondary dex file '" +extractedFile.getPath() + "'");}}return sourcePaths;}/*** get all the external classes name in "classes2.dex", "classes3.dex" ....** @param context the application context* @return all the classes name in the external dex* @throws PackageManager.NameNotFoundException* @throws IOException*/public List<String> getExternalDexClasses(Context context) throws PackageManager.NameNotFoundException, IOException {final List<String> paths = getSourcePaths(context);if(paths.size() <= 1) {// no external dexreturn null;}// the first element is the main dex, remove it.paths.remove(0);final List<String> classNames = new ArrayList<>();for (String path : paths) {try {DexFile dexfile = null;if (path.endsWith(EXTRACTED_SUFFIX)) {//NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"dexfile = DexFile.loadDex(path, path + ".tmp", 0);} else {dexfile = new DexFile(path);}final Enumeration<String> dexEntries = dexfile.entries();while (dexEntries.hasMoreElements()) {classNames.add(dexEntries.nextElement());}} catch (IOException e) {throw new IOException("Error at loading dex file '" +path + "'");}}return classNames;}/*** Get all loaded external classes name in "classes2.dex", "classes3.dex" ....* @param context* @return get all loaded external classes*/public List<String> getLoadedExternalDexClasses(Context context) {try {final List<String> externalDexClasses = getExternalDexClasses(context);if (externalDexClasses != null && !externalDexClasses.isEmpty()) {final ArrayList<String> classList = new ArrayList<>();final java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[]{String.class});m.setAccessible(true);final ClassLoader cl = context.getClassLoader();for (String clazz : externalDexClasses) {if (m.invoke(cl, clazz) != null) {classList.add(clazz.replaceAll("\\.", "/").replaceAll("$", ".class"));}}return classList;}} catch (Exception e) {e.printStackTrace();}return null;}
}

  

结论

这里是我们在多个设备上观察到的启动性能的提升效果。第一列(蓝色)是没有multidexing的基准app启动时间。你可以在第二列(红色)看到明显的增加,这是启用了multidex但没有其它任何额外工作的app启动时间。第三列(绿色)是开启了multidex 并且使用了我们提升方法的app启动时间。就如图中所看到的,app启动时间降到了multidex开启之前的水平,甚至更低。自己试试吧,你应该也能观察到性能的提升。

后记

仅仅因为你能并不意味着你应该。你应该把multidex看成最后的办法因为它对app启动时间存在很大影响而且要解决这个问题你需要维护额外的代码并解决奇怪的错误(比如: java.lang.NoClassDefFoundError)。一旦达到了65k方法数的限制,我们应该先避免去使用multidex以防止性能问题。我们不断的检查使用的sdk找出许多可以移除或者重构的无用代码。只有此时仍然没有办法的时候我们才考虑multidex。那时我们的代码质量也会有个质的飞跃。不要直接使用multidex,要先保持代码的干净,复用现有组建,或者重构代码来避免65k方法数限制。

转载于:https://www.cnblogs.com/yaya25001/p/5566000.html

Android的multidex带来的性能问题-减慢app启动速度, from泡在网上的日子相关推荐

  1. Android专用Log开源项目——KLog - 泡在网上的日子

    Android专用Log开源项目--KLog 泡在网上的日子 / 文 发表于2015-11-15 13:55 第4427次阅读 log 1 编辑推荐:稀土掘金,这是一个针对技术开发者的一个应用,你可以 ...

  2. android 组件的id,Android@id和@+id的区别 - 泡在网上的日子

    Android中的组件需要用一个int类型的值来表示,这个值就是组件标签中的id属性值. id属性只能接受资源类型的值,也就是必须以@开头的值,例�[email protected]/abc.@+id ...

  3. 关于Android Studio项目的Gradle构建 泡在网上的日子 / 文 发表于2016-02-16 12:16 第2500次阅读 Gradle 3 编辑推荐:稀土掘金,这是一个针对技术开发者的

    http://www.jcodecraeer.com/a/anzhuokaifa/Android_Studio/2016/0216/3969.html 编辑推荐:稀土掘金,这是一个针对技术开发者的一个 ...

  4. Android关于线程优化以及性能优化的一些建议

    线程优化 线程优化的思想是采用线程池,避免程序中存在大量的Thread,线程池可以重用内部的线程,从而避免线程的创建或销毁带来的性能开销,同时线程池还能有效的控制最大并发数,避免大量的线程因为互相抢占 ...

  5. Android 进阶——MultiDex分包与动态加载原理剖析

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

  6. 39 | 案例篇:怎么缓解 DDoS 攻击带来的性能下降问题?

    上一节,学习了 tcpdump 和 Wireshark 的使用方法,并通过几个案例,带你用这两个工具实际分析了网络的收发过程.碰到网络性能问题,不要忘记可以用 tcpdump 和 Wireshark ...

  7. android try catch并不影响性能

    今天,简单讲讲android里使用try--catch语句是否会影响性能. 我在app的代码里有一些for循环里面有try - catch语句,担心循环里一直执行try - catch语句会影响效率, ...

  8. 持续提高 Android 应用的安全性与性能

    为了提升App的安全性及性能,确保每个用户都能够获取最佳体验,Google对Android应用开发者提出了一些变更: 今天,我们想要和各位 Android 开发者简单说明一下三项变更,它们背后的原因, ...

  9. 移动互联网和Android给你带来的机会[轉]

    移动互联网和Android给你带来的机会 最近大家都知道的一件事情就是移动互联网的春天已经到老,夏天不久了,冬天先不用考虑,因为过好了夏天冬天等着过年就行了.从李开复的创新工厂完全瞄准了移动互联网的应 ...

最新文章

  1. Win2D 官方文章系列翻译 - 避免内存泄漏
  2. Springboot - -web应用开发-Servlets, Filters, listeners
  3. python pass 占位符 占位语句
  4. Java数组--获取数组中的最大值案例
  5. 软件工程概论_课堂测试
  6. 浅谈RDMA流控设计
  7. 检测日期格式是否为yyyy-MM-dd
  8. php获取当前几点,学习猿地-php 怎么获取当前几点
  9. poj 1236 Network of Schools (强连通分支缩点)
  10. Java基础知识之方法的返回值与重载
  11. 虚拟机实验Windows10备份和还原
  12. 药品生产质量管理 计算机,《药品生产质量管理规范(2010年修订)》计算机化系统附录...
  13. HTML+CSS基础课程 笔记
  14. oracle数据文件大小
  15. 本地电脑连接阿里云RDS云数据库
  16. c语言综合合计实验报告,C语言设计实验报告(第一次)
  17. 甘思咪哚,肉骨茶,Greenland
  18. Java如何将文件打包成Zip、Rar压缩包
  19. 标准正态分布函数数值表
  20. 电脑屏幕设置(亮度,防蓝光...)-台式机显示器

热门文章

  1. 阿里云sls日志系统接入
  2. Android应用市场
  3. jQuery Steps插入或移除步骤
  4. 关于区块链应用和技术的4个PPT
  5. 内网渗透----netcat工具使用
  6. java前端接收回显图片_图片上传并回显后端篇
  7. Python保护视力小程序
  8. 解决联想笔记本安装银河麒麟系统安装时只有机械硬盘,没有固态硬盘的方法
  9. matlab汽车座椅脉冲振动冲击仿真
  10. 用java实现Simsimi小黄鸡接口