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')
            ...
        }
    }
}
复制代码

参考

相关文章
相关标签/搜索