Gradle核心(五):Gradle高級自定義

博客主頁html

Android Gradle 高級自定義

批量修改生成的apk文件名

Android Gradle中有不少相同的任務,這些任務的名字都是經過Build TypesProduct Flavors 動態建立和生成的。java

若是修改生成的apk文件名,就要修改Android Gradle打包的輸出。Android對象提供了3個屬性:applicationVariants 僅僅用於Android應用Gradle插件,libraryVariants 僅僅適用於Android庫Gradle插件,testVariants 以上兩種Gradle插件都適用。android

這3個屬性返回的都是DomainObjectSet對象集合,裏面元素分別是ApplicationVariantLibraryVariantTestVariant ,這3個元素都是變體(就是Android構建產物)。如:ApplicationVariant表示Baidu渠道的release包,是基於Build TypesProduct Flavors 生成的產物。segmentfault

android {
       buildTypes {
            release {
                minifyEnabled true
                signingConfig signingConfigs.release
                proguardFiles getDefaultProguardFile('proguard-android.txt')
            }

            debug {
                minifyEnabled false
            }
        }

       productFlavors {
           //發佈使用
           arm {
           }

          //開發使用
          dev {
          }
      }
       
       // variant就是生成的產物,共有armRelease,armDebug,devRelease,devDebug四個產物
       applicationVariants.all { variant ->
        variant.outputs.all { output ->
            println "applicationVariants>>>>> outputFile: ${output.outputFile}, name: ${output.outputFile.name}"
            println "applicationVariants>>>>> flavorName: ${variant.flavorName}, baseName: ${variant.baseName}, name: ${variant.name}"
            if (output.outputFile != null && output.outputFile.name.endsWith('.apk')
                    && 'debug'.equals(variant.buildType.name)) {

                def apkFile = new File(output.outputFile.getParent(),
                        "${project.name}-${variant.baseName}-${new Date().format('yyyyMMdd')}.apk")

                outputFileName = apkFile.name

                println "output apk file: >>>>>${output.outputFile}"
            }
        }
    }
  }

其中一個輸出
applicationVariants>>>>> outputFile: D:\work\yqb.com\newCode\merchantApp\app\build\outputs\apk\dev\debug\app-dev-debug.apk, name: app-dev-debug.apk
applicationVariants>>>>> flavorName: dev, baseName: dev-debug, name: devDebug
output apk file: >>>>>D:\work\yqb.com\newCode\merchantApp\app\build\outputs\apk\dev\debug\app-dev-debug-20190903.apk

動態修改版本信息

版本通常由3個部分構成:major.minor.patch,版本號.副版本號.補丁號api

原始配置方式,比較直觀。最大問題就是修改不方便安全

android {
    defaultConfig {
        applicationId "com.kerwin"
        minSdkVersion 19
        targetSdkVersion 26
        versionCode 100
        versionName "1.0.0"
   }
}

能夠分模塊方式配置,將版本號的配置單獨抽取出來,放在單獨的文件裏,供其餘build引用。Android是能夠經過apply from方式引用app

// 新建config.gradle文件
ext {
  versionCode      = 100
  versionName      = '1.0.0'
}

ext { }塊爲當前project建立擴展屬性。其餘build.gradle中引用後就可使用less

apply from: 'config.gradle'

咱們也能夠從屬性文件中動態獲取,例如建立一個config.properties屬性文件ide

// config.properties
versionCode=100
versionName='1.0.0'

而後在build.gradle文件中動態獲取函數

Properties properties = new Properties()
if (project.hasProperty("config.properties")
            && file(project.property("config.properties")).exists()) {
    properties .load(new FileInputStream(file(project.property("config.properties"))))
}

if (properties != null && properties .size() > 0) {
     String versionCode= properties ['versionCode']
     String versionName= properties ['versionName']
}

動態配置AndroidManifest文件

在構建的過程當中,動態修改AndroidManifest文件中內容。在使用友盟第三方分析統計時,要求在AndroidManifest文件中指定渠道名

<meta-data 
   android:name="UMENG_CHANNEL" 
   // ${UMENG_CHANNEL}佔位符,UMENG_CHANNEL是變量名
   android:value="${UMENG_CHANNEL}"/>

其中Channel ID要替換成不一樣渠道名,如google,baidu,miui。在構建時,根據生成的不一樣渠道包來指定不一樣的渠道名,Android Gradle提供manifestPlaceholdersManifest佔位符替換AndroidManifest文件中的內容

android {

  productFlavors {
     google {
         // 是一個屬性,Map類型。key就是在AndroidManifest文件中佔位符變量
         manifestPlaceholders.put("UMENG_CHANNEL", "google")
     }
  }
   // 也能夠迭代productFlavors批量修改
   productFlavors.all { flavor ->
        println "productFlavors>>> name: ${flavor.name}"
        manifestPlaceholders.put("UMENG_CHANNEL", flavor.name)
    }
}

自定義BuildConfig

下面是Android Gradle自動生成的

/**
 * Automatically generated file. DO NOT MODIFY
 */
package ${packageName};

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "${packageName}";
  public static final String BUILD_TYPE = "debug";
  public static final String FLAVOR = "arm";
  public static final int VERSION_CODE = 215;
  public static final String VERSION_NAME = "2.1.5";
}

還能夠自定義一些常量,動態配置。Android Gradle提供了buildConfigField(@NonNull String type, @NonNull String name, @NonNull String value)能夠自定義常量到BuildConfig中。

android {
  buildTypes {
    debug {
       buildConfigField "String", "testBuildConfig", "\"測試\""
    }
    
    release {
       buildConfigField "String", "testBuildConfig", "\"測試\""
    }
  }
}

動態添加自定義的資源

除了能夠res/values文件夾中使用xml的方式定義資源外,還能夠在Android Gradle中定義。

經過**resValue(@NonNull String type,

@NonNull String name,
        @NonNull String value)**方法,在 BuildType 和 ProjectFlavor 中都存在,能夠針對不一樣的渠道,或者不一樣的構建類型自定義特有資源。
android {
  buildTypes {
    debug {
       // 第一個參數能夠是 string、id、bool、dimen、integer、color
       resValue "string", "baidu_map_api_key", "\"1234567\""
    }
    
    release {
       resValue "string", "baidu_map_api_key", "\"76544321\""
    }
  }
}

在下圖目錄中能夠找到生成的自定義資源

Java編譯選項

能夠經過compileOptions對java源文件編碼、源文件使用的JDK版本配置

android {
    compileOptions {
        encoding Charsets.UTF_8.name()
        // Java源代碼編譯級別,格式能夠是 "1.8" 、1.8 、JavaVersion.VERSION_1_8 、VERSION_1_8
        sourceCompatibility JavaVersion.VERSION_1_8
        // 配置生成的Java字節碼的版本
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

DEX選項配置

android {
    dexOptions {
        // 配置最大堆內存
        javaMaxHeapSize "4g"
        // 函數超過65535個時,有時須要開啓jumbo模式才能夠構建成功。默認是false
        jumboMode = true
        // 配置是否預執行dex Libraries庫工程,開啓後能夠提升增量構建速度,默認是開啓的
        // 當使用dx的--multi-dex選項生成多個dex,會致使和庫工程衝突,應該關閉
        preDexLibraries true
        // Integer類型,配置運行dx命令時使用的線程數
        threadCount 4
    }
}

開啓MultiDex,突破65535方法限制

APK中包含 Dalvik Executable (DEX) 文件形式的可執行字節碼文件,這些DEX文件包含應用運行已編譯代碼。 65,535等於 64 X 1024 - 1

由於Dalvik虛擬機在執行DEX文件時,使用short類型索引DEX文件中方法,單個DEX文件中方法能夠被定義最可能是65535個,當超過就會報錯。

trouble writing output:
    Too many field references: 131000; max is 65536.
    You may try using --multi-dex option.

較低版本的編譯系統會報告一個不一樣的錯誤,但指示的是同一問題:

Conversion to Dalvik format failed:
    Unable to execute dex: method ID not in [0, 0xffff]: 65536

可採用生成多個DEX文件來解決這個問題。

在Android 5.0以後,Android使用ART運行時方式,支持從APK文件加載多個DEX文件,ART在APK安裝時執行預編譯,掃描classesN.dex文件,將多個DEX文件合併成一個.oat文件執行;在 minSdkVersion 爲 21 或者更高,不須要多dex文件支持庫。

https://source.android.google...

在Android 5.0以前,Android使用Dalvik運行,而Dalvik虛擬機限制每一個APK只能使用一個classes.dex字節碼文件,要使用必須使用Multidex庫。

配置多dex

minSdkVersion爲 21 或者 以上,只需將 multiDexEnabled 設置爲 true 就能夠。

當配置multidex後,當超過65535時生成多個dex文件,文件名爲classes.dex,classes2.dex,classesN.dex

android {
   defaultConfig {
     // 啓用multidex
     multiDexEnabled true
  }
}

若是minSdkVersion爲 21 如下(不包括21)

  1. multiDexEnabled 設置爲 true,同時還需添加多dex依賴庫
dependencies {
  // 配置multidex依賴
  implementation 'com.android.support:multidex:1.0.1'
}
https://developer.android.goo...
  1. 控制Application入口
// 1. 若是沒有自定義Application,只需在AndroidManifest文件中直接配置MultiDexApplication
<application
    android:name="android.support.multidex.MultiDexApplication" />


// 2. 若是有自定義的Application,而且是直接繼承Application的。能夠將修改成MultiDexApplication
public class MMApplication extends MultiDexApplication{ }


// 3. 若是自定義的Application已經繼承的第三方提供的Application,就不能繼承了。能夠在從新attachBaseContext方法實現
  @Override
  protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);
  }
在 MultiDex.install(this)完成以前,不要經過反射或者其餘任何代碼,不然致使ClassNotFoundException

Android 編譯工具會根據須要構建主 DEX 文件 (classes.dex) 和輔助 DEX 文件(classes2.dex 和 classes3.dex 等)。而後,編譯系統會將全部 DEX 文件打包到您的 APK 中。

後續會講解下MultiDex實現原理

多dex侷限性

  1. 若是輔助DEX文件較大,可能致使應用無響應ANR
  2. 多DEX文件配置會增長編譯處理時間,由於編譯系統須要作出決策,哪些類包含在主DEX中,哪些類包含在輔助DEX中。

可使用dex預處理縮短增量編譯時間。dex 預處理依賴於Android 5.0或以上版本中提供的 ART 格式。Android Studio2.3或以上版本會自動使用此功能。若是命令行運行Gradle編譯。須要設置minSdkVersion 21或以上啓用dex預處理。

一個開發類型dev 和一個發佈類型prod,它們具備不一樣的 minSdkVersion 值,來建立兩個應用版本

android {
        defaultConfig {
            ...
            multiDexEnabled true
            // The default minimum API level you want to support.
            minSdkVersion 15
        }
        productFlavors {
            // Includes settings you want to keep only while developing your app.
            dev {
                // Enables pre-dexing for command line builds. When using
                // Android Studio 2.3 or higher, the IDE enables pre-dexing
                // when deploying your app to a device running Android 5.0
                // (API level 21) or higher—regardless of what you set for
                // minSdkVersion.
                minSdkVersion 21
            }
            prod {
                // If you've configured the defaultConfig block for the production version of
                // your app, you can leave this block empty and Gradle uses configurations in
                // the defaultConfig block instead. You still need to include this flavor.
                // Otherwise, all variants use the "dev" flavor configurations.
            }
        }
        buildTypes {
            release {
                minifyEnabled true
                proguardFiles getDefaultProguardFile('proguard-android.txt'),
                                                     'proguard-rules.pro'
            }
        }
    }
    dependencies {
        compile 'com.android.support:multidex:1.0.3'
    }

聲明主DEX中必需的類

在構建多DEX時, 編譯工具會執行復雜的決策來肯定主DEX文件中須要的類,以便可以成功啓動。若是主DEX文件中沒有提供啓動時須要的任何類,就會奔潰出現java.lang.NoClassDefFoundError錯誤。

對於代碼依賴複雜或者自檢機制,就可能不會將這些類識別爲主DEX文件中必需類。須要使用multiDexKeepFile 或者multiDexKeepProguard 聲明主DEX文件中必需的類,在構建時若是匹配到就添加到主DEX文件中。

  1. multiDexKeepFile

建立一個名爲multidex-config.txt文件,在文件中添加須要放在主DEX的類,每行包含一個類,格式以下:

com/example/MyClass.class
com/example/MyOtherClass.class

Gradle會讀取相對於build.gradle文件路徑,multidex-config.txtbuild.gradle 文件在同一目錄中。

android {
   buildTypes {
      release {
         multiDexKeepFile file('multidex-config.txt')
      }
   }
}
  1. multiDexKeepProguard

multiDexKeepProguard中文件添加內容格式與支持 Proguard 語法相同,包含-keep選項

-keep class com.example.MyClass
  -keep class com.example.MyClassToo

  或者指定包中全部的類
  -keep class com.example.** { *; } // All classes in the com.example package
https://www.guardsquare.com/e...
android {
   buildTypes {
      release {
         multiDexKeepFile file('multidex-config.pro')
      }
   }
}

代碼和資源壓縮

爲了減少APK的大小,應該啓動壓縮來移除發佈構建中未使用的代碼和資源。

代碼壓縮經過 ProGuard 提供,ProGuard 會檢測和移除應用中未使用的類、字段、方法和屬性,包括自帶代碼庫中的未使用項。ProGuard 還可優化字節碼,移除未使用的代碼指令,以及用短名稱混淆其他的類、字段和方法。

資源壓縮經過 Gradle 的 Android 插件提供,該插件會移除應用中未使用的資源,包括代碼庫中未使用的資源。

代碼壓縮

要經過 ProGuard 啓用代碼壓縮,在 build.gradle 文件內相應的構建類型中添加 minifyEnabled true

代碼壓縮會影響構建速度,避免在調試中使用。

android {
   buildTypes {
       release {
          minifyEnabled true
          // 用於定義 ProGuard 規則,getDefaultProguardFile 從 ${Android SDK}\tools\proguard\文件夾獲取默認的ProGuard 配置
         // proguard-rules.pro文件用於添加自定義ProGuard 配置,默認文件位於模塊根目錄
          proguardFiles getDefaultProguardFile('proguard-android.txt'),
                        'proguard/proguard-rules.pro'
       }
   }
}

每次構建時,ProGuard 都會輸出下列文件:

  1. dump.txt 說明 APK 中全部類文件的內部結構
  2. mapping.txt 提供原始與混淆過的類、方法和字段名稱之間的轉換
  3. seeds.txt 列出未進行混淆的類和成員
  4. usage.txt 列出從 APK 移除的代碼
這些文件保存在 ${module-name}/build/outputs/mapping/release/ 中

自定義要保留的代碼

默認 ProGuard 配置文件 (proguard-android.txt) 足以知足須要,ProGuard 會移除全部(而且只會移除)未使用的代碼。可是,ProGuard 很難以對多狀況進行正確分析,可能會移除應用須要的代碼。舉例來講,它可能錯誤移除代碼的狀況包括:

  1. 當應用引用的類只來自 AndroidManifest.xml 文件時
  2. 當應用調用的方法來自 Java 原生接口 (JNI) 時
  3. 當應用在運行時(例如使用反射或自檢)操做代碼時

能夠強制 ProGuard 保留指定代碼,在 ProGuard 配置文件中添加一行 -keep 代碼。或者在想保留的代碼添加 @keep 註解,在類上添加 @keep 可原樣保留整個類,在方法或者字段上添加可完整保留方法/字段以及類名稱。

-keep public class * extends android.app.Activity

解碼混淆過的代碼追蹤

在 ProGuard 壓縮代碼後,代碼追蹤變得困難,由於方法名稱都混淆處理了。可是ProGuard 每次運行時都會建立一個 mapping.txt 文件,其中顯示了與混淆過的名稱對應的原始類名稱、方法名稱和字段名稱。ProGuard 將該文件保存在應用的 <module-name>/build/outputs/mapping/release/ 目錄中。

可使用Android SDK 提供的工具解碼混淆過的代碼,retrace腳本(Window上爲retrace.bat,Mac/Linux上爲retrace.sh),位於<sdk-root>/tools/proguard/目錄中

retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]

例如:
retrace.bat -verbose mapping.txt obfuscated_trace.txt

也能夠直接使用 proguardgui.bat 圖形化工具,位於<sdk-root>/tools/proguard/bin/目錄中

資源壓縮

資源壓縮只與代碼壓縮協同工做。代碼壓縮器移除全部未使用的代碼後,資源壓縮器即可肯定應用仍然使用的資源。

啓用資源壓縮,在 build.gradle 文件中將 shrinkResources 屬性設置爲 true,默認是false

android {
   buildTypes {
       release {
          shrinkResources true
          minifyEnabled true
          proguardFiles getDefaultProguardFile('proguard-android.txt'),
                        'proguard/proguard-rules.pro'
       }
   }
}
資源壓縮器目前不會移除 values/ 文件夾中定義的資源(例如字符串、尺寸、樣式和顏色)。這是由於 Android 資源打包工具 (AAPT) 不容許 Gradle 插件爲資源指定預約義版本

在開始 shrinkResources 後,打包構建時,Android Gradle自動處理未使用的資源,生成的apk就不會包含。能夠在構建輸出日誌中查看,gradlew assembleArmRelease --info | grep "unused resource"

Removed unused resources: Binary resource data reduced from 2977KB to 2879KB: Removed 3%

可是可能會誤刪有用的資源,如使用反射去引用資源文件,Android Gradle區分不出來,認爲這些資源沒有被使用。咱們可使用keep配置哪些資源不被清理。

自定義要保留的資源

若是有想要保留或捨棄的特定資源,在項目中建立一個包含 resources 標記的 XML 文件,並在 tools:keep 屬性中指定每一個要保留的資源,在 tools:discard 屬性中指定每一個要捨棄的資源。這兩個屬性都接受逗號分隔的資源名稱列表。還可使用星號字符做爲通配符。

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
    tools:discard="@layout/unused2" />

將該文件保存在項目資源中,例如,保存在 res/raw/keep.xml。構建不會將該文件打包到 APK 之中。

啓用嚴格引用檢查

正常狀況下,資源壓縮器可準確斷定系統是否使用了資源。可是,若是在代碼調用 Resources.getIdentifier(),這就表示代碼會根據動態生成的字符串查詢資源名稱。當執行這一調用時,默認狀況下資源壓縮器會採起防護性行爲,將全部具備匹配名稱格式的資源標記爲可能已使用,沒法移除。

// 會使全部帶 img_ 前綴的資源標記爲已使用
String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());

默認狀況下啓用的是安全壓縮模式,tools:shrinkMode="safe"。若是將 keep.xml 文件中 shrinkMode 設置爲 strict,也就是啓用嚴格壓縮模式,而且代碼也引用了包含動態生成字符串的資源,則必須利用 tools:keep 屬性手動保留這些資源。若是不保留,也會被清理掉。

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:shrinkMode="strict" />

移除未使用的備用資源

shrinkResources只會移除代碼未被引用的資源,不會移除不一樣設備的備用資源。好比引用的第三方庫,特別是Support Library,爲了國際化支持幾十種語言,可是有的App不用支持這麼多的語言,只需中文和英文就能夠了;好比圖片只支持xhdpi格式就能夠。

可使用 Android Gradle 插件的 resConfigs 屬性來移除您的應用不須要的備用資源文件。

android {
  defaultConfig {
     // 只會保留默認的default 和 en 資源 ,其餘的不會打包到APK中
     resConfigs "en"
  }
}

resConfigs的參數是資源限定符,包括屏幕方向(port,land),屏幕尺寸(small,normal,large,xlarge),屏幕像素密度(hdpi,xhdpi),API Level(V3,V4)等

參考文檔
https://developer.android.goo...
https://developer.android.goo...

若是個人文章對您有幫助,不妨點個贊鼓勵一下(^_^)

相關文章
相關標籤/搜索