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。