Android Gradle 常用配置
Gradle:multiDexEnabled之DEX 方法超过64K限制和gradle编译OOM问题解决DEX
方法超过64K限制
UNEXPECTED TOP-LEVEL EXCEPTION: com.android.dex.DexIndexOverflowException: method ID not in [, 0xffff]:
at com.android.dx.merge.DexMerger$.updateIndex(DexMerger.java:)
at com.android.dx.merge.DexMerger$IdMerger.mergeSorted(DexMerger.java:)
at com.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:)
at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:)
at com.android.dx.merge.DexMerger.merge(DexMerger.java:)
at com.android.dx.command.dexer.Main.mergeLibraryDexBuffers(Main.java:)
at com.android.dx.command.dexer.Main.runMonoDex(Main.java:)
at com.android.dx.command.dexer.Main.run(Main.java:)
at com.android.dx.command.dexer.Main.main(Main.java:)
at com.android.dx.command.Main.main(Main.java:)
如果你是一个Android开发者,你至少听说过的Dalvik的蛋疼的64K方法限制。概括地说,在一个DEX文件,你可以调用很多的方法,但你只能调用它们最前面的65,536个 ,因为这是在方法调用集合中的所有的空间了。如果你的源代码和狂拽炫酷叼炸天的三方库中方法超过了这个限制
为了解决这个问题,Android开发社区有人想出了一些解决方案,比如dmarcato的这个,还有casidiablo的这个。他们都是可行的,但是需要一些比较严格的条件。
最终,Google决定提供一套官方的解决方案,在10月14日的时候发布了MultiDex 支持库,随后几周gradle在 v0.14.0版本中也支持了
使用MultiDex支持库
如果你在使用 Android Studio,这个用起来很简单。如果不是,强烈建议你迁移过来。因为Google很快就会不知处Eclipse插件和旧的基于Ant的系统构建方式。
第1步 添加依赖于你的build.gradle支持MultiDex库
dependencies {
...
compile 'com.android.support:multidex:'
... }
第2步 在buildType或productFlavor中开启multiDexEnabled
defaultConfig {
...
multiDexEnabled true
...
}
第三步 Application注册
现在,根据你的项目情况,你有3种选择:
1、 如果你没有创建自己的Application 类,在你的清单文件
AndroidManifest.xml中配置android.support.multidex.MultiDexApplication就可以了。
android:name="android.support.multidex.MultiDexApplication"
2、如果你有自己的Application类了
让它继承android.support.multidex.MultiDexApplication而不是android.app.Application
3、如果你的Application继承了其他的类,并且你不想改变或者没办法改变 按照下面的方法重写attachBaseContext()
public class MyApplication extends FooApplication { @Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}
不论你选择上面哪种,都会创建多个大小差不多的dex文件代替单个庞大的dex文件。运行的时候回同事加载所有的这些dex文件。
当年编译app的时候,Gradle会生成很多个dex文件和一个apk文件让你可以在设备或者模拟器上运行。
Out of memory 问题
对于有很多依赖的项目,编译可能因为下面的错误中断
Error:Execution failed for task ':app:dexDebug'. ...
Error Code: Output: UNEXPECTED TOP-LEVEL ERROR:
java.lang.OutOfMemoryError: GC overhead limit exceeded at com.android.dx.cf.cst.ConstantPoolParser.parse0(ConstantPoolParser.java:)
在build.gralde android标签下面添加下面代码可以解决
dexOptions {
incremental true
javaMaxHeapSize "4g"
}
应用启动缓慢
根据我们的经验,添加了这个支持库以后,大多数情况下都正常了。这对某些设备,比如Kindle Fire上面,应用启动会比之前慢很多。加载所有的类在应用一启动的时候会花费大量的时间。这就会导致黑屏一段时间,甚至导致ANR
这个虽然在大多数时候可以解决DEX 64K的问题,但是应该是保留使用。当你尝试使用它以前,请先尝试删除不需要的依赖并且使用ProGuard混淆,如果你必须要使用这个方案。请确保在旧设备上做了测试
multidex带来的性能问题-减慢app启动速度
背景
先为外行做一下科普。安卓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 file
w.append('\n')
new File("${projectDir}/multidex.keep").withReader { r ->
// And write data from the input into the output
w << 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(), );
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.classes
final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
//the total dex numbers
final int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, ); for (int secondaryNumber = ; 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() <= ) {
// no external dex
return null;
}
// the first element is the main dex, remove it.
paths.remove();
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", );
} 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方法数限制。
Android Gradle 常用配置的更多相关文章
- Gradle系列之Android Gradle高级配置
本篇文章主要在之前学习的基础上,从实际开发的角度学习如何对 Android Gradle 来进行自定义以满足不同的开发需求,下面是 Gradle 系列的几篇文章: Gradle系列之初识Gradle ...
- Android Gradle 依赖配置:implementation & api
背景: Android Gradle plugin 3.0开始(对应Gradle版本 4.1及以上),原有的依赖配置类型compile已经被废弃,开始使用implementation.api和anno ...
- 【Android Studio安装部署系列】九、Android Studio常用配置以及快捷键
版权声明:本文为HaiyuKing原创文章,转载请注明出处! 概述 整理Android Studio的常用配置和快捷键. 常用配置 显示行号 临时显示 永久显示 File——Settings——Edi ...
- Gradle系列之Android Gradle基础配置
原文发于微信公众号 jzman-blog,欢迎关注交流. 通过前面几篇文章学习了 Gradle 基础知识以及 Gradle 插件相关的知识,关于 Gradle 及其插件相关知识请先阅读下面几篇文章: ...
- Android gradle 相关配置
有时候我们需要重命名输出apk文件名,在Android studio 3.0以前我们是这样写的: applicationVariants.all { variant -> variant.out ...
- Android Studio常用配置
目录 1. 主题颜色设置 2. Logcat颜色设置 3. 类注释 4. 编译器添加背景图 4.1 第一种方式 Background Image 4.2 第二种方式 Sexy Editor 5. 修改 ...
- Android Gradle基于参数化配置实现差异化构建
一.背景: 项目中有一些特殊的需求,如个别渠道集成腾讯bugly,个别渠道集成易观统计,不同的渠道集成不同的推送策略(如Oppo渠道优先Opush推送),不同的渠道拥有不同的第三方登录集成等等.这些需 ...
- Android Gradle 学习笔记(七):Android Gradle 插件
我们知道Android Gradle其实就是一个Gradle的一个第三方插件,它是由Google的Android团队开发的,基于Gradle构建的,和Android Studio完美搭配.相比于旧的构 ...
- Gradle系列之Android Gradle插件
原文发于微信公众号 jzman-blog,欢迎关注交流. 通过前面几篇文章学习了 Gradle 基础知识以及 Gradle 插件相关的知识,关于 Gradle 及其插件相关知识请先阅读下面几篇文章: ...
随机推荐
- Jenkins学习指南
jenkinshttps://www.cnblogs.com/jimmy-xuli/p/9020825.htmlhttps://www.cnblogs.com/along21/p/10172855.h ...
- [转帖]使用Gnome文件管理器连接到服务器:FTP/SFTP、Samba、NFS的方法
使用Gnome文件管理器连接到服务器:FTP/SFTP.Samba.NFS的方法 2019-05-09 16:28:44作者:雷增线稿源:云网牛站 https://ywnz.com/linuxyffq ...
- Hadoop_常用命令(hdfs上)
Hadoop_常用命令(hdfs上) hadoop fs所有文件系统都可以使用 hdfs dfs仅针对于hdfs文件系统 - 1 - 查看所有目录(文件夹)及文件 hdfs dfs -ls / - ...
- SQLite基础-4.数据定义语言(DDL)
目录 一.创建数据库 1. 创建方式 2. 数据库命名规范 二. 创建表 1. 基本用法 2. 数据表命名规范 3. 字段命名规范 三. 删除表 一.创建数据库 1. 创建方式 在第二章中我们讲了如何 ...
- SpringBoot起飞系列-拦截器和统一错误处理(七)
一.前言 在前边部分我们已经学会了基本的web开发流程,在web开发中,我们通常会对请求做统一处理,比如未登录的用户要拦截掉相关请求,报错页面统一显示等等,这些都需要配置,可以大大简化我们的代码,实现 ...
- python商城项目总结
项目概括 本项目用于针对Django框架的练习,主要分为前端和后端两部分.前端负责用户注册.登录.商品展示以及添加购物车和显示订单列表的功能:后端负责会员信息.商品类别.商品信息的增删改查以及订单状态 ...
- Django-choices字段值对应关系(性别)-MTV与MVC科普-Ajax发json格式与文件格式数据-contentType格式-Ajax搭配sweetalert实现删除确认弹窗-自定义分页器-批量插入-07
目录 models 字段补充 choices 参数/字段(用的很多) MTV与MVC模型 科普 Ajax 发送 GET.POST 请求的几种常见方式 用 Ajax 做一个小案例 准备工作 动手用 Aj ...
- paramiko-ssh实例
import paramiko # 创建SSH对象 ssh = paramiko.SSHClient() # 允许连接不在know_hosts文件中的主机 ssh.set_missing_host_k ...
- mqtt协议实现 java服务端推送功能(一)安装
最近有个新需求,需要通过java服务端把信息推送到mqtt服务器上,安卓和ios端从mqtt服务器上获取信息实现推送. 1. 本地需要安装Mosquitto服务器 http://mosquitto. ...
- python 常见内置函数setattr、getattr、delattr、setitem、getitem、delitem
常见内置函数 内置函数:在类的内部,特定时机自动触发的函数 示例1:setattr.getattr.delattr class Person: # def __init__(self, name): ...