Android Studio 構建那些事

Android 構建系統

概述

構建 APK 的過程是個至關複雜的過程,Android 構建系統須要將應用的資源文件和源文件一同打包到最終的 APK 文件中。應用可能會依賴一些外部庫,構建工具要靈活地管理這些依賴的下載、編譯、打包(包括合併、解決衝突、資源優化)等過程。應用的源碼可能包括 Java 、RenderScript、AIDL 以及 Native 代碼,構建工具須要分別處理這些語言的編譯打包過程,而有些時候咱們須要生成不一樣配置(如不一樣 CPU 架構、不一樣 SDK 版本、不一樣應用商店配置等)的 APK 文件,構建工具就須要根據不一樣狀況編譯打包不一樣的 APK。總之,構建工具須要完成從工程資源管理到最終編譯、測試、打包、發佈的幾乎全部工做。而 Android Studio 選擇了使用 Gradle,一個高級、靈活、強大的自動構建工具構建 Android 應用,利用 Gradle 和 Android Gradle 插件能夠很是靈活高效地構建和測試 Android 應用了:
html

The build process of a typical Android app module
Gradle和其Android插件能夠幫助你自定義如下幾方面的構建配置:

Build Types : Build types(構建類型)定義了一些構建、打包應用時 Gradle 要用到的屬性,主要用於不一樣開發階段的配置,如 debug 構建類型要啓用 debug options 並用 debug key 簽名,release 構建類型要刪除無效資源、混淆源碼以及用 release key 簽名java

Product Flavors : Product flavors(產品風味)定義了你要發佈給用戶的不一樣版本,好比免費版和付費版。你能夠在共享重用通用版本功能的時候自定義 product flavors 使用不一樣的代碼和資源,Product flavors 是可選的因此你必須手動建立android

Build Variants : build variant(構建變體)是 build type 和 product flavor 的交叉輸出(如free-debug、free-release、paid-debug、paid-release),Gradle 構建應用要用到這個配置。也就是說添加 build types 或 product flavors 會相應的添加 build variantsgit

Manifest Entries : 你能夠在 build variant 配置中指定 manifest 文件中的某個屬性值(如應用名、最小 SDK 版本、target SDK 版本),這個值會覆蓋 manifest 文件中原來的屬性值web

Dependencies : 構建系統會管理工程用要用到的本地文件系統和遠程倉庫的依賴。spring

Signing : 構建系統會讓你指定簽名設置以便在構建時自動給你的 APK 簽名,構建工具默認會使用自動生成的 debug key 給 debug 版本簽名,你也能夠生成本身的 debug key 或 release key 使用。apache

ProGuard : 構建系統讓你能夠爲每一個構建變體指定不一樣的混淆規則文件api

Multiple APK Support : 構建系統讓你能夠爲不一樣屏幕密度或 Application Binary Interface (ABI)的設備生成包含其所須要的資源的 APK 文件,如爲 x86 CPU 架構的設備生成只包含該 x86 架構 so 庫的 APK 文件。緩存

而這些構建配置要體如今不一樣的構建配置文件中,典型的Android應用結構爲:
bash

The default project structure for an Android app module

Gradle Settings 文件

位於工程根目錄的 settings.gradle 文件用於告訴Gradle構建應用時須要包含哪些 module,如 :

include ':app', ':lib'
複製代碼

頂層 Build 文件

位於工程根目錄的 build.gradle 文件用於定義工程全部 module 的構建配置,通常頂層 build 文件使用 buildscript 代碼塊定義 Gradle 的 repositories 和 dependencies,如自動生成的頂層 build 文件:

/** * buildscript代碼塊用來配置Gradle本身的repositories和dependencies,因此不能包含modules使用的dependencies */

buildscript {

    /** * repositories 代碼塊用來配置 Gradle 用來搜索和下載依賴的倉庫 * Gradle 默認是支持像 JCenter,Maven Central,和 Ivy 遠程倉庫的,你也可使用本地倉庫或定義你本身的遠程倉庫 * 下面的代碼定義了 Gradle 用於搜索下載依賴的 JCenter 倉庫和 Google 的 Maven 倉庫 */

    repositories {
        google()
        jcenter()
    }

    /** * dependencies 代碼塊用來配置 Gradle 用來構建工程的依賴,下面的代碼表示添加一個 * Gradle 的 Android 插件做爲 classpath 依賴 */

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

/** * allprojects 代碼塊用來配置工程中全部 modules 都要使用的倉庫和依賴 * 可是你應該在每一個 module 級的 build 文件中配置 module 獨有的依賴。 * 對於一個新工程,Android Studio 默認會讓全部 modules 使用 JCenter 倉庫和 Google 的 Maven 倉庫 */

allprojects {
   repositories {
       google()
       jcenter()
   }
}
複製代碼

除了這些,你還可使用 ext 代碼塊在這個頂層 build 文件中定義工程級(工程中全部 modules 共享)的屬性:

buildscript {...}

allprojects {...}

ext {
    // 如讓全部 modules 都使用相同的版本以免衝突
    compileSdkVersion = 26
    supportLibVersion = "27.0.2"
    ...
}
...
複製代碼

每一個 module 的 build 文件使用 rootProject.ext.property_name 語法使用這些屬性便可:

android {
  compileSdkVersion rootProject.ext.compileSdkVersion
  ...
}
...
dependencies {
    compile "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
    ...
}
複製代碼

Module 級 Build 文件

位於每一個 project/module/ 目錄的 build.gradle 文件用於定義該 module 本身的構建配置,同時你也能夠重寫頂層 build 文件或 main app manifest 的配置:

/** * 爲這個構建應用 Gradle 的 Android 插件,以便 android 代碼塊中 Android 特定的構建配置可用 */

apply plugin: 'com.android.application'

/** * android 代碼塊用來配置 Android 特定的構建配置 */

android {

  /** * compileSdkVersion 用來指定 Gradle 用來編譯應用的 Android API level,也就是說 * 你的應用可使用這個 API level 及更低 API level 的 API 特性 */

  compileSdkVersion 26

  /** * buildToolsVersion 用來指定 SDK 全部構建工具、命令行工具、以及 Gradle 用來構建應用的編譯器版本 * 你須要使用 SDK Manager 下載好該版本的構建工具 * 在 3.0.0 或更高版本的插件中。該屬性是可選的,插件會使用推薦的版本 */

  buildToolsVersion "27.0.3"

  /** * defaultConfig 代碼塊包含全部構建變體(build variants)默認使用的配置,也能夠重寫 main/AndroidManifest.xml 中的屬性 * 固然,你也能夠在 product flavors(產品風味)中重寫其中一些屬性 */

  defaultConfig {

    /** * applicationId 是發佈時的惟一指定包名,儘管如此,你仍是須要在 main/AndroidManifest.xml 文件中 * 定義值是該包名的 package 屬性 */

    applicationId 'com.example.myapp'

    // 定義能夠運行該應用的最小 API level
    minSdkVersion 15

    // 指定測試該應用的 API level
    targetSdkVersion 26

    // 定義應用的版本號
    versionCode 1

    // 定義用戶友好型的版本號描述
    versionName "1.0"
  }

  /** * buildTypes 代碼塊用來配置多個構建類型,構建系統默認定義了兩個構建類型: debug 和 release * debug 構建類型默認不顯式聲明,但它包含調試工具並使用 debug key 簽名 * release 構建類型默認應用了混淆配置 */

  buildTypes {
    release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }

  /** * 因爲 product flavors 必須屬於一個已命名的 flavor dimension,因此你至少須要定義一個 flavor dimension * 如定義一個等級和最小 api 的 flavor dimension */

  flavorDimensions "tier", "minApi"

  productFlavors {
       free {
        // 這個 product flavor 屬於 "tier" flavor dimension
        // 若是隻有一個 dimension 那麼這個屬性就是可選的
        dimension "tier"
        ...
      }

      paid {
        dimension "tier"
        ...
      }

      minApi23 {
          dimension "minApi"
          ...
      }

      minApi18 {
          dimension "minApi"
          ...
      }
  }

  /** * 你可使用 splits 代碼塊配置爲不一樣屏幕分辨率或 ABI 的設備生成僅包含其支持的代碼和資源的 APK * 同時你須要配置 build 文件以便每一個 APK 使用不一樣的 versionCode */

  splits {
    density {

      // 啓用或禁用構建多個 APK
      enable false

      // 構建多個 APK 時排除這些分辨率
      exclude "ldpi", "tvdpi", "xxxhdpi", "400dpi", "560dpi"
    }
  }
}

/** * 該 module 級 build 文件的 dependencies 代碼塊僅用來指定該 module 本身的依賴 */

dependencies {
    implementation project(":lib")
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.0.2'
}
複製代碼

Gradle 屬性文件

位於工程根目錄的 gradle.properties 文件和 local.properties 用來指定 Gradle 構建工具本身的設置。
gradle.properties 文件能夠用來配置工程的 Gradle 設置,如 Gradle 守護進程的最大堆棧大小
local.properties 文件用來配置構建系統的本地環境屬性,如 SDK 安裝路徑,因爲該文件內容是 Android Studio 自動生成的且與本地開發環境有關,因此你不要更改更不要上傳到版本控制系統中。

Gradle 概述

Gradle 是專一於靈活性和性能的開源自動構建工具。Gradle 的構建腳本使用 GroovyKotlin 語言。Gradle 構建工具的優點在於:

  • 高度可定製 - Gradle 是以最基本的可定製和可擴展的方式模塊化的
  • 更快 - Gradle 經過重用以前執行的輸出、只處理更改的輸入以及並行執行 task 的方式加快構建速度
  • 更強大 - Gradle 支持跨多語言和平臺,是 Android 官方構建工具,支持不少主流 IDE,包括 Android Studio、Eclipse、IntelliJ IDEA、Visual Studio 2017 以及 XCode,未來會支持更多語言和平臺

學習 Gradle 的途徑有不少:

Gradle 的依賴管理

依賴管理(Dependency management)是每一個構建系統的關鍵特徵,Gradle 提供了一個既容易理解又其餘依賴方法兼容的一流依賴管理系統,若是你熟悉 Maven 或 Ivy 用法,那麼你確定樂於學習 Gradle,由於 Gradle 的依賴管理和二者差很少但比二者更加靈活。Gradle 依賴管理的優點包括:

  • 傳遞依賴管理 - Gradle 讓你能夠徹底控制工程的依賴樹
  • 支持非託管依賴 - 若是你只依賴版本控制系統或共享磁盤中的單個文件,Gradle 提供了強大的功能支持這種依賴
  • 支持個性化依賴定義 - Gradle 的 Module Dependencies 讓你能夠在構建腳本中描述依賴層級
  • 爲依賴解析提供徹底可定製的方法 - Gradle 讓你能夠自定義依賴解析規則以便讓依賴能夠方便地替換
  • 徹底兼容Maven和Ivy - 若是你已經定義了 Maven POM 或 Ivy 文件,Gradle 能夠經過相應的構建工具無縫集成
  • 能夠與已存在的依賴管理系統集成 - Gradle 徹底兼容 Maven 和 Ivy 倉庫,因此若是你使用 Archiva、Nexus 或 Artifactory,Gradle 能夠100%兼容全部的倉庫格式

經常使用的依賴配置

Java Library插件 繼承自 Java插件,但 Java Library 插件與 Java 插件最主要的不一樣是 Java Library 插件引入了將 API 暴露給消費者(使用者)的概念,一個 library 就是一個用來供其餘組件(component)消費的 Java 組件。 Java Library 插件暴露了兩個用於聲明依賴的 Configuration(依賴配置): apiimplementation。出如今 api 依賴配置中的依賴將會傳遞性地暴露給該 library 的消費者,並會出如今其消費者的編譯 classpath 中。而出如今 implementation 依賴配置中的依賴將不會暴露給消費者,也就不會泄漏到消費者的編譯 classpath 中。所以,api 依賴配置應該用來聲明library API 使用的依賴,而 implementation 依賴配置應該用來聲明組件內部的依賴。implementation 依賴配置有幾個明顯的優點:

  • 依賴不會泄漏到消費者的編譯 classpath 中,因此你也就不會無心中依賴一個傳遞依賴了
  • 因爲 classpath 大小的減小編譯也會更快
  • implementation 的依賴改變時,消費者不須要從新編譯,要從新編譯的不多
  • 更清潔地發佈,當結合新的 maven-publish 插件使用時,Java librariy 會生成一個 POM 文件來精確地區分編譯這個 librariy 須要的東西和運行這個 librariy 須要的東西

那到底何時使用 API 依賴何時使用 Implementation 依賴呢?這裏有幾個簡單的規則: 一個 API 是 library binary 接口暴露的類型,一般被稱爲 ABI (Application Binary Interface),這包括但不限於:

  • 父類或接口用的類型
  • 公共方法中參數用到的類型,包括泛型類型(公共指的是對編譯器可見的 public,protected 和 package private)
  • public 字段用到的類型
  • public 註解類型

相反,下面列表重要到的全部類型都與 ABI 無關,所以應該使用 implementation 依賴:

  • 只用在方法體內的類型
  • 只出如今 private 成員的類型
  • 只出如今內部類中的類型

例如

// The following types can appear anywhere in the code
// but say nothing about API or implementation usage
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.*;
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

public class HttpClientWrapper {

    private final HttpClient client; // private member: implementation details

    // HttpClient is used as a parameter of a public method
    // so "leaks" into the public API of this component
    public HttpClientWrapper(HttpClient client) {
        this.client = client;
    }

    // public methods belongs to your API
    public byte[] doRawGet(String url) {
        GetMethod method = new GetMethod(url);
        try {
            int statusCode = doGet(method);
            return method.getResponseBody();

        } catch (Exception e) {
            ExceptionUtils.rethrow(e); // this dependency is internal only
        } finally {
            method.releaseConnection();
        }
        return null;
    }

    // GetMethod is used in a private method, so doesn't belong to the API
    private int doGet(GetMethod method) throws Exception {
        int statusCode = client.executeMethod(method);
        if (statusCode != HttpStatus.SC_OK) {
            System.err.println("Method failed: " + method.getStatusLine());
        }
        return statusCode;
    }
}
複製代碼

其中,public 構造器 HttpClientWrapper 使用了 HttpClient 參數暴露給了使用者,因此屬於 API 依賴。而 ExceptionUtils 只在方法體中出現了,因此屬於 implementation 依賴。因此 build 文件這樣寫:

dependencies {
    api 'commons-httpclient:commons-httpclient:3.1'
    implementation 'org.apache.commons:commons-lang3:3.5'
}
複製代碼

所以,應該優先選擇使用 implementation 依賴:缺乏一些類型將會直接致使消費者的編譯錯誤,能夠經過移除這些類型或改爲 API 依賴解決。 compileOnly 依賴配置會告訴 Gradle 將依賴只添加到編譯 classpath 中(不會添加到構建輸出中),在你建立一個 Android library module 且在編譯時須要這個依賴時使用 compileOnly 是個很好的選擇。但這並不能保證運行時良好,也就是說,若是你使用這個配置,那麼你的 library module 必須包含一個運行時條件去檢查依賴是否可用,在不可用的時候仍然能夠優雅地改變他的行爲來正常工做,這有助於減小最終 APK 的大小(經過不添加不重要的transient依賴)。 runtimeOnly 依賴配置告訴 Gradle 將依賴只添加到構建輸出中,只在運行時使用,也就是說這個依賴不添加到編譯 classpath 中。 此外,debugImplementation 會使依賴僅在 module 的 debug 變體中可用,而如 testImplementationandroidTestImplementation 等依賴配置能夠更好地處理測試相關依賴。

聲明依賴

聲明 binary 依賴

如今的軟件工程不多單獨地構建代碼,由於如今的工程一般爲了重用已存在且久經考驗的功能而引入外部庫,所以被稱爲 binary dependencies。Gradle 會解析 binary 依賴而後從專門的遠程倉庫中下載並存到 cache 中以免沒必要要的網絡請求:

Resolving binary dependencies from remote repositories

每一個 artifact 在倉庫中的 coordinate 都會包含 groupIdartifactIdversion 三個元素,如在一個使用 Spring 框架的 Java 工程中添加一個編譯時依賴:

apply plugin: 'java-library'
repositories {
    mavenCentral()
}
dependencies {
    implementation 'org.springframework:spring-web:5.0.2.RELEASE'
}
複製代碼

Gradle 會從 Maven中央倉庫 解析並下載這個依賴(包括它的傳遞依賴),而後使用它去編譯 Java 源碼,其中的 version 屬性是指定了具體版本,代表老是使用這個具體的依賴再也不更改。 固然,若是你老是想使用最新版本的 binary 依賴,你可使用動態的 version,Gradle 默認會緩存 24 小時:

implementation 'org.springframework:spring-web:5.+'
複製代碼

有些狀況開發團隊在徹底完成新版本的開發以前爲了讓使用者能體驗最新的功能特點,會提供一個 changing version,在 Maven 倉庫中 changing version 一般被稱做 snapshot version,而 snapshot version會包含-SNAPSHOT後綴,如:

implementation 'org.springframework:spring-web:5.0.3.BUILD-SNAPSHOT'
複製代碼

聲明文件依賴

工程有時候不會依賴 binary 倉庫中的庫,而是把依賴放在共享磁盤或者版本控制系統的工程源碼中(JFrog Artifactory 或 Sonatype Nexus 能夠存儲解析這種外部依賴),這種依賴被稱爲 file dependencies ,由於它們是以不涉及任何 metadata(如傳遞依賴、做者)的文件形式存在的。如咱們添加來自 antlibstools 目錄的文件依賴:

configurations {
    antContrib
    externalLibs
    deploymentTools
}

dependencies {
    antContrib files('ant/antcontrib.jar')
    externalLibs files('libs/commons-lang.jar', 'libs/log4j.jar')
    deploymentTools fileTree(dir: 'tools', include: '*.exe')
}
複製代碼

聲明工程依賴

如今的工程一般把組件獨立成 module 以提升可維護性及防止強耦合,這些 module 能夠定義相互依賴以重用代碼,而 Gradle 能夠管理這些 module 間的依賴。因爲每一個 module 都表現成一個 Gradle project,這種依賴被稱爲 project dependencies 。在運行時,Gradle 構建會自動確保工程的依賴以正確的順序構建並添加到 classpath 中編譯。

project(':web-service') {
    dependencies {
        implementation project(':utils')
        implementation project(':api')
    }
}
複製代碼

Gradle 經常使用配置

強制全部的 android support libraries 使用相同的版本:

configurations.all {
    resolutionStrategy {
        eachDependency { details ->
            // Force all of the primary support libraries to use the same version.
            if (details.requested.group == 'com.android.support' &&
                    details.requested.name != 'multidex' &&
                    details.requested.name != 'multidex-instrumentation') {
                details.useVersion supportLibVersion
            }
        }
    }
}
複製代碼

更改生成的 APK 文件名:

android.applicationVariants.all { variant ->
    variant.outputs.all {
        outputFileName = "${variant.name}-${variant.versionName}.apk"
    }
}
複製代碼

若是開啓了 Multidex 後在 Android 5.0 如下設備上出現了 java.lang.NoClassDefFoundError 異常,多是因爲構建工具沒能把某些依賴庫代碼放進主 dex 文件中,這時就須要手動指定還有哪些要放入主 dex 文件中的類。在構建類型中指定 multiDexKeepFilemultiDexKeepProguard 屬性便可:
在 build 文件同級目錄新建 multidex-config.txt 文件,文件的每一行爲類的全限定名,如:

com/example/MyClass.class
com/example/MyOtherClass.class
複製代碼
android {
    buildTypes {
        release {
            multiDexKeepFile file('multidex-config.txt')
            ...
        }
    }
}
複製代碼

或者新建 multidex-config.pro 文件,使用 Proguard 語法指定放入主 dex 文件中的類,如:

-keep class com.example.** { *; }
複製代碼
android {
    buildTypes {
        release {
            multiDexKeepProguard file('multidex-config.pro')
            ...
        }
    }
}
複製代碼

參考

相關文章
相關標籤/搜索