gradle知識點總結分享

本文原作者為:kale2010 .blog地址:http://www.cnblogs.com/tianzhijiexian/
微博:https://weibo.com/shark0017

Gradle技巧


全局配置

Android工程的每個module都有一個自己私有的build.gradle(綠色部分),而整個項目的根目錄中也有一個build.gradle(灰色部分),我們這里談論的全局配置基本都是在根build.gradle中進行的。

image_1bp80no6ubmgo4itssrg7kfam.png-117.1kB
image_1bp80no6ubmgo4itssrg7kfam.png-117.1kB

設定UTF-8

一個項目的根目錄的build.gradle決定了項目的全局配置,對于編碼這種所有module的通用配置自然就是在這里定義的:

allprojects {
    repositories {
        jcenter()
        mavenCentral()
    }

    tasks.withType(JavaCompile){
        options.encoding = "UTF-8"
    }
}

題外話:

UTF-8是Unicode的實現方式之一,IDE默認的編碼也是UTF-8。

支持Google倉庫

buildscript {
    /**
     * The repositories {} block configures the repositories Gradle uses to
     * search or download the dependencies. Gradle pre-configures support for remote
     * repositories such as JCenter, Maven Central, and Ivy. You can also use local
     * repositories or define your own remote repositories. The code below defines
     * JCenter as the repository Gradle should use to look for its dependencies.
     */
    repositories {
        // ...
        google()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0'
    }
}

/**
 * The allprojects {} block is where you configure the repositories and
 * dependencies used by all modules in your project, such as third-party plugins
 * or libraries. Dependencies that are not required by all the modules in the
 * project should be configured in module-level build.gradle files. For new
 * projects, Android Studio configures JCenter as the default repository, but it
 * does not configure any dependencies.
 */
allprojects {
    repositories {
        google()
        jcenter()
    }
}

我們可以在項目根目錄中的build.gradle給單個項目或全部工程啟用google的倉庫,配置后我們就可以讓其自動下載最新的Android plugin了,再也無需我們手動干預。

image_1bp7vae1vc1sbosedr1vaf1i209.png-18kB
image_1bp7vae1vc1sbosedr1vaf1i209.png-18kB

目前所有的support包都是通過google倉庫進行遠程依賴,如果不配置倉庫的依賴就必然會出現support庫依賴異常:

Could not find com.android.support:appcompat-v7:25.4.0.

如果我們想要看下google這個倉庫的地址,可以打印一下它的url:

buildscript {
    repositories {
        google()
        jcenter()
        mavenCentral()
    }
    
    repositories.each {
        println it.getUrl() // 輸出url
    }
}

輸出:

file:/D:/android-studio-ide-171.4195411-windows/android-studio/gradle/m2repository/
https://dl.google.com/dl/android/maven2/
https://jcenter.bintray.com/
https://repo1.maven.org/maven2/

支持Groovy

在根目錄的build.gradle中:

allprojects {
    // ...
}

apply plugin: 'groovy'

dependencies {
    compile localGroovy()
}

這個是可選配置,配置后可以減少一些Groovy的warnning。但如果你的項目中用到了自己寫的Gradle插件,那么添加apply plugin: 'groovy'就是必須的了。

配置Java版本

如果工程中的大多數module都是支持到Java7,那么可以在根目錄中的build.gradle中配置最低Java版本:

allprojects {
    repositories {
        jcenter()
    }
    tasks.withType(JavaCompile) {
        sourceCompatibility = JavaVersion.VERSION_1_7
        targetCompatibility = JavaVersion.VERSION_1_7
    }
}

對于某個支持到Java8的module,當然可以在它里面的build.gradle配置Java8的支持:

android {
    // ...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

配置好后可以通過項目的圖形界面進行查看:

image_1bp8s6d521v9gk2u10gcfq31qnu3q.png-46.3kB
image_1bp8s6d521v9gk2u10gcfq31qnu3q.png-46.3kB

如果你的項目比較老,可以考慮使用Gradle Retrolambda Plugin來引用Java8的語法。

定義全局變量

當我們在開發多module工程的時候,最麻煩的就是為每個module管理targetSdkVersion。老的module的minSdkVersion很低,新的module因為是Android Studio自動建立的,經常會把targetSdkVersion升級到最新。我們十分希望全部的module的targetSdkVersion都能進行統一的管理,這時我們就可以考慮定義全局變量了。

寫法一

在project根目錄下的build.gradle定義全局變量:

ext {
    minSdkVersion = 16
    targetSdkVersion = 24
}
buildscript {
    // ...
}

然后在各module的build.gradle中可以通過rootProject.ext來引用:

android {
    defaultConfig {
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
    }
}

這里添加rootProject.ext是因為這個變量定義在根build.gradle中的,如果是在私有build.gradle文件中定義的話就不用加了。

寫法二

最新的Android項目都用到了kotlin的支持庫,我們可以通過另一種方式來將kotlin的版本變為全局變量。

在根build.gradle中:

buildscript {
    ext.kotlin_version = '1.1.3-2'
    ext.support_version = '25.4.0'
    ext {
        age = 31
    }
}

在module進行依賴的時候:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation "com.android.support:appcompat-v7:$support_version"
}

只有在雙引號包裹的GString中我們才能用$,如果你想用單引號的話,可以用字符串拼接的方式來做:

implementation 'com.android.support:appcompat-v7:' + rootProject.ext.support_version

寫法三

除了在build.gradle中定義全局變量外,我們還可以在gradle.properties中定義變量:

# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true

isFusion = false

使用時:

if (!isFusion.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

在組件化的模式中,我們每個業務部門都是一個獨立的項目,所以在獨自開發的時候我們的項目都是一個獨立的App;在需要整體打包測試的時候就需要每個部門的項目變成library了。通過上述的代碼,我們可以很輕易的用配置文件的方式進行環境切換,更方便進行CI的配置化處理。

操控Task

更改輸出的APK的名字

在開發的過程中,Android Studio默認會生成app開頭的apk。但如果我們有多個團隊,打包機器肯定肯定會要求多個團隊的apk有明顯的名稱區分,所以我們需要在輸出的時候讓apk的名稱有意義:

static def releaseTime() {
    return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
}

android {
    buildTypes {
       // ...
    }
    
    android.applicationVariants.all { variant ->
        variant.outputs.all { output ->
            def outputFile = output.outputFile
            if (outputFile != null && outputFile.name.endsWith('.apk')) {
                def fileName = outputFile.name.replace("app", 
                        "${defaultConfig.applicationId}_${defaultConfig.versionName}_${releaseTime()}")
                outputFileName = fileName
    
            }
    
        }
    }
}

上面的task可以將我們apk以“包名+版本名+生產時間”的方式進行命名:

image_1bp8f00691rmc1q9len5snt19n913.png-15.1kB
image_1bp8f00691rmc1q9len5snt19n913.png-15.1kB

更改AAR的輸出的位置

在插件項目的開發過程中,在調試模式時輸出的是apk,在插件模式中時aar。宿主App會在運行時自動加載SD卡根目錄中的aar,將其當作一個資源來進行管理。

在Android Studio中我們的aar都是輸出在outputs目錄(最新的Android Studio的輸出路徑可能變更)下的,每次輸出后都扔到手機里很麻煩。我們可以通過copy命令將輸出的文件復制到想要的路徑中,節約人力成本。

android.libraryVariants.all { variant ->
    variant.outputs.all { output ->
        if (output.outputFile != null
                && output.outputFile.name.endsWith('.aar')
                && output.outputFile.size() != 0) {

            copy {
                from output.outputFile
                into "${rootDir}/libs/"
            }
        }
    }
}

這里的${rootDir}關鍵字是項目的跟路徑。通過這個路徑變量,我們可以屏蔽不同開發者電腦路徑不同產生的差異性。

跳過AndroidTest

我們在項目構建的過程中會有很多的task,有些是Android默認的,有些是我們自定義的。在很多時候我們并不需要運行test相關的Task,我們可以通過task.enable來強制跳過它。

tasks.whenTaskAdded { task ->
    if (task.name.contains('AndroidTest')) {
        task.enabled = false
    }
}

之前:

image_1bp8i68921q101o70pk929a12221g.png-162.8kB
image_1bp8i68921q101o70pk929a12221g.png-162.8kB

之后:

image_1bp8iah1jv618c53561vu310di3d.png-174.5kB
image_1bp8iah1jv618c53561vu310di3d.png-174.5kB

每一個Task都有inputs和outputs,如果在執行一個Task時,如果它的輸入和輸出與前一次執行時沒有發生變化(通過快照來判斷),那么Gradle便會認為該Task是沒變的,Gradle將不予執行,這就是所謂的增量構建。為了更好的說明這點,我們可以定義一個查看輸出/輸出詳細信息的Task:

gradle.taskGraph.afterTask { task ->
    StringBuffer taskDetails = new StringBuffer()
    taskDetails << """"-------------\nname:$task.name"""
    taskDetails << "\nInputs:\n"
    task.inputs.files.each{ inp ->
        taskDetails << " ${inp}\n"
    }
    taskDetails << "Outputs:\n"
    task.outputs.files.each{ out ->
        taskDetails << " ${out.absolutePath}\n"
    }
    println taskDetails
}

示例:

-------------
:lib:compileDebugRenderscript UP-TO-DATE
"-------------
name:compileDebugRenderscript
Inputs:
 D:\studio\kaleExample\lib\src\main\rs
 D:\studio\kaleExample\lib\src\debug\rs
Outputs:
 D:\studio\kaleExample\lib\build\intermediates\rs\debug\lib
 D:\studio\kaleExample\lib\build\intermediates\rs\debug\obj
 D:\studio\kaleExample\lib\build\generated\res\rs\debug
 D:\studio\kaleExample\lib\build\generated\source\rs\debug

順便一提,一個任務如果沒有定義輸出的話, 那么Gradle永遠都沒用辦法判斷是UP-TO-DATE。

抽離Task腳本

一個成熟的項目必然有著龐大的app.gradle,腳本多了自然就想要抽離出去。我們可以建立一個xxx.gradle來存放這些腳本,引用者只需要通過apply from依賴即可。

taskcode.gradle:

buildscript {
    repositories {
        jcenter()
    }
}

ext.autoVersionName = { ->
    def branch = new ByteArrayOutputStream()
    exec {
        commandLine 'git', 'rev-parse', '--abbrev-ref', 'HEAD'
        standardOutput = branch
    }
    def cmd = 'git describe --tags'
    def version = cmd.execute().text.trim()

    return branch.toString().trim() == "master" ? version :
            version.split('-')[0] + '-' + branch.toString().trim() // v1.0.1-dev
}


ext.autoVersionCode = {
    def cmd = 'git tag --list'
    def code = cmd.execute().text.trim()
    return code.toString().split("\n").size()
}


tasks.whenTaskAdded { task ->
    if (task.name.contains('AndroidTest')) {
        task.enabled = false
    }
}


android {
    applicationVariants.all { variant ->
        variant.assemble.doLast {
            //If this is a 'release' build, reveal the compiled apk in finder/explorer
            if (variant.buildType.name.contains('release')) {
                def path = null
                variant.outputs.each { output ->
                    path = output.outputFile
                }
                if (path != null) {
                    if (System.properties['os.name'].toLowerCase().contains('mac os x')) {
                        ['open', '-R', path].execute()
                    } else if (System.properties['os.name'].toLowerCase().contains('windows')) {
                        ['explorer', '/select,', path].execute()
                    }
                }
            }
        }
    }
}

build.gradle:

apply plugin: 'com.android.application'
// ...
apply from: 'taskcode.gradle'

這樣配置后,上面的的taskcode.gradle中的腳本就能和之前一樣引用進來了,十分方便。

動態化

動態設置BuildConfig

在測試開發階段,開發人員并不會修改老的版本號,每次打包提測給不同的測試人員的時候就會遇到不知道當前是在什么節點上的包。這種問題十分難查,有時候是開發人員自己失誤沒有merge,有時候是測試人員失誤打錯包了。

為了解決這個問題,我們可以在App的詳情頁面增加一個commit值,讓測試和開發人員可以迅速定位當前包的節點。

image_1bpauii7v1c6hgv11oaj15immnh4k.png-39.2kB
image_1bpauii7v1c6hgv11oaj15immnh4k.png-39.2kB

項目中的BuildConfig文件隨著編譯環境的不同BuildConfig的內容也是不同的,所以我們可以利用它來做一些事情。

/**
 * Automatically generated file. DO NOT MODIFY
 */
package com.example.kale;

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.example.kale";
  public static final String BUILD_TYPE = "debug";
  public static final String FLAVOR = "dev";
  public static final int VERSION_CODE = 1;
  public static final String VERSION_NAME = "1.0";
}

我們可以看到這里有構建的Type和Flavor,VersionCode和VersionName也是可以直接拿到。Gradle提供了一個buildConfigField的DSL,在編譯的時候我們可以直接設置其中的一些參數,從而在項目中利用這些參數進行邏輯判斷。

android {
   defaultConfig {
        // String中的引號記得加轉義符
        buildConfigField 'String', 'API_URL', '"http://www.kale.com/api"'
        buildConfigField "boolean", "IS_FOR_TEST", "true"
        buildConfigField "String" , "LAST_COMMIT" , "\""+ revision() + "\""
        
        resValue "string", "build_host", hostName()
    }
}

def hostName() {
    return System.getProperty("user.name") + "@" + InetAddress.localHost.hostName
}

def revision() {
    def code = new ByteArrayOutputStream()
    exec {
        // 執行:git rev-parse --short HEAD
        commandLine 'git', 'rev-parse', '--short', 'HEAD'
        standardOutput = code
    }
    return code.toString().substring(0, code.size() - 1) // 去掉最后的\n
}

BuildConfig的參數會變成靜態變量:

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.example.kale";
  public static final String BUILD_TYPE = "debug";
  public static final String FLAVOR = "dev";
  public static final int VERSION_CODE = 1;
  public static final String VERSION_NAME = "1.0";
  
  // Fields from default config.
  public static final String API_URL = "http://www.kale.com/api";
  public static final boolean IS_FOR_TEST = true;
  public static final String LAST_COMMIT = "2b07344";
}

res生成的參數會在build/generated/res/resValue/.../generated.xml中看到,在代碼中可以通過getString來拿到:

image_1bpau5ijj64rdr217n11mf91ii047.png-25.4kB
image_1bpau5ijj64rdr217n11mf91ii047.png-25.4kB

現在我們可以通過LAST_COMMIT來拿到最近的commit的SHA,并且在有多個測試Flavor的時候,可以通過IS_FOR_TEST來判斷了。

填充Manifest中的值

我們在開發第三方庫的時候可能需要根據引用者的不同來定義Manifest中的值,但是Manifest本身就是一個寫死的xml文件,并非擁有Java類那種靈活性。比如我開發一個了第三方登錄分享的庫(ShareLoginLib),這個庫的Manifest中必須配置一個騰訊的id,但是這個id肯定是根據使用的app來定義的。這就是變和不變的矛盾,為了解決這個問題,我希望將變化的部分抽離出去,讓Mainfest中的變化元素變成變量。

[代碼地址]

<!-- 騰訊的認證activity -->
<activity
    android:name="com.tencent.tauth.AuthActivity"
    android:launchMode="singleTask"
    android:noHistory="true"
    >
    <intent-filter>
        <!-- 僅僅是用來占位的key -->
        <data android:scheme="${tencentAuthId}" />
    </intent-filter>
</activity>

我們用${tencentAuthId}來做占位,使用者在編譯的時候會動態設置tencentAuthId這個的值:

[代碼地址]

defaultConfig {
    manifestPlaceholders = [
            "tencentAuthId": "tencent123456",
    ]
}

如果你想要一次性填充某些或者所有Flavor的Apk中的Manifest,使用遍歷是最快速的方案:

android {
    // ...
    productFlavors {
        google {
        }
        baidu {
        }
    }
    productFlavors.all { flavor ->
        manifestPlaceholders.put("UMENG_CHANNEL",name)
    }
}

通過這種方式我們就可以把所有的靈活改變的東西都變為動態配置,讓本身很死板的xml文件具有動態化的特性。

讓BuildType支持繼承

一個復雜項目的buildType是很多的。如果我們想要新增加一個buildType,又想要新的buildType繼承之前配置好的參數,那么用init.with()就很適合了:

buildTypes {
    release {
        zipAlignEnabled true
        minifyEnabled true
        shrinkResources true // 是否去除無效的資源文件
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        signingConfig signingConfigs.release
    }
    
    rtm.initWith(buildTypes.release) // 繼承release的配置
    rtm {
        zipAlignEnabled false // 覆蓋release中的一些配置
    }
}

讓Flavor支持繼承

在開發的過程中,我們有開發版本、提測版本、內部測試版本、預發版本、正式版本等多個版本。為了標識這些版本,我們就需要用到Flavor來區分了。

Flavor也是一個多維度的,可以類比為中國-上海-黃埔,變為渠道就是:

flavorDimensions "china", "shanghai", "huangpu" // 按照先后進行排序

每個Flavor可以定義自己屬于的維度:

flavorDimensions "china", "shanghai", "huangpu"

productFlavors {
    country {
        dimension "china"
    }
    city {
        dimension "shanghai"
    }
    town {
        dimension "huangpu"
    }
image_1bpb67drbrpg4e3m6f1hulcvk51.png-18.8kB
image_1bpb67drbrpg4e3m6f1hulcvk51.png-18.8kB

說的實際一點:

[是否免費]+[渠道]+[針對用戶]+[Debug/Release]

flavorDimensions("isfree", "channel", "sex")

productFlavors {
    // 是否免費的維度
    free { dimension "isfree" }
    paid { dimension "isfree" }

    // 渠道維度
    googleplay { dimension "channel" }
    wandoujia { dimension "channel" }

    // 用戶維度
    male { dimension "sex" }
    female { dimension "sex" }
}
image_1bpb9n0mlnn91hu718gc80ubbp8m.png-38.3kB
image_1bpb9n0mlnn91hu718gc80ubbp8m.png-38.3kB

這其實就是間接實現了Flavor的繼承,有了這種維度的幫助,我們可以實現父Flavor做通用配置,子Flavor做差異化配置。比方說我們有多個內部測試渠道,但對于開發者來說內部測試的代碼都是幾乎一樣的,所以只需要一個IS_FOR_TEST的變量來標識:

forJackTest {
    buildConfigField "boolean", "IS_FOR_TEST", "true"

}
forTonyTest {
    buildConfigField "boolean", "IS_FOR_TEST", "true"

}
forSamTest {
    buildConfigField "boolean", "IS_FOR_TEST", "true"
}

這種重復寫多次的變量肯定是有更優解的,而dimension就給了我們一個優雅的處理方式:

flavorDimensions("innertest", "channel")

productFlavors {
    innertest{
        dimension "innertest"
        buildConfigField "boolean", "IS_FOR_TEST", "true"
    }
    forJackTest {
        dimension "channel"
    }
    forTonyTest {
        dimension "channel"
    }
    forSamTest {
        dimension "channel"
    }
}
image_1bpfi2buhvi71g28sdcmps1eod9.png-26.7kB
image_1bpfi2buhvi71g28sdcmps1eod9.png-26.7kB

在Java代碼中我們可以很容易的進行這種二維的判斷:

if (BuildConfig.IS_FOR_TEST) {
    switch (BuildConfig.FLAVOR_channel) {
        case "forJackTest":
            break;

        case "forTonyTest":
            break;

        case "forSamTest":
            break;
    }
}

題外話:

FLAVOR_channel是系統自動生成的,FLAVOR_channel這種大小寫混合的寫法特別奇怪,但官方推薦的flavorDimensions中定義的都是小寫字母,所以這點可以暫時不用管它。

測試App有獨特Icon

測試人員的手機經常被借來借去,他們很難知道當前手機上的包是否是他們想要的版本。為了方便測試人員區分包和避免測錯包的情況,我們希望開發版本和測試版本的圖標和app的名字是不同的,這樣一眼就可以分辨出是正式包還是測試包了。

image_1bpb8ners1nov10ai108c1nlrg287s.png-252.1kB
image_1bpb8ners1nov10ai108c1nlrg287s.png-252.1kB

不同的Flavor在目錄結構中映射不同的文件夾。我們可以在src中建立以Flavor命名的包,然后在里面做一些某個Flavor私有的操作.

image_1bpfjitgi1vap12fl1kb6ihm1o6f1g.png-33.2kB
image_1bpfjitgi1vap12fl1kb6ihm1o6f1g.png-33.2kB

為了區別于正式包的Icon,我們得給InnerTest建立自己私有的啟動Icon:

image_1bpfjcjga5bi14cs11ar17cm9ci13.png-89.2kB
image_1bpfjcjga5bi14cs11ar17cm9ci13.png-89.2kB

在FemaleApplication這個類推薦繼承自原始App的Application類,它只需要做自己的差異化工作就好:

package com.example.kale;

public class InnerTestApplication extends MyApplication {

    @Override
    public void onCreate() {
        super.onCreate();
        // do something for test
        
        // 差異化工作,比如Debug功能
        Stetho.initialize(
                Stetho.newInitializerBuilder(this)
                        .enableDumpapp(Stetho.defaultDumperPluginsProvider(this))
                        .enableWebKitInspector(
                                Stetho.defaultInspectorModulesProvider(this)).build());
        HttpHelper.getInstance().openDebugMode();
    }
}

然后,在Manifest中我們可以進行需要項目的替換:

<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.example.kale"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    >

    <application
        android:name=".GooglePlayApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="GooglePlay-Example"

        tools:replace="android:name,android:icon,android:label"
        />

</manifest>

很多時候我們可以把那些不必打包到正式版本中的類定義在InnerTest的src下,這樣測試環境可以引用而且有時候還可以省去了no-op的依賴。這種方法可玩性很大,大家可以多多思考把玩。

不同渠道不同包名

flavorDimensions("innertest", "channel")

productFlavors {
    innertest {
        dimension "innertest"
        applicationIdSuffix '.test' // 包名后綴
        buildConfigField "boolean", "IS_FOR_TEST", "true"
    }
    dev {
        dimension "channel"
        applicationIdSuffix '.dev' // 包名后綴
        minSdkVersion 21 // 設置某個Flavor的minSdkVersion
        // ...
        versionNameSuffix "-minApi21" // 版本名后綴

    }
}

以dev渠道為例,通過applicationIdSuffix可以給原始包名增加后綴,通過versionNameSuffix可以給原始版本名字增加后綴。

最終得到:

名稱 內容 來源
基礎包名 com.example.kale applicationId的值
包名 com.example.kale.test.dev test來自innertest,dev來自channel
版本名 1.0-minApi21 -minApi21來自dev

不同包名不同的簽名就決定了不同的App,通過這種方式我們可以讓手機上同時安裝測試版本和正式版本。因為后綴會根據Flavor的維度層層添加,所以我們甚至可以把基本包名定為com或org,然后根據輸出的方案拼接包名,大大增加了打包的靈活性。

對于某些不想打包的Flavor或者維度,我們可以利用variantFilter進行操作,下面的代碼會將“minApi21”和“demo”的類型直接跳過:

android {

 buildTypes {...}

 flavorDimensions "api", "mode"
 productFlavors {
    demo {...}
    full {...}
    minApi24 {...}
    minApi23 {...}
    minApi21 {...}
  }

  variantFilter { variant ->
    def names = variant.flavors*.name
    // To check for a build type instead, use variant.buildType.name == "buildType"
    if (names.contains("minApi21") && names.contains("demo")) {
      // Gradle ignores any variants that satisfy the conditions above.
      setIgnore(true)
    }
  }
}

自動升級版本號

自動填寫versionName

我們知道build.gradle中管理了versionCode和versionName:

android {  
    // ...
    defaultConfig {
        // ...
        versionCode 1
        versionName "1.0"
    }
}

versionName和git的tag是有相關性的,我們希望可以將每次的tag和當前的versionName進行關聯,實現自動化設置版本名稱的功能。

image_1bpi867g2oie1t21bl71vi8qbam.png-8.8kB
image_1bpi867g2oie1t21bl71vi8qbam.png-8.8kB

我們執行git describe --tag就可以看到最近的一次tag,所以靠這個就可以實現自動版本名了:

def autoVersionName() {
    def cmd = 'git describe --tags'  
    def version = cmd.execute().text.trim()
    return version.toString()
}

如果不是在master分支,那么得到的結果就可能是:v1.0.1-1-g0cb4465,所以我們可以處理一下:

def autoVersionName() {
    def branch = new ByteArrayOutputStream()
    exec {
        commandLine 'git', 'rev-parse', '--abbrev-ref', 'HEAD'
        standardOutput = branch
    }
    def cmd = 'git describe --tags'
    def version = cmd.execute().text.trim()
    
    return branch.toString().trim() == "master" ? version : 
        version.split('-')[0] + '-' + branch.toString().trim() // v1.0.1-dev
}

題外話:

上面是tag和verionName完全相同時的例子,如果你的tag和versionName不同,你可以在autoVersionName()利用String的Api對原始的tag進行處理,處理后返回即可。

實現versionCode自增

有了通過tag來映射versionName的經驗后,我們可以考慮通過tag數量來映射versionCode。每一次發版我們就會打一個tag,tag的數量也會增加1個,和我們版本號的遞增邏輯是符合的。

def autoVersionCode() {
    def cmd = 'git tag --list'  
    def code = cmd.execute().text.trim()
    return code.toString().split("\n").size()
}

最終結果:

android {  
    // ...
    defaultConfig {
        // ...
        versionCode autoVersionCode() // 4
        versionName autoVersionName() // v1.0.1
    }
}

這里有一點需要注意,打tag是在即將發版的時候才進行的,如果我們想要在調試的時候先升級一下versionCode的話,那肯定不能走這套自動化方案。在實際中我推薦在dev的Flavor中將版本名和版本號手動填寫,在正式版中用tag做自動化處理。最后再寫個腳本在打tag的時候自動修改開發版本的versionCode,一切都變得輕松許多。

隱藏Release簽名

signingConfigs {
    storeFile file('../test_key.jks')
    storePassword 'test123'
    keyAlias 'kale'
    keyPassword 'test123'
}

通常情況下我們是這么配置簽名的,但這樣的話安全性就是一個問題。簽名會被自動提交到git倉庫中,所有倉庫的只讀權限的人員都可以看到,十分不安全。我們的目標應該是少數人掌握release的簽名,所有人可以有debug版本的簽名。這樣的話,在別的部門想要看下這個工程的代碼做參考的時候,項目組長就可以放心大膽的給別人開權限了。

很多人推薦在gradle.properties中存放配置信息:

STORE_FILE_PATH ../test_key.jks
STORE_PASSWORD test123
KEY_ALIAS kale
KEY_PASSWORD test123
signingConfigs {
    release {
        storeFile file(STORE_FILE_PATH)
        storePassword STORE_PASSWORD
        keyAlias KEY_ALIAS
        keyPassword KEY_PASSWORD
    }
}

但是gradle.properties也是被git管理的文件,如果你ignore掉了gradle.properties,就會出現文件找不到的錯誤,所以我強烈不建議在gradle.properties中存放正式版的簽名信息

我們可以參考ShareLoginLib的方式,可以建立一個signing.properties的文件,然后在里面寫上信息:

STORE_FILE_PATH = ../signing.keystore
STORE_PASSWORD = jack2017
KEY_ALIAS = jack
KEY_PASSWORD = jack@2017

gradle.properties中寫上debug的簽名:

STORE_FILE_PATH ../test_key.jks
STORE_PASSWORD test123
KEY_ALIAS kale
KEY_PASSWORD test123

build.gradle:

Properties props = new Properties()
File f = file(rootProject.file("signing.properties"))

// 如果這個簽名文件存在則用,如果不存在就從gradle.properties中取
if (!f.exists()) {
    f = file(rootProject.file("gradle.properties"))
}
props.load(new FileInputStream(f))

android {
    signingConfigs {
        release {
            storeFile file(props['STORE_FILE_PATH'])
            storePassword props['STORE_PASSWORD']
            keyAlias props['KEY_ALIAS']
            keyPassword props['KEY_PASSWORD']
        }
    }
}

因為signing.properities是被ignore掉的,所以這個文件不會被git管理,增加了簽名的可控性。

還有一種寫法是將簽名放入環境變量:

android {
    // ...
    signingConfigs {
        def appStoreFile = System.getenv("STORE_FILE")
        def appStorePassword = System.getenv("STORE_PASSWORD")
        def appKeyAlias = System.getenv("KEY_ALIAS")
        def appKeyPassword = System.getenv("KEY_PASSWORD")
        
        // 四要素中的任何一個沒有獲取到,就使用默認的簽名信息
        if(!appStoreFile||!appStorePassword||!appKeyAlias||!appKeyPassword){
            appStoreFile = "debug.keystore"
            appStorePassword = "android"
            appKeyAlias = "androiddebugkey"
            appKeyPassword = "android"
        }
        release {
            storeFile file(appStoreFile)
            storePassword appStorePassword
            keyAlias appKeyAlias
            keyPassword appKeyPassword
        }
    }
}

這里用到了System.getenv()方法,你可以參考java中System下的getenv()來理解,就是當前機器的環境變量。比如System.getenv().get("ADB")在我機器上得到的就是:H:\Android\sdk\platform-tools;H:\Android\sdk\tools。

自動打開apk的目錄

開發人員通常情況下是不Build Apk的,開發都是直接run app,但是測試人員經常要編譯各種版本,很少去run app。Android Studio在每次生成apk后都會提示去打開本地目錄,那么我們能否在編譯成功Release版本的時候自動打開本地目錄呢?

image_1bpi4qa3eblt1gu71n6a15olb1i13.png-23kB
image_1bpi4qa3eblt1gu71n6a15olb1i13.png-23kB
image_1bpi4p7gj1q1m1nkba0abc6fko9.png-9.5kB
image_1bpi4p7gj1q1m1nkba0abc6fko9.png-9.5kB

下面的腳本通過applicationVariants來監聽apk生成的時機,如果是Rlease版本就打開文件管理器:

android {
    applicationVariants.all { variant ->
        variant.assemble.doLast {
            //If this is a 'release' build, reveal the compiled apk in finder/explorer
            if (variant.buildType.name.contains('release')) {
                def path = null
                variant.outputs.each { output ->
                    path = output.outputFile
                }
                if (path != null) {
                    if (System.properties['os.name'].toLowerCase().contains('mac os x')) {
                        ['open', '-R', path].execute()
                    } else if (System.properties['os.name'].toLowerCase().contains('windows')) {
                        ['explorer', '/select,', path].execute()
                    }
                }
            }
        }
    }
}

遠程依賴

配置倉庫

Gradle管理依賴是它的一大特點,想當年還在Eclipse時代的時候,所有的依賴都必須打包成jar,資源文件還得依次復制到工程中,十分難以管理。

無論是遠程依賴還是本地依賴,配置依賴的倉庫總是我們的第一步:

buildscript {
  repositories {
    maven { url 'https://maven.fabric.io/public' }
  }

  dependencies {
    // These docs use an open ended version so that our plugin
    // can be updated quickly in response to Android tooling updates

    classpath 'io.fabric.tools:gradle:1.+'
    classpath 'com.antfortune.freeline:gradle:0.8.'
    classpath 'me.tatarka:gradle-retrolambda:3.2.5'
  }
}

配置多個maven倉庫:

allprojects {
    repositories {
        jcenter()
        maven {
            url="http://maven.mbd.qiyi.domain/nexus/content/repositories/mbd-vertical/"
        }
        maven {
            url "https://jitpack.io"
        }
        maven {
            url 'http://repo.xxxx.net/nexus/'
            name 'maven name'
            credentials {
                username = 'username'
                password = 'password'
            }
        }
    }
}

其中name和credentials是可選項,視具體情況而定。

基礎Api

image_1bpi8nt3m1ulotlo160ac7lc8q2d.png-82.7kB
image_1bpi8nt3m1ulotlo160ac7lc8q2d.png-82.7kB

Gradle提供了多種依賴方式的Api:

配置 解釋
api 編譯時依賴和運行時依賴
implementation 基礎依賴方式,運行時依賴,對于依賴結構做了優化
compileOnly 類似于provided,僅僅在編譯時進行依賴,不會將依賴打包到app中
runtimeOnly 類似于apk,它僅僅將依賴打包到apk中,在編譯時無法獲得依賴的類
annotationProcessor 類似于apt,是注解處理器的依賴
testImplementation Java測試庫的依賴,僅僅在測試環境生效
androidTestImplementation Android測試庫的依賴,僅僅在測試環境生效
[Flavor]Api 針對于某個Flavor的依賴,寫法是Flavor的名稱+依賴方式

舉例:

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    // 基礎依賴方式
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:' + rootProject.ext.support_version
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    
    // 依賴注解,注解處理器不會打包到apk中
    compileOnly 'com.baoyz.treasure:treasure:0.7.4'
    annotationProcessor 'com.baoyz.treasure:treasure-compiler:0.7.4'
    annotationProcessor 'com.google.dagger:dagger-compiler:<version-number>'
    
    // buildTypes是debug的時候才能被依賴
    debugImplementation 'com.github.nekocode.ResourceInspector:resinspector:0.5.3'
    
    testImplementation 'junit:junit:4.12'
    
    androidTestImplementation 'com.android.support.test:runner:1.0.0'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.0'
    
    // 編寫時無法訪問lib中的資源,lib會被打包到apk中
    runtimeOnly project(':lib')
}

組合依賴

有時候一些庫是一并依賴的,刪除的時候也是要一并剔除的,如果像上面一樣多條引用的話,很容易不知道哪些庫是要一并刪除的。為了解決這個問題,我們可以像下面這樣進行統一引入:

implementation([
        'com.github.tianzhijiexian:logger:2e5da00f0f', // logger和timber總是結合使用的
        'com.jakewharton.timber:timber:4.1.2'
])

這樣整合起來的庫就成了一組,開發者一眼就知道這些庫是有相關性的,在刪除庫的時候十分方便。

implementation([
    'io.reactivex.rxjava2:rxjava:2.1.3', 
    'io.reactivex.rxjava2:rxandroid:2.0.1'
])

rxandroid本身是自帶rxjava的依賴的,但是rxjava的升級很快,rxandroid十分穩定,幾乎不怎么升級。在保證Api穩定的前提下,我們通過這種聚合依賴的方式可以很方便升級rxjava,讓核心代碼的升級不被rxandroid限制。

依賴傳遞

我們配置Crashlytics的依賴的時候一般會這樣寫:

api('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') {
    transitive = true;
}

為什么要寫transitive = true呢?其實@符號的作用是僅僅下載文件本身,不下載它自身的依賴,等于關閉了以來傳遞。如果你要支持依賴傳遞,那么就必須要寫transitive = true

更多配置方案可參考:Crashlytics for Android - Fabric Install

動態版本號

如果想要自己的依賴庫永遠保持最新版本,那么就可以利用版本名+-SNAPSHOT的方式來做:

implementation 'com.android.support:appcompat-v7:23.0.+'

implementation 'com.kale.business:CommonAdapter:1.0.6-SNAPSHOT'

同時還要記得開啟offline功能:

image_1bpialnaf9g51jfi1umu1vjp1ubf2q.png-84.4kB
image_1bpialnaf9g51jfi1umu1vjp1ubf2q.png-84.4kB

Gradle默認24小時自動檢查一次更新,我們可通過resolutionStrategy來修改檢查周期:

configurations.all {
    // check for updates every build
    resolutionStrategy.cacheDynamicVersionsFor 0, 'seconds'
    resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}
dependencies {
    implementation 'com.android.support:appcompat-v7:23.0.+'
    implementation 'com.kale.business:CommonAdapter:1.0.6-SNAPSHOT'
}

在實際中我不建議采用動態版本的方式做依賴。動態依賴就必然會出現版本不穩定的情況,你無法確定所有項目組的成員是否都保持了一致的依賴版本,而且一旦依賴版本的最新版出現了bug,你會不自覺的將bug引入進來,十分難以排查。對于這種不受到版本控制系統管理的危險方案,請不要隨意嘗試。

強制版本號

有時候第三方的lib中用到了很高版本的support包,而那個高版本的support包可能有一個bug,我們肯定不想因為它而引入這個bug。事實上Gradle的默認機制是有高版本則用高版本,這就讓我們處于了一種進退兩難的境地。幸好,configurations提供了強制約束庫版本的能力。

我們先在根build.gradle中配置一個task:

subprojects {
    task allDeps(type: DependencyReportTask) {}
}

使用命令行gradlew alldeps得到輸出:

image_1bpnccniu9oi1ffr15869q5b8k19.png-146.9kB
image_1bpnccniu9oi1ffr15869q5b8k19.png-146.9kB

配置強制的support版本號:

android {
    configurations.all {
        // 指定某個庫的版本
        resolutionStrategy.force "com.android.support:appcompat-v7:25.4.0"
        
        // 一次指定多個庫的版本
        resolutionStrategy {
            force 'com.android.support.test.espresso:espresso-core:3.0.0',
                     "com.android.support:appcompat-v7:25.4.0"
        }
    }
}
image_1bpnch7h511ak1g66p1god8i1e1m.png-155kB
image_1bpnch7h511ak1g66p1god8i1e1m.png-155kB

此外,我們還可以通過force來強制指定某個庫的版本號:

implementation group: 'com.android.support', name: 'appcompat-v7', version: '26.0.2', force: true

exclude關鍵字

如果我們引用的庫多了,各個庫之間可能會出現相互引用。Gradle的默認處理是進行依賴分析的時候自動將多個相同庫的最高版本定位最終依賴。但有時候會出現一個jar包打包了庫A,而我們依賴的庫B也有庫A。在實際中,我們經常會通過exclude關鍵字來剔除某些依賴:

implementation('com.android.support:appcompat-v7:23.2.0') {
    exclude group: 'com.android.support', module: 'support-annotations' // 寫全稱
    exclude group: 'com.android.support', module: 'support-compat'
    exclude group: 'com.android.support', module: 'support-v4'
    exclude group: 'com.android.support', module: 'support-vector-drawable'
}

剔除整個組織的庫(一下子剔除所有support庫):

implementation('com.facebook.fresco:animated-webp:0.13.0') {
    exclude group: 'com.android.support' // 僅僅寫組織名稱
}

exclude的參數有group和module,可以分別單獨使用。如果你想要全局剔除某個庫,可以在configurations中進行配置:

configurations {
   all*.exclude group: 'org.hamcrest', module: 'hamcrest-core'
}

順便一提,大廠部門眾多,不同部門的庫很容易就出現了依賴沖突。早期Google的Espresso庫就和support-annotations有沖突,所以Android官方給出了下面的方案:

// Espresso UI Testing
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
    exclude group: 'com.android.support', module: 'support-annotations'
})

現在3.x的版本就沒有這方面的問題了:

androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'

動態依賴第三方庫

用變量判斷

在Dev版本的時候我們可能會依賴很多測試庫,整合很多開發插件,App很容易就突破了65535的方法數限制。但是在Release版本中方法數卻很少,可能只需要一個Dex(一個Dex大概是10M)。最好的方式是我們可以通過判斷當前的開發狀態來決定是否需要依賴multidex這個庫。

除了可以通過BuildType、Flavor來判斷不同的開發環境外,我們還可以通過內部變量的邏輯判斷來做:

def needMultidex = true

android {
    buildTypes {
        release {
            multiDexEnabled = false // 關閉multiDex
            // ...
        }
        
        debug {
            multiDexEnabled true // 開啟multiDex
            // ...
        }
    }
}

dependencies {
    if (!needMultidex.toBoolean()) {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        // ....
    } else {
        implementation 'com.android.support:multidex:1.0.0'
        // ...
    }
}

用Flavor實現

如果這里的needMultidex修改的很頻繁,每次打包都需要改代碼,那么這樣的方案就不太合理了。在這種情況下,我建議通過Flavor來做依賴配置:

image_1bppsjsu67kmdep10d81d801qda9.png-18.2kB
image_1bppsjsu67kmdep10d81d801qda9.png-18.2kB
debugImplementation 'com.android.support:multidex:1.0.0'

通過Flavor的方式可以讓我們通過切換Build Variants的方式來切換環境,可以將環境切換和代碼分開,解決硬編碼的情況。但是如果我們不同環境的差異性很大,仍舊會出現不方便管理的情況。

以插件化方案舉例子,我們定義兩個Flavor:

android {
    productFlavors {
        // 插件
        plugin {
            buildConfigField "boolean", "IS_PLUGIN", "true"
        }
        // 獨立App
        single {
            buildConfigField "boolean", "IS_PLUGIN", "false"
        }
    }
}

// 這里的baseLibxxx在插件的時候是不需要打包的,而獨立App的時候是需要打包的
dependencies {
    pluginImplementation project(':pluginLib')
    pluginCompileOnly project(':baseLib01')
    pluginCompileOnly project(':baseLib02')
    pluginCompileOnly project(':baseLib03')
    pluginCompileOnly project(':baseLib04')

    singleImplementation 'com.android.support:multidex:1.0.0'
    singleImplementation project(':singleLib')
    singleImplementation project(':baselib01')
    singleImplementation project(':baselib02')
    singleImplementation project(':baselib03')
    singleImplementation project(':baselib04')
}

用回變量

通過區分Flavor的方式固然可以實現根據環境來依賴不同的東西,但如果更復雜一些呢?涉及到Task呢?對于復雜的需求,我們只有通過建立判斷邏輯來解決了。

增加判斷邏輯的好處是省去了一個Flavor,順便增加了動態程度:

// 每次切換插件/獨立App模式都得要改一次代碼,十分麻煩
ext { IS_PLUGIN = true; }

apply plugin: 'com.android.application'

// 判斷是否引入某個插件
if (IS_PLUGIN) {
    apply plugin: "build-time-tracker"
    buildtimetracker {
    reporters {
       csv {
           output "build/times.csv"
           append true
           header false
       }

       summary {
           ordered false
           threshold 50
           barstyle "unicode"
       }

       csvSummary {
           csv "build/times.csv"
       }
    } 
}

android {
    defaultConfig {
        // 判斷是否使用multiDex
        if (IS_PLUGIN) {
            multiDexEnabled false
        } else {
            multiDexEnabled true
        }
    }
    dexOptions {
        // 判斷內存配置
        if (!IS_PLUGIN) {
            javaMaxHeapSize "4g"
            jumboMode = true
        }
    }
    buildTypes {
        debug {
            buildConfigField("boolean", "IS_PLUGIN", "$IS_PLUGIN")
        }
        release {
            buildConfigField("boolean", "IS_PLUGIN", "$IS_PLUGIN")
        }
    }
    sourceSets {
        main {
            // 判斷資源
            if (!IS_PLUGIN) {
                jniLibs.srcDirs = ['libs']
            }
           res.srcDirs += ['src/main/res' , 'src/main/res-v7-appcompat']
        }
    }
}

dependencies {
    // 這里還是出現了大量的相似依賴,說明可以進一步的進行優化
    if (IS_PLUGIN) {
        compileOnly fileTree(dir: 'libs-common', include: ['*.jar'])
        compileOnly fileTree(dir: 'libs', include: ['*.jar'])
        compileOnly 'com.android.support:support-annotations:23.0.1'
    } else {
        implementation fileTree(dir: 'libs-compile', include: ['*.jar'])
        implementation fileTree(dir: 'libs-common', include: ['*.jar'])
        implementation(name: 'lintaar-release', ext: 'aar')

        implementation 'com.android.support:multidex:1.0.0'
    }
}

優化依賴

有些庫在插件的宿主中是有的,但是調試的時候是獨立的App,所以只需要在調試時依賴。為了聚合這些類似的庫,我們可以將其封裝為數組,最終進行一次性的依賴判斷:

dependencies {
    ext.libs =
            ['com.baoyz.treasure:treasure:0.7.4',
            'com.squareup.okhttp3:okhttp:3.9.0',
            'io.reactivex.rxjava2:rxjava:2.1.3']

    if (isPlugin()) {
        compileOnly(libs) // 不將依賴打入App中
    } else {
        implementation(libs)
    }

    if (isPlugin()) {
        implementation project(':releaselib')
    } else {
        implementation project(':debuglib')
    }
}

優化配置

如果我們切換一次獨立App/插件模式,那么IS_PLUGIN字段就得修改一遍。一個項目組內有些同事在調試插件模式,一些同事在調試獨立App,那么每次的Git提交就很容易在這個字段上沖突。為了解決這個問題,我們可以通過Gradle的打包命令,在執行命令行的時候動態設置這個字段,讓所有的修改和代碼分離:

// 并不定義PLUGIN這個變量
//ext.PLUGIN = true

ext.isPlugin = {
    try {
        if (PLUGIN.toBoolean()) {
            return true
        }
    } catch (Exception ignore) {
    }
    return false
}

默認是獨立App模式,執行命令行時可進行修改:

gradlew clean -P PLUGIN=true installInnertestDevDebug
image_1bpq3onor1m5i8715l0u88mmv9.png-62.1kB
image_1bpq3onor1m5i8715l0u88mmv9.png-62.1kB

總的來說,如果你的需求不是很復雜,那么推薦用Flavor的方式,如果你的需求十分復雜,對于動態化和靈活性的要求很高,那么建議通過變量的方式來做。

依賴管理

如果一個項目依賴多了,自然就變得難以管理了,尤其是多個module依賴了多個相同的庫的時候。為了實現一次庫升級全部module生效的目標,我們將差異化的參數可以提取到一個文件中來配置。

在根目錄中建立一個xxx.gradle文件,比如libconfig.gradle文件:

ext {
    android = [
        compileSdkVersion: 23,
        applicationId: "com.kale.gradle",
    ]
    
    dependencies = [
        "kalelib": "com.kale.support:kalelib:4.2.1",
    ]
}

然后在根目錄的build.gradle中加入libconfig.gradle

apply from: "libconfig.gradle" // 引入該文件

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.2'
    }
    // ...
}

之后就可以在其余的gradle中讀取變量了:

defaultConfig {
    applicationId rootProject.ext.android.applicationId // 引用applicationId
    minSdkVersion 14
    targetSdkVersion 20
}

dependencies {
    implementation rootProject.ext.dependencies["kalelib"] // 引用dependencide
}

這種寫法適合于被多個module依賴的庫,但我不建議用這種方式管理support庫。一旦用這種方式管理了support庫,那么Android Studio的升級提示就完全失效了,其余類似的官方庫也是同理。

image_1bpijpqvh1n23sjva9b14fp85v9.png-32kB
image_1bpijpqvh1n23sjva9b14fp85v9.png-32kB

本地依賴

引用aar

有時候我們有部分代碼需要給多個項目組共用,在不方便上傳倉庫的時候,可以做一個本地的aar依賴。

1.把aar文件放在某目錄內,比如就放在app的libs目錄內

image_1bpkjae8p1n1l117i1jnv1k141hmn9.png-15.1kB
image_1bpkjae8p1n1l117i1jnv1k141hmn9.png-15.1kB

2.在app的build.gradle文件中添加:

apply plugin: 'com.android.application'

repositories {
    flatDir {
        dirs 'libs' // this way we can find the .aar file in libs folder
    }
}

3.之后在其他項目中添加下面的代碼后就引用了該aar

dependencies {
    // name不用加aar的后綴
    implementation(name:'lib-release', ext:'aar')
}

目前暫不知曉如何依賴根目錄中的aar文件。

依賴module/jar

module

依賴module:

implementation project(':lib')

如果的module在多級目錄中,那么首先要在settings.gradle中進行配置:

include ':app', ':lib', ':libraries:lib01', ':libraries:lib02'
image_1bpkouc9iah616lv2go473lig2n.png-6.1kB
image_1bpkouc9iah616lv2go473lig2n.png-6.1kB

依賴方式:

implementation project(':libraries:lib01')
implementation project(':libraries:lib02')

jar

依賴指定路徑下的全部jar文件

image_1bpko53ob1a6hd5ivnd1b621c5k1t.png-39kB
image_1bpko53ob1a6hd5ivnd1b621c5k1t.png-39kB
dependencies {
    // 依賴當前module的libs目錄下的所有jar
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    
    // 依賴外部目錄,librarymodule中libs目錄下的所有jar
    implementation fileTree(dir: '../somedir/libstore', include: '*.jar')
}

如果用這種模糊依賴的話,我們只需要把要依賴的jar放入某個目錄中就好,但是這就有難以被版本控制系統管理的問題。一般情況下,我建議通過指定依賴的方式來做

// 依賴當前目錄下的某個jar
implementation files('libs/guava-19.0.jar') // 指定依賴某個jar

// 依賴其他目錄下的某個jar
implementation files('../somedir/libstore/gson-2.8.1.jar')

為了方便管理和維護,放入jar文件的時候記得帶上版本號:

image_1bpkn7e2vo919ik1k9ubct1g8613.png-8.8kB
image_1bpkn7e2vo919ik1k9ubct1g8613.png-8.8kB

題外話:

jar所在的目錄的名字可以隨便定義的,不局限于libs和是否在當前工程,見名之意即可。

自建倉庫

除了直接依賴aar或某個module外,我們可以將自己的module變成本地依賴的方式提供出去。

一個倉庫通常具有如下參數:

<?xml version="1.0" encoding="UTF-8"?>
<metadata>

  <groupId>com.kale.github.example</groupId>
  <artifactId>LocalLib</artifactId>
  
  <versioning>
    <release>1.1.1</release>
    <versions>
      <version>1.1.1</version>
    </versions>
    <lastUpdated>20170910025547</lastUpdated>
  </versioning>
</metadata>

這里的groupId和庫名字一定要和公司的其余項目組協調定義,一般情況下一個公司的庫的groupId都是一致的,這個是要寫入wiki的。

可以參考Gson的信息:

image_1bpkuj8ffenb1vta1bdf1gc8121n58.png-40.5kB
image_1bpkuj8ffenb1vta1bdf1gc8121n58.png-40.5kB

生成庫

1.在根路徑下的gradle.properties添加:

#org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true

# 組織信息
GROUP_ID=com.kale.github.example

# Licence信息(一般用apache的就行)
PROJ_LICENCE_NAME=The Apache Software License, Version 2.0
PROJ_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
PROJ_LICENCE_DEST=repo

2.在module(library)的build.gradle中定義發布配置:

apply plugin: 'com.android.library'

apply plugin: 'maven'

uploadArchives {
    repositories.mavenDeployer {
        pom.artifactId = 'LocalLib'
        pom.groupId = GROUP_ID
        pom.version = '1.1.1'
        repository(url: "file:///${rootDir}/localstorage/locallib")
    }
}

3.執行發布命令,等待文件生成完畢:

// 我演示的library叫做locallib
./gradlew -p <Library name> clean build uploadArchives --info
image_1bpksi0upj281djvece1g9p1b2i34.png-9.9kB
image_1bpksi0upj281djvece1g9p1b2i34.png-9.9kB
image_1bpksmia45d21edq1tic5hens73h.png-35.7kB
image_1bpksmia45d21edq1tic5hens73h.png-35.7kB

依賴庫

依賴本地庫的方式將在依賴React Native的時候講,這里直接列代碼:

repositories {
    // ...
    maven {
        url "$rootDir/localstorage/locallib/"
    }
}

implementation 'com.kale.example:LocalLib:1.1.1'

順便一提,你也可以在Libary的根目錄下新建gradle.properties文件來填寫配置參數:

ARTIFACTID = androidLib
LIBRARY_VERSION = 2.2.2

LOCAL_REPO_URL = file:///D:/kale/my/local/repo // 可以是絕對路徑,但一定是file:開頭的

在build.gradle中:

apply plugin: 'com.android.library'
apply plugin: 'maven'

uploadArchives{
    repositories.mavenDeployer{
        repository(url:LOCAL_REPO_URL)
        pom.groupId = GROUP_ID
        pom.artifactId = ARTIFACTID
        pom.version = LIBRARY_VERSION
    }
}

本地依賴React Native

FaceBook的React Native因為更新速度很快,在它不支持遠程依賴的時候,我們可以考慮將項目作為一個倉庫進行配置,而倉庫的地址就是本地的目錄。

1.先將庫文件放入一個module的libs目錄中:

2.配置maven的url為本地地址:

allprojects {
    repositories {
        maven {
            // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
            url "$rootDir/module_name/libs/android" // 路徑是根據放置的目錄來定的
        }
    }
}

3.正常使用:

dependencies {
    implementation 'com.facebook.react:react-native:0.32.0'
}

這里用到了$rootDir來屏蔽多個開發者機器環境的差異性,保證了項目的兼容性。

依賴沖突

我們依賴本地jar的時候可能會出現jar中也打包了別的庫代碼的情況,如果是aar我們可以通過gradle來做處理,但在面對依賴沖突的時候,jar文件就變得令人棘手了。

shevek/jarjar是一個再次打包工具,它可以為我們提供一次性更換包名的功能,是一個解決一來沖突的利器。

它還提供了gradle的腳本來操作你依賴的jar文件:

dependencies {
    // Use jarjar.repackage in place of a dependency notation.
    compile jarjar.repackage {
        from 'com.google.guava:guava:18.0'

        classDelete "com.google.common.base.**"

        classRename "com.google.**" "org.private.google.@1"
    }
}

這回我們嘗試通過手動的方式來操作gson.jar,我們希望把原本的com.google.gson的包換為com.gg.gson

1.先建立一個rule.txt的文本文件,內容:

rule  com.google.gson.** com.gg.gson.@1

2.執行命令:

java -jar jarjar.jar process rule.txt gson.jar gg.jar

執行后我們可以看到在當前目錄生成了一個gg.jar的文件,分析后就可以發現其內容已經變了:

image_1bpo183ba1jhu1u7g5ibmln2m423.png-20.9kB
image_1bpo183ba1jhu1u7g5ibmln2m423.png-20.9kB

jarjar并不提供修改META-INF的功能,但這并不影響我們使用。

如果你想要刪除特定包或特定的類,那么就在rule.txt中加入zap命令。

rule  com.google.gson.** com.gg.gson.@1

zap com.google.gson.reflect.TypeToken // 刪除某個類

zap com.google.gson.stream.**

zap com.google.gson.annotations.**

zap com.google.gson.internal.**

原始的gson:

image_1bpo1tnunvgabiki9jsl813m12g.png-29.3kB
image_1bpo1tnunvgabiki9jsl813m12g.png-29.3kB

刪除后:

image_1bpo1u8rk179i1991qurps81igr2t.png-16.8kB
image_1bpo1u8rk179i1991qurps81igr2t.png-16.8kB

除了上面提到的rule、zap外還是有keep。首先zap會刪除需要刪除的所有類,然后執行rule替換符合要求的類,最后如果配置了keep的話,將不符合規則的所有類的移除,只保留keep指定的包。總結來說,這三條命令的執行優先級是:zap > rule > keep。

需要注意的是:jarjar無法支持反射,如果jar包內有使用反射調用的情況,替換操作是十分危險的。

另一個插件dinuscxj/ClassPlugin還提供了替換依賴中的類的功能,有興趣可以嘗試一下。

題外話:

對于aar文件,我們只有將aar解壓后對解壓的jar進行處理,最后再打包成aar。

資源管理

多個manifest
指定資源目錄
微信組件化
替換資源的前綴

還沒有寫?。。。。?/strong>

總結

還沒有寫?。。。。?/strong>

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容