1. 程式人生 > >搞懂 Android Studio 構建那些事

搞懂 Android Studio 構建那些事

1.Android 構建系統

構建 APK 的過程是個相當複雜的過程,Android 構建系統需要將應用的資原始檔和原始檔一同打包到最終的 APK 檔案中。應用可能會依賴一些外部庫,構建工具要靈活地管理這些依賴的下載、編譯、打包(包括合併、解決衝突、資源優化)等過程。

應用的原始碼可能包括 Java 、RenderScript、AIDL 以及 Native 程式碼,構建工具需要分別處理這些語言的編譯打包過程,而有些時候我們需要生成不同配置(如不同 CPU 架構、不同 SDK 版本、不同應用商店配置等)的 APK 檔案,構建工具就需要根據不同情況編譯打包不同的 APK。

總之,構建工具需要完成從工程資源管理到最終編譯、測試、打包、釋出的幾乎所有工作。而 Android Studio 選擇了使用 Gradle,一個高階、靈活、強大的自動構建工具構建 Android 應用,利用 Gradle 和 Android Gradle 外掛可以非常靈活高效地構建和測試 Android 應用了:


 Gradle和其Android外掛可以幫助你自定義以下幾方面的構建配置:

  • Build Types

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

  • Product Flavors

Product flavors(產品風味)定義了你要釋出給使用者的不同版本,比如免費版和付費版。你可以在共享重用通用版本功能的時候自定義 product flavors 使用不同的程式碼和資源,Product flavors 是可選的所以你必須手動建立


  • Build Variants

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

  • Manifest Entries

你可以在 build variant 配置中指定 manifest 檔案中的某個屬性值(如應用名、最小 SDK 版本、target SDK 版本),這個值會覆蓋 manifest 檔案中原來的屬性值

  • Dependencies

構建系統會管理工程用要用到的本地檔案系統和遠端倉庫的依賴。

  • Signing

構建系統會讓你指定簽名設定以便在構建時自動給你的 APK 簽名,構建工具預設會使用自動生成的 debug key 給 debug 版本簽名,你也可以生成自己的 debug key 或 release key 使用。

  • ProGuard

構建系統讓你可以為每個構建變體指定不同的混淆規則檔案

  • Multiple APK Support

構建系統讓你可以為不同螢幕密度或 Application Binary Interface (ABI)的裝置生成包含其所需要的資源的 APK 檔案,如為 x86 CPU 架構的裝置生成只包含該 x86 架構 so 庫的 APK 檔案。

而這些構建配置要體現在不同的構建配置檔案中,典型的Android應用結構為:

1.1 Gradle Settings 檔案

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

include ':app', ':lib'

對於setting.gradle中也可以寫程式碼的,可以參考:

1.2 頂層 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}"
    ...
}

1.3 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'
}

1.4 Gradle 屬性檔案

位於工程根目錄的 gradle.properties 檔案和 local.properties 用來指定 Gradle 構建工具自己的設定。

gradle.properties 檔案可以用來配置工程的 Gradle 設定,如 Gradle 守護程序的最大堆疊大小

local.properties 檔案用來配置構建系統的本地環境屬性,如 SDK 安裝路徑,由於該檔案內容是 Android Studio 自動生成的且與本地開發環境有關,所以你不要更改更不要上傳到版本控制系統中。

2.Gradle 概述

Gradle 是專注於靈活性和效能的開源自動構建工具。Gradle 的構建指令碼使用 Groovy 或 Kotlin 語言。Gradle 構建工具的優勢在於:

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

學習 Gradle 的途徑有很多:

  • 每個月都有由 Gradle 核心工程師主持的 免費線上培訓,你需要提前註冊預約
  • Gradle 的 guides 和 reference documentation 也是很好的閱讀材料
  • 通過 Gradle Newsletter 閱讀Gradle最新期刊
  • 通過 Gradle Forum 論壇獲取幫助,其他成員或核心開發者會回答你的問題
  • 還可以通過 Command-line completion 學習 bash 和 zsh 命令列

2.1 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%相容所有的倉庫格式

2.2 常用的依賴配置

Java Library外掛 繼承自 Java外掛,但 Java Library 外掛與 Java 外掛最主要的不同是 Java Library 外掛引入了將 API 暴露給消費者(使用者)的概念,一個 library 就是一個用來供其他元件(component)消費的 Java 元件。

Java Library 外掛暴露了兩個用於宣告依賴的 Configuration(依賴配置): api 和 implementation。出現在 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 變體中可用,而如 testImplementation、androidTestImplementation 等依賴配置可以更好地處理測試相關依賴。

2.3 宣告依賴

2.3.1 宣告 binary 依賴

現在的軟體工程很少單獨地構建程式碼,因為現在的工程通常為了重用已存在且久經考驗的功能而引入外部庫,因此被稱為 binary dependencies。Gradle 會解析 binary 依賴然後從專門的遠端倉庫中下載並存到 cache 中以避免不必要的網路請求:


每個 artifact 在倉庫中的 coordinate 都會包含 groupId 、 artifactId 和 version 三個元素,如在一個使用 Spring 框架的 Java 工程中新增一個編譯時依賴:

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

Gradle 會從 Maven中央倉庫 https://search.maven.org/ 解析並下載這個依賴(包括它的傳遞依賴),然後使用它去編譯 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'

2.3.2 宣告檔案依賴

工程有時候不會依賴 binary 倉庫中的庫,而是把依賴放在共享磁碟或者版本控制系統的工程原始碼中(JFrog Artifactory 或 Sonatype Nexus 可以儲存解析這種外部依賴),這種依賴被稱為 file dependencies ,因為它們是以不涉及任何 metadata(如傳遞依賴、作者)的檔案形式存在的。如我們新增來自 ant、libs 和 tools 目錄的檔案依賴:

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')
}

2.3.3 宣告工程依賴

現在的工程通常把元件獨立成 module 以提高可維護性及防止強耦合,這些 module 可以定義相互依賴以重用程式碼,而 Gradle 可以管理這些 module 間的依賴。由於每個 module 都表現成一個 Gradle project,這種依賴被稱為 project dependencies 。在執行時,Gradle 構建會自動確保工程的依賴以正確的順序構建並新增到 classpath 中編譯

project(':web-service') {
    dependencies {
        implementation project(':utils')
        implementation project(':api')
    }
}

3.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 檔案中的類。在構建型別中指定 multiDexKeepFile 或 multiDexKeepProguard 屬性即可: 

在 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')
            ...
        }
    }
}

參考