优化编译速度

较长的编译时间会减慢您的开发过程。本页面将介绍一些可以帮助您打破编译速度瓶颈的技巧。

提高编译速度的一般过程如下所示:

  1. 采取一些可以使大多数 Android Studio 项目立即受益的措施,优化您的编译配置
  2. 分析您的编译版本,确定并诊断一些对您的项目或工作站来说比较棘手的问题。

在开发应用时,您应该尽可能将其部署到搭载 Android 7.0(API 级别 24)或更高版本的设备中。较新版本的 Android 平台具有向应用推送更新的更出色机制,例如 Android 运行时 (ART) 以及对多个 DEX 文件的原生支持。

注意:您完成首个整洁编译版本后,可能会注意到后续编译版本(整洁和增量)的执行速度明显加快了(即使您没有使用本页面介绍的任何优化措施,也是如此)。这是因为 Gradle 守护进程有一个性能提升“预热”期,类似于其他 JVM 进程。

优化编译配置

按照下面的提示操作,以提高 Android Studio 项目的编译速度。

确保工具已是最新版本

几乎每次更新时,Android 工具都会获得编译优化和新功能,本页面介绍的一些提示假设您使用的是最新版本。要充分利用最新的优化,请确保以下工具已是最新版本:

为开发创建编译变体

准备发布应用时所需的许多配置在应用开发过程中都是不必要的。启用不必要的编译流程会减慢您的增量编译和整洁编译速度,因此,请配置一个编译变体,使之仅包含开发应用时所需的编译配置。以下代码示例为您的发布版本配置创建了一个“dev”类型和一个“prod”类型:

    android {
      ...
      defaultConfig {...}
      buildTypes {...}
      productFlavors {
        // When building a variant that uses this flavor, the following configurations
        // override those in the defaultConfig block.
        dev {
          // To avoid using legacy multidex when building from the command line,
          // set minSdkVersion to 21 or higher. When using Android Studio 2.3 or higher,
          // the build automatically avoids legacy multidex when deploying to a device running
          // API level 21 or higher—regardless of what you set as your minSdkVersion.
          minSdkVersion 21
          versionNameSuffix "-dev"
          applicationIdSuffix '.dev'
        }

        prod {
          // If you've configured the defaultConfig block for the release version of
          // your app, you can leave this block empty and Gradle uses configurations in
          // the defaultConfig block instead. You still need to create this flavor.
          // Otherwise, all variants use the "dev" flavor configurations.
        }
      }
    }
    

如果您的编译配置已使用“product”类型创建应用的不同版本,您可以使用类型维度,将“dev”和“prod”配置与这些类型组合起来。例如,如果您已经配置了“demo”和“full”类型,则可以使用以下示例配置创建组合类型,如“devDemo”和“prodFull”:

    android {
      ...
      defaultConfig {...}
      buildTypes {...}

      // Specifies the flavor dimensions you want to use. The order in which you
      // list each dimension determines its priority, from highest to lowest,
      // when Gradle merges variant sources and configurations. You must assign
      // each product flavor you configure to one of the flavor dimensions.

      flavorDimensions "stage", "mode"

      productFlavors {
        dev {
          dimension "stage"
          minSdkVersion 21
          versionNameSuffix "-dev"
          applicationIdSuffix '.dev'
          ...
        }

        prod {
          dimension "stage"
          ...
        }

        demo {
          dimension "mode"
          ...
        }

        full {
          dimension "mode"
          ...
        }
      }
    }
    

启用单变体项目同步

将项目与编译配置同步,是让 Android Studio 了解项目结构的重要步骤。不过,对于大型项目来说,此过程可能非常耗时。如果您的项目使用了多个编译变体,那么您现在可以限定仅同步当前所选的变体,从而优化项目同步操作。

您需要结合使用 Android Studio 3.3 或更高版本与 Android Gradle Plugin 3.3.0 或更高版本,才能启用此优化。所有项目默认都启用了此优化。

要手动启用此优化功能,请依次点击 File > Settings > Experimental > Gradle(在 Mac 上,则依次点击 Android Studio > Preferences > Experimental > Gradle),然后选中 Only sync the active variant 复选框。

注意:此优化完全支持包含 Java 和 C++ 语言的项目,部分支持包含 Kotlin 语言的项目。在为包含 Kotlin 内容的项目启用此优化时,Gradle 同步会回退到在内部使用完整的变体。

避免编译不必要的资源

避免编译和打包不测试的资源(例如,其他语言本地化和屏幕密度资源)。为此,您可以仅为“dev”类型指定一个语言资源和屏幕密度,如下面的示例中所示:

    android {
      ...
      productFlavors {
        dev {
          ...
          // The following configuration limits the "dev" flavor to using
          // English stringresources and xxhdpi screen-density resources.
          resConfigs "en", "xxhdpi"
        }
        ...
      }
    }
    

对调试编译版本停用 Crashlytics

如果您不需要生成 Crashlytics 报告,请按如下方法停用该插件,以提高调试编译速度:

    android {
      ...
      buildTypes {
        debug {
          ext.enableCrashlytics = false
        }
    }
    

此外,您还需要通过更改在应用中初始化对 Fabric 的支持的方式,在运行时对调试编译版本停用 Crashlytics 套件,如下所示:

Kotlin

    // Initializes Fabric for builds that don't use the debug build type.
    Crashlytics.Builder()
            .core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build())
            .build()
            .also { crashlyticsKit ->
                Fabric.with(this, crashlyticsKit)
            }
    

Java

    // Initializes Fabric for builds that don't use the debug build type.
    Crashlytics crashlyticsKit = new Crashlytics.Builder()
        .core(new CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build())
        .build();

    Fabric.with(this, crashlyticsKit);
    

如果您想要将 Crashlytics 用于调试编译版本,仍可以通过阻止 Crashlytics 在每次编译过程中使用自己的唯一编译 ID 更新应用资源,提高增量编译的速度。要阻止 Crashlytics 不断更新其编译 ID,请将以下内容添加到 build.gradle 文件中:

    android {
      ...
      buildTypes {
        debug {
          ext.alwaysUpdateBuildId = false
        }
    }
    

要详细了解如何在使用 Crashlytics 时优化您的编译版本,请参阅官方文档

将静态编译配置值用于调试编译版本

始终为会进入调试编译类型的清单文件或资源文件的属性使用静态/硬编码值。如果清单文件或应用资源中的值需要随每个版本更新,那么 Instant Run 将无法执行代码交换;它必须编译和安装新的 APK。

例如,您每次需要运行更改时,要使用动态版本代码、版本名称、资源或可以更改清单文件的任何其他编译逻辑,都需要完整的 APK 编译版本,即使实际更改仅需要一个热交换,也是如此。如果您的编译配置需要此类动态属性,请将其隔离到您的发布版编译变体中,并使该值对您的调试编译版本保持静态,如下面的 build.gradle 文件所示。

    int MILLIS_IN_MINUTE = 1000 * 60
    int minutesSinceEpoch = System.currentTimeMillis() / MILLIS_IN_MINUTE

    android {
        ...
        defaultConfig {
            // Making either of these two values dynamic in the defaultConfig will
            // require a full APK build and reinstallation because the AndroidManifest.xml
            // must be updated (which is not supported by Instant Run).
            versionCode 1
            versionName "1.0"
            ...
        }

        // The defaultConfig values above are fixed, so your incremental builds don't
        // need to rebuild the manifest (and therefore the whole APK, slowing build times).
        // But for release builds, it's okay. So the following script iterates through
        // all the known variants, finds those that are "release" build types, and
        // changes those properties to something dynamic.
        applicationVariants.all { variant ->
            if (variant.buildType.name == "release") {
                variant.mergedFlavor.versionCode = minutesSinceEpoch;
                variant.mergedFlavor.versionName = minutesSinceEpoch + "-" + variant.flavorName;
            }
        }
    }
    

使用静态依赖项版本

build.gradle 文件中声明依赖项时,您应当避免在结尾处使用带加号的版本号,例如 'com.android.tools.build:gradle:2.+'。使用动态版本号可能会导致意外的版本更新和难以解析版本差异,并会因 Gradle 检查有无更新而减慢编译速度。您应该使用静态/硬编码版本号。

启用离线模式

如果网络连接速度比较慢,那么在 Gradle 尝试使用网络资源解析依赖项时,编译时间可能会延长。您可以指示 Gradle 仅使用已缓存到本地的工件,从而避免使用网络资源。

要在使用 Android Studio 编译项目时离线使用 Gradle,请执行以下操作:

  1. 依次点击 File > Settings(在 Mac 上,则依次点击 Android Studio > Preferences),打开 Preferences 窗口。
  2. 在左侧窗格中,依次点击 Build, Execution, Deployment > Gradle
  3. 勾选 Offline work 复选框。
  4. 点击 ApplyOK

如果您正在通过命令行编译,请传递 --offline 选项。

创建库模块

在应用中查找可以转换成 Android 库模块的代码。以这种方式将您的代码模块化,可以让编译系统仅编译您修改的模块,并缓存输出以用于未来的编译版本。此外,这种方式也会让并行项目执行更有效(当您启用该优化时)。

为自定义编译逻辑创建任务

生成编译分析报告后,如果分析报告显示相当长的一部分编译时间用在了“配置项目”阶段,请检查 build.gradle 脚本并查找您可以添加到自定义 Gradle 任务中的代码。将某些编译逻辑移到任务中后,它仅会在需要时运行,可以缓存结果以用于后续编译版本,并且该编译逻辑将可以并行运行(如果您已启用并行项目执行)。要了解详情,请阅读官方 Gradle 文档

提示:如果您的编译版本中包含大量自定义任务,则您可能需要通过创建自定义任务类来整理 build.gradle 文件。将您的类添加到 project-root/buildSrc/src/main/groovy/ 目录中,Gradle 会自动将其添加到项目中所有 build.gradle 文件的类路径中。

将图片转换为 WebP 格式

WebP 是一种图片文件格式,提供有损压缩(如 JPEG)以及透明度(如 PNG),不过与 JPEG 或 PNG 相比,这种格式可以提供更好的压缩效果。降低图片文件大小可以加快编译速度(无需在编译时进行压缩),尤其是当应用使用大量图片资源时。不过,在解压缩 WebP 图片时,您可能会注意到设备的 CPU 使用量有小幅上升。通过使用 Android Studio,您可以轻松地将图片转换为 WebP 格式

停用 PNG 处理

如果您无法(或者不想)将 PNG 图片转换为 WebP 格式,仍可以在每次编译应用时停用自动图片压缩,从而提高编译速度。如果您使用的是 Android 插件 3.0.0 或更高版本,则默认情况下仅针对“调试”编译类型停用 PNG 处理。要针对其他编译类型停用此优化,请将以下代码添加到 build.gradle 文件中:

    android {
        buildTypes {
            release {
                // Disables PNG crunching for the release build type.
                crunchPngs false
            }
        }

    // If you're using an older version of the plugin, use the
    // following:
    //  aaptOptions {
    //      cruncherEnabled false
    //  }
    }
    

由于编译类型或生产类型不定义此属性,因此在编译应用的发布版本时,您需要手动将此属性设置为 true

启用 Instant Run

Instant Run 可以在不编译新 APK 的情况下(有时甚至不需要重启当前 Activity)推送特定代码和资源更改,从而显著缩短更新应用所需的时间。对应用进行更改后,点击 Apply Changes 即可使用 Instant Run。此外,在您执行以下操作时,Instant Run 也会默认启用:

  • 使用调试编译变体编译应用。
  • 使用 Android plugin for Gradle 2.3.0 或更高版本。
  • 在应用的模块级 build.gradle 文件中将 minSdkVersion 设置为 15 或更高。
  • 点击 Run 以将应用部署到搭载 Android 5.0(API 级别 21)及更高版本的设备上。

如果您在满足上述要求后并没有在工具栏中看到 Apply Changes 按钮,请按以下步骤操作,以确保未在 IDE 设置中停用 Instant Run:

  1. 打开 SettingsPreferences 对话框。
  2. 依次转到 Build, Execution, Deployment > Instant Run
  3. 确保勾选 Enable Instant Run

启用编译缓存

编译缓存可以存储编译项目时 Android Plugin for Gradle 生成的特定输出(例如,未打包的 AAR 和经过 dex 预处理的远程依赖项)。使用缓存时,整洁编译版本的速度会明显加快,因为编译系统在进行后续编译时可以直接重用这些缓存文件,而无需重新创建。

使用 Android 插件 2.3.0 及更高版本的新项目会默认启用编译缓存(除非您明确停用编译缓存)。要了解详情,请参阅利用编译缓存加快整洁编译版本的速度

使用增量注解处理器

Android Gradle 插件 3.3.0 及更高版本改进了使用注解处理器时对增量 Java 编译的支持。因此,要提高增量编译速度,您应更新 Android Gradle 插件并尽可能仅使用增量注解处理器。

注意:此功能与 Gradle 4.10.1 及更高版本(Gradle 5.1 除外)兼容(请参阅 Gradle 问题 #8194)。

要开始使用,请参阅以下支持增量 Java 编译的热门注解处理器列表。有关更完整的列表,请参阅 GitHub 问题 #5277。某些插件可能需要额外的步骤才能启用优化,因此请务必查看每个插件的文档。

请注意,如果您使用一个或多个不支持增量编译的注解处理器,则增量 Java 编译不会启用。如果您需要使用不支持此优化的注解处理器,请在 gradle.properties 文件中添加以下标记。添加后,Android Gradle 插件会在一个单独的任务中执行所有注解处理器,并允许 Java 编译任务以增量方式运行。

    android.enableSeparateAnnotationProcessing = true
    

分析编译版本

对于较大的项目或者实现大量自定义编译逻辑的项目,您可能需要深入了解编译流程才能找到瓶颈。为此,您可以分析 Gradle 执行编译生命周期的每个阶段和每个编译任务所需的时间。例如,如果编译分析报告显示 Gradle 在配置项目时花费了过多的时间,则表明您需要将自定义编译逻辑移出配置阶段。此外,如果 mergeDevDebugResources 任务占用了大量编译时间,则表明您还需要将图片转换为 WebP 格式停用 PNG 处理

要通过分析报告提高编译速度,通常需要在启用分析功能的情况下运行编译,对编译配置进行一些调整,以及进行更多分析来观察更改的结果。

要生成和查看编译分析报告,请按以下步骤操作:

  1. 在 Android Studio 中打开项目后,依次选择 View > Tool Windows > Terminal,以在项目的根目录下打开命令行。
  2. 输入以下命令,以执行整洁编译版本。在分析编译版本时,您应该在分析的每个编译之间执行整洁编译,这是因为如果某个任务的输入内容(例如源代码)未发生更改,Gradle 就会跳过它。因此输入内容未发生更改的第二个编译版本始终会以更快的速度运行,因为任务不会重复运行。在编译版本之间运行 clean 任务可以确保您能够分析完整的编译流程。
        // On Mac or Linux, run the Gradle wrapper using "./gradlew".
        gradlew clean
        
  3. 使用以下标记为您的某个生产类型(例如“dev”类型)执行调试编译版本:
        gradlew --profile --offline --rerun-tasks assembleFlavorDebug
        
    • --profile:启用分析。
    • --offline:禁止 Gradle 提取在线依赖项。这样可以确保因 Gradle 尝试更新依赖项而导致的任何延迟都不会干扰您的分析数据。您应该已将项目编译一次,以确保 Gradle 已经下载和缓存您的依赖项。
    • --rerun-tasks:强制 Gradle 重新运行所有任务并忽略任何任务优化。
  4. 图 1 指示分析报告所在位置的 Project 视图。

    编译完成后,使用 Project 窗口转到 project-root/build/reports/profile/ 目录(如图 1 中所示)。

  5. 右键点击 profile-timestamp.html 文件,然后依次选择 Open in Browser > Default。报告应与图 2 中显示的类似。您可以查看报告中的每个标签以了解您的编译版本,例如,Task Execution 标签显示了 Gradle 执行各个编译任务所花费的时间。

    图 2. 在浏览器中查看报告。

  6. 可选:在对您的项目或编译配置进行任何更改之前,请重复第 3 步中的命令,但请忽略 --rerun-tasks 标记。由于 Gradle 会尝试通过不重复执行输入内容未发生更改的任务(这些任务在报告的 Task Execution 标签中标记为 UP-TO-DATE,如图 3 中所示)来节省时间,您可以确定哪些任务在不必要的时间执行了工作。例如,如果 :app:processDevUniversalDebugManifest 未标记为 UP-TO-DATE,则可能表明您的编译配置会随每次编译动态更新清单文件。不过,有些任务(例如 :app:checkDevDebugManifest)需要在每次编译版本中都运行。

    图 3. 查看任务执行结果。

现在,您已经有了一份编译分析报告,可以通过查看报告中每个标签下的信息发现优化机会。有些编译设置需要进行实验,因为其优势在不同项目和工作站之间可能有所不同。例如,包含大型代码库的项目可能会受益于使用 ProGuard 移除不用的代码和压缩 APK 大小。但是,较小的项目则可能会从完全停用 ProGuard 中受益。此外,增加 Gradle 堆的大小(使用 org.gradle.jvmargs)可能会对内存较小的机器的性能产生负面影响。

对编译配置进行更改后,请重复上述步骤并生成新的编译分析报告,以观察更改的结果。例如,图 4 显示了同一示例应用在采纳本页面中介绍的一些基本优化后的报告。

图 4. 查看优化编译速度后的新报告。

提示:如需更强大的分析工具,不妨考虑使用 Gradle 的开放源代码分析器