Gradle plugin 3.0 & Android Studio 3.0
我们主要讲一下升级gradle plugin 3.0过程中遇到的问题与解决方案。
Gradle Plugin 3.0
1. 升级gradle plugin插件版本为3.x
buildscript {
repositories {
mavenLocal()
jcenter()
// You need to add the following repository to download the
// for new plugin.
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0-beta2'
}
}
2. 升级gradle版本为4.1
修改gradle/wrapper/gradle-wrapper.properties
中的distributionUrl
为gradle-4.1-all
。
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
3. 升级build-tools为25.0.3
The specified Android SDK Build Tools version (22.0.1) is ignored, as it is below the minimum supported version (25.0.0) for Android Gradle Plugin 3.0.0-beta2.
2. DSL Changes
1. enforceUniquePackageName被删除
android {
// enforceUniquePackageName已经被删除,需要删除。
// enforceUniquePackageName = false
}
2. consumerProguardFiles不支持fileTree
// 不支持这种写法
// consumerProguardFiles fileTree(dir: projectDir, include: 'proguard*')
// 支持这种写法
consumerProguardFiles 'proguard.pro','proguard-fresco.pro'
3. aapt2
aapt2是支持增量编译资源开发,目前不支持Robelectric
,并且在一些情况下会导致编译失败,此时可以选择关闭aapt2。
在根项目的gradle.properties文件中添加android.enableAapt2=false
,然后在终端中执行./gradlew --stop
即可,更新信息可以参考gradle-plugin-3-0-0。
。
4. not support local aar
While using this plugin with Android Studio, dependencies on local AAR files are not yet supported.
5. not support protobuf plugin
Does not currently work with the Protobuf plugin.
6. not support android-apt
- The third party android-apt plugin is no longer supported. You should switch to the built-in annotation processor support, which has been improved to handle resolving dependencies lazily.
7. support java 1.8
当前已经支持java 8的部分特性,但是使用java 8某些特性可能会导致编译失败,可以手动禁止使用java 8的特性。在根项目的gradle.properties文件中添加android.enableDesugar=false
即可,更多信息可以参考disable Java 8 language features。
4. Bug & Solution
1. 解决multidex插件错误
FAILURE: Build failed with an exception.
What went wrong:
A problem occurred configuring project ':app'.Failed to notify project evaluation listener.
Cannot invoke method doLast() on null object
No such property: multiDex for class: com.android.build.gradle.internal.transforms.DexTransform
由于目前使用的gradle插件版本的DSL已经支持了additionalParameters
,所以我们移除了自己编写的用于给dx
追加additionalParameters
参数的插件。
dexOptions {
additionalParameters = ["--minimal-main-dex"]
}
2. aop插件gradle-android-plugin-aspectjx兼容
启用aop编译时,会出现如下错误:
Unexpected scopes found in folder '/Users/program/git_proj/new_arch/test_gradle/ClientProject/WuxianClient/build/intermediates/transforms/AspectTransform/debug'. Required: PROJECT, SUB_PROJECTS, EXTERNAL_LIBRARIES. Found: EXTERNAL_LIBRARIES, PROJECT, PROJECT_LOCAL_DEPS, SUB_PROJECTS, SUB_PROJECTS_LOCAL_DEPS
在项目对应的github上面已经有人提出了类似的
issue: java.lang.RuntimeException: Unexpected scopes found in folder,但是一直没有修复。
首先,我们知道在Transform
中,getScopes()
方法的返回值表示了这个Transform
可以处理的文件类型,那么根据报错信息,我们可以知道aspect插件的getScopes()
的方法返回了错误的类型导致编译终止。
撸一眼gradle plugin 3.x版本的QualifiedContent.Scope
类,
/**
* The scope of the content.
*
* <p>
* This indicates what the content represents, so that Transforms can apply to only part(s)
* of the classes or resources that the build manipulates.
*/
enum Scope implements ScopeType {
/** Only the project content */
PROJECT(0x01),
/** Only the sub-projects. */
SUB_PROJECTS(0x04),
/** Only the external libraries */
EXTERNAL_LIBRARIES(0x10),
/** Code that is being tested by the current variant, including dependencies */
TESTED_CODE(0x20),
/** Local or remote dependencies that are provided-only */
PROVIDED_ONLY(0x40),
/**
* Only the project's local dependencies (local jars)
*
* @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}
*/
@Deprecated
PROJECT_LOCAL_DEPS(0x02),
/**
* Only the sub-projects's local dependencies (local jars).
*
* @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}
*/
@Deprecated
SUB_PROJECTS_LOCAL_DEPS(0x08);
private final int value;
Scope(int value) {
this.value = value;
}
@Override
public int getValue() {
return value;
}
}
我们可以知道PROJECT_LOCAL_DEPS
和SUB_PROJECTS_LOCAL_DEPS
这两个类型目前都属于EXTERNAL_LIBRARIES
类型,并且已经不推荐使用,所以解决方案就呼之欲出了: 在gradle plugin的不同版本返回不同的scops集合即可。
那么如何区分不同的gradle插件版本呢?
我们知道在2.x版本中PROJECT_LOCAL_DEPS
和SUB_PROJECTS_LOCAL_DEPS
都是可用的,直到3.x才被标记为了不推荐,所以,我们可以通过获取这两个枚举常量的注解来解决这个问题,具体修改代码如下:
@Override
Set<QualifiedContent.Scope> getScopes() {
def name = QualifiedContent.Scope.PROJECT_LOCAL_DEPS.name()
def deprecated = QualifiedContent.Scope.PROJECT_LOCAL_DEPS.getClass()
.getField(name).getAnnotation(Deprecated.class)
if (deprecated == null) {
println "cannot find QualifiedContent.Scope.PROJECT_LOCAL_DEPS Deprecated.class "
return ImmutableSet.<QualifiedContent.Scope> of(QualifiedContent.Scope.PROJECT
, QualifiedContent.Scope.PROJECT_LOCAL_DEPS
, QualifiedContent.Scope.EXTERNAL_LIBRARIES
, QualifiedContent.Scope.SUB_PROJECTS
, QualifiedContent.Scope.SUB_PROJECTS_LOCAL_DEPS)
} else {
println "find QualifiedContent.Scope.PROJECT_LOCAL_DEPS Deprecated.class "
return ImmutableSet.<QualifiedContent.Scope> of(QualifiedContent.Scope.PROJECT
, QualifiedContent.Scope.EXTERNAL_LIBRARIES
, QualifiedContent.Scope.SUB_PROJECTS)
}
}
由于目前我们发起的pull request仍然没有被合并,所以我们发布了com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.11.1
到公司内部的maven仓库来解决目前的问题。
3. 修复替换登录库So文件的插件
FAILURE: Build failed with an exception.
What went wrong:
Execution failed for task ': WuxianClient:transformNativeLibsWithMergeJniLibsForDebug'.
More than one file was found with OS independent path 'lib/armeabi/libcom_cc_aes_ExecV4_0_1.so'
由于升级插件之后,我们hook的替换so文件时的task名称发生了变化,从transformNative_libsWithMergeJniLibsForDebug
变成了
transformNativeLibsWithMergeJniLibsForDebug
,对此进行兼容即可。
4. Tinker 1.7.5版本插件兼容
1. Unexpected scopes found in folder '/AspectTransform/debug' Required: PROJECT, SUB_PROJECTS, EXTERNAL_LIBRARIES
Unexpected scopes found in folder '/Users/program/git_proj//new_arch/test_gradle/ClientProject/WuxianClient/build/intermediates/transforms/AspectTransform/debug'. Required: PROJECT, SUB_PROJECTS, EXTERNAL_LIBRARIES. Found: EXTERNAL_LIBRARIES, PROJECT, PROJECT_LOCAL_DEPS, SUB_PROJECTS, SUB_PROJECTS_LOCAL_DEPS
我们目前使用的tinker版本仍然是1.7.5,未使用到AuxiliaryInjectTransform
,所以这里我们直接干掉了AuxiliaryInjectTransform
,如果需要修复的话,可以使用上文aop插件的修复方案.
2. unknown property 'apkVariantData'
[exec] A problem occurred configuring project ':WuxianClient'.
[exec] > Could not get unknown property 'apkVariantData' for object of type com.android.build.gradle.internal.api.ApplicationVariantImpl.
这个是因为在2.x中的getApkVariantData()
函数在3.x中被修改成了getVariantData()
,所以ApplicationVariantImpl
类的apkVariantData
属性就不存在了;这是因为groovy会为get函数映射一个对应的属性,如getApkVariantData()函数就被映射出了apkVariantData属性。
由于在2.x与3.x的ApplicationVariantImpl
类中一直存在variantData
属性,为了同时兼容2.x和3.x,这里我们使用variant.getProperty('variantData')
来替换先前的variant.apkVariantData
写法。
3. unknown property 'resDir'
原始写法:
applyResourceTask.resDir = variantOutput.processResources.resDir
兼容2.x与3.x:
if (variantOutput.processResources.properties['resDir'] != null) {
applyResourceTask.resDir = variantOutput.processResources.resDir;
} else if (variantOutput.processResources.properties['resPackageOutputFolder'] != null){
def resPackageOutputFolder = new File("${variantOutput.processResources.resPackageOutputFolder}");
applyResourceTask.resDir = "${resPackageOutputFolder.parentFile.absolutePath}/merged/${resPackageOutputFolder.name}"
}
4. unknown property 'manifestOutputFile'
原始写法:
manifestTask.manifestPath = variantOutput.processManifest.manifestOutputFile
兼容2.x与3.x:
if (variantOutput.processManifest.properties['manifestOutputFile'] != null) {
manifestTask.manifestPath = variantOutput.processManifest.manifestOutputFile;
} else if (variantOutput.processResources.properties['manifestFile'] != null){
manifestTask.manifestPath = variantOutput.processResources.manifestFile;
}
综上,由于目前tinker已经发布了N多新版本,所以我们没有发起pull-request,而是在公司内部maven上面发布了1.7.5.1版本。
5. Packager打包插件兼容
1. Cannot invoke method doLast() on null object
在3.x版本中,本地开发开启增量编译时,不会生成transformClassesWithDexForDebug
这个task,使用前需要进行判空。
2. unknown property 'manifestOutputFile'
修复方案见Tinker热修复本节。
3. 删除walle预生成dex文件
由于walle插件与gradle plugin 3.0冲突,所以我们关闭了walle插件,然而由于先前在发布aar文件时,walle预生成了dex并存放到了assets/dexs目录中,导致本地打包生成的apk文件的assets/dexs中包含了全部的预生成dex文件,进而导致apk文件过大。
目前使用临时方案解决这个问题,在合并assets资源之前,我们删除掉了walle预先生成的dex文件,代码大致如下:
project.gradle.taskGraph.beforeTask { Task task ->
if (mergeAssetsName == task.name) {
removeWalleDexsInAssetsBeforeTask(task);
}
}
public static void removeWalleDexsInAssetsBeforeTask(Task task) {
task.inputs.getFiles().each { File file ->
println "AssetsUtils.removeWalleDexsInAssetsBeforeTask: file=${file}";
def dexsDir = new File(file, 'dexs');
if (dexsDir.exists() && dexsDir.isDirectory()
&& (file.absolutePath.contains('intermediates') || file.absolutePath.contains('.gradle'))) {
def result = dexsDir.deleteDir();
println "AssetsUtils.removeWalleDexsInAssetsBeforeTask: delete $dexsDir successed? $result";
}
}
}
最终待全部升级之后,可以通过重新发布不带dex的aar来解决这个问题。
4. com.android.dex.DexException: Library dex files are not supported in multi-dex mode
[exec] Dex: Error converting bytecode to dex:
[exec] Cause: com.android.dex.DexException: Library dex files are not supported in multi-dex mode
[exec] UNEXPECTED TOP-LEVEL EXCEPTION:
[exec] com.android.dex.DexException: Library dex files are not supported in multi-dex mode
[exec] at com.android.dx.command.dexer.Main.runMultiDex(Main.java:371)
[exec] at com.android.dx.command.dexer.Main.run(Main.java:275)
[exec] at com.android.dx.command.dexer.Main.main(Main.java:245)
[exec] at com.android.dx.command.Main.main(Main.java:106)
[exec]
[exec] org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':WuxianClient:transformClassesWithDexForRelease'.
这个问题看起来像是multidex导致的一个问题,但是在本地编译期间并未出现该问题,只有在jenkins上面编译才出现了该问题。
根据错误信息google一番,基本上能搜索到的信息都说是由于pre-dexing与multidex冲突导致该问题产生,如:StackOverflow。
(注: pre-dexing指的是为了加快增量编译,而预先将aar的java代码转换为dex的一种方法)。
通过错误信息+搜索结果,我们可以推测出,在执行转dex的task时,其输入文件中包含dex文件,那么转dex的task的输入文件是从哪里来的呢?
查看日志文件,我们可以知道在release模式下转dex的task的执行顺序大致如下,其中上一次task的输出是下一个task的输入,
-> transformClassesAndResourcesWithProguardForRelease (混淆优化java代码生成jar文件)
-> transformClassesWithMultidexlistForRelease (生成main dex list)
-> transformClassesWithDexForRelease (将jar转换为dex)
由于transformClassesWithMultidexlistForRelease
只是生成mainDexList,并未对transformClassesAndResourcesWithProguardForRelease
生成的jar文件进行任何处理,所以jar文件最终会作为transformClassesWithDexForRelease
的输入jar被dx
转换为dex文件。当dx
在对jar文件进行转换时发现输入的文件中包含dex
文件,就会抛出了上面的错误信息。
进入proguard对应的transformClassesAndResourcesWithProguardForRelease
输出文件目录/build/intermediates/transforms/proguard/release/
,打开0.jar文件可以发现在该文件中确实存在一个classes.dex文件,其目录结构大致如下:
0.jar
├── META-INF
│ └── MANIFEST.MF
├── assets
├── classes.dex
└── com
那么知道了问题所在,解决起来就很容易了,我们只需要在proguard执行完成之后,过滤到其中的已经存在的dex文件即可,但是,这个classes.dex文件属于谁?可以被删除吗?
反编译上面提到的classes.dex
文件,通过包名我们可以发现该文件属于广点通sdk,解压缩后其文件目录结构如下:
GDTUnionSDK.4.8.520.min.jar
├── META-INF
│ └── MANIFEST.MF
├── assets
│ └── gdt_plugin
│ └── gdtadv2.jar
│ └── classes.dex
└── com
进入目录打开gdtadv2.jar
,也可以验证其中确实包括classes.dex
文件,至此,我们可以确认classes.dex
文件属于广点通sdk,并且是广点通sdk的assets目录的一个资源文件。
我们知道assets资源文件编译后仍然会原封不动的合并到apk的assets目录中,所以我们猜测proguard在处理文件时将广点通assets目录的gdtadv2.jar提取出classes.dex并打包进0.jar应该是一个bug,classes.dex应该是可以安全删除的,我们可以在打包成功之后进行验证测试。
重新打包生成apk进行验证,发现apk的assets目录中并没有gdt_plugin/gdtadv2.jar文件,并没有,没有,没。。。
出现这个问题是因为我们误解了transformClassesAndResourcesWithProguardForRelease
这个task的含义,该task包含两层意思:
- transform classes with proguard
- transform resources
通过上面我们已经之后,在transform resources的时候出错了,它将assets/gdt_plugin/gdtadv2.jar文件中的classes.dex提取到了jar文件的根目录classes.dex,破坏了assets目录的结构,所以我们解决这个问题需要做两件事:
- 删除0.jar根目录的classes.dex
- 在0.jar中插入assets/gdt_plugin/gdtadv2.jar文件
代码如下:
def proguardTask = project.getTasksByName(proguardTaskName, false).getAt(0);
if (proguardTask != null) {
proguardTask.doLast {
ProguardUtils.removeDexAndJavaInProguardJar(proguardTask);
}
}
public static void removeDexAndJavaInProguardJar(Task task) {
def streamOutputFolder = new File("${task.streamOutputFolder}");
println "removeDexAndJavaInProguardJar: streamOutputFolder=${streamOutputFolder}"
def jarFiles = streamOutputFolder.listFiles(new FileFilter() {
@Override
boolean accept(File file) {
return file.name.endsWith(".jar");
}
});
println "removeDexAndJavaInProguardJar: jarFiles=${jarFiles}"
if (jarFiles != null && jarFiles.length > 0) {
File originJar = jarFiles[0];
println "removeDexAndJavaInProguardJar: originJar=${originJar}"
String originName = originJar.name;
File targetJar = new File(originJar.parentFile, "${System.currentTimeMillis()}_${originName}");
println "removeDexAndJavaInProguardJar: targetJar=${targetJar}"
ZipFile originZip = new ZipFile(originJar);
ZipOutputStream targetJarOut = new ZipOutputStream(new FileOutputStream(targetJar));
originZip.entries().each { entry ->
if (!entry.name.endsWith('.dex') && !entry.name.endsWith('.java')) {
targetJarOut.putNextEntry(entry);
targetJarOut << originZip.getInputStream(entry).bytes;
targetJarOut.closeEntry();
}
}
originZip.close()
def jarsInAssets = task.inputs.getFiles().findAll { File file ->
file.name.endsWith('.jar') && file.absolutePath.contains('mergeJavaRes') && file.absolutePath.contains('assets')
}
if (jarsInAssets != null && !jarsInAssets.isEmpty()) {
println "removeDexAndJavaInProguardJar: jarsInAssets=${jarsInAssets}";
jarsInAssets.each { File file ->
String entryName = file.name;
File parentFile = file.parentFile;
while (parentFile.name != "assets") {
entryName = parentFile.name + "/" + entryName;
parentFile = parentFile.parentFile;
}
entryName = "assets/" + entryName;
println "removeDexAndJavaInProguardJar: jarsInAssets.entryName=${entryName}"
ZipEntry zipEntry = new ZipEntry(entryName);
targetJarOut.putNextEntry(zipEntry);
targetJarOut << file.bytes;
targetJarOut.closeEntry();
}
}
targetJarOut.flush()
targetJarOut.close();
originJar.delete();
targetJar.renameTo(originJar);
println "removeDexAndJavaInProguardJar: rename ${targetJar} to ${originJar}"
}
}
这个问题到此为止算是解决了。
5. 删除java源代码
由于HouseLib/libs/mpandroidchartlibrary-2-1-6.jar
中包含了源代码和编译后的class文件,导致在jenkins上面使用gradle plugin 3.0生成的apk中同样也包含了java源代码,目前我们在proguard生成的jar中过滤掉了java源文件(见上面的代码),后续希望大家在引入依赖时不要将源代码也一并引入打包进jar中。
6. 兼容apk路径改变
3.x与2.x生成的apk路径和名称都发生了变化,为了避免ant脚本进行修改,这个我们直接在插件中将生成的apk复制到了旧的路径。
2.x apk路径
build/outputs/apk/WuxianClient-debug.apk
3.x apk路径
build/outputs/apk/debug/WuxianClient-armeabi-debug.apk
在3.x版本中与apk同级目录中存在output.json,这个json文件描述了本次打包生成的apk的信息,虽然现在我们的so库都是armeabi类型,为了兼容其他情况,我们在获取apk的文件名称的时候仍然从output.json中获取,output.json文件内容如下:
[
{
"outputType": {
"type": "APK"
},
"apkInfo": {
"type": "FULL_SPLIT",
"splits": [
{
"filterType": "ABI",
"value": "armeabi"
}
],
"versionCode": 71500
},
"path": "WuxianClient-armeabi-debug.apk",
"properties": {
"packageId": "com.cc",
"split": "",
"minSdkVersion": "16"
}
}
]
获取apk以及复制apk到旧路径的代码大致如下:
public static File getApk(File rootDir, ProjectConfig config, boolean needCopy = false) {
File apkFile = null;
if (config.flavor.isEmpty()) {
apkFile = new File("${rootDir}/${config.name}/build/outputs/apk/${config.name}-${config.buildType}.apk");
} else {
apkFile = new File("${rootDir}/${config.name}/build/outputs/apk/${config.name}-${config.flavor}-${config.buildType}.apk");
}
if (!apkFile.exists()) {
File apkDir = new File("${rootDir}/${config.name}/build/outputs/apk/${config.buildType}/");
File outputJson = new File(apkDir, "output.json");
String apkName = new JsonSlurper().parse(outputJson)[0].path;
File realApkFile = new File(apkDir, apkName);
if (needCopy) {
java.nio.file.Files.copy(
realApkFile.toPath(),
apkFile.toPath(),
java.nio.file.StandardCopyOption.COPY_ATTRIBUTES,
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
println "getApk: copy from=${realApkFile} to=${apkFile}";
}
apkFile = realApkFile;
}
println "getApk: apkFile=${apkFile}"
return apkFile;
}
6. AAPT2 link failed: style attribute @android:attr/windowExitAnimation not found
这是因为在属性名前面多了@字符导致,删除@符号即可,如ccBasicBusinessLib库的styles.xml文件就有这个错误:
<!-- 导致编译出错的xml属性 -->
<style name="coin_flow_dialog_out" parent="android:Animation">
<item name="@android:windowExitAnimation">@anim/task_center_coin_increase</item>
</style>
<!-- 修改后的xml属性 -->
<style name="coin_flow_dialog_out" parent="android:Animation">
<item name="android:windowExitAnimation">@anim/task_center_coin_increase</item>
</style>
Android Studio 3.0
使用Android Studio 3.0 Beta 2之后,编译7.15.0分支代码,直接在android studio的terminal中可以编译成功,但是打开java类时,大部分引用都会飘红。
- 使用rebuild/sync等时,可能出现类似下面这样的错误:
Error:Argument for @NotNull parameter 'key' of com/android/tools/idea/gradle/project/model/ide/android/ModelCache.computeIfAbsent must not be null
- 在terminal显示/输入中文均存在问题Terminal in AndroidStudio can't show chinese!。
- 无法输入中文,输入后显示为
<00e3><0080><0082><00e3><0080><0082>
- 无法显示中文,中文会显示为“???”(如使用ls显示中文名称文件)。
-rw-r--r-- 1 staff 0B Aug 22 14:44 chinese.??????.test
- gradle文件中大写的字母P不显示cannot show uppercase character P in *.gradle files.
- 目前android studio自己使用gradle wrapper时可能会报错,错误信息如下(但是在android studio自己的terminal中使用wrapper确实正常的):
Error:Failed to open zip file.
Gradle's dependency cache may be corrupt (this sometimes occurs after a network connection timeout.)
<a href="syncProject">Re-download dependencies and sync project (requires network)</a>
<a href="syncProject">Re-download dependencies and sync project (requires network)</a>
解决方案就是自己在android studio中指定gradle的路径即可。
开发期间快速编译
开发期间为了加快编译速度默认值设置为:minSdkVersion=21,preDexLibraries=true,android.enableAapt2=true。