体验飞一般的感觉,将Android 构建速度提升10倍以上

96
呆萌狗和求疵喵
2017.07.18 18:39* 字数 2255

相信很多Android开发者每天都会被Android的构建速度影响到,很多人说我只改了一行代码,又要编译2分钟,还是有人说我什么都没动,却还要等待这么久,严重影响工作效率。好消息是Google IO大会上Android 推出了插件3.0.0版本,并且专门做了一次分享,介绍了新的插件特色,并给他家分享了提升Android构建速度的一些使用tips,按照大神给的tips我专门对我们的项目做了一次优化,结果让我非常惊喜,构建速度提升了10倍以上,本篇文章,就要跟大家分享我的优化之路,以及每一步优化所带来的实际效果。
在我们谈论优化措施之前,我们先来看看优化之前项目的build性能

./gradlew clean app:assembleDebug --profile

我们使用这个命令执行full-build(从头创建)的过程,来衡量build的性能。


image.png

从profile报告里可以看到,没有优化之前我们项目执行一次full-build的时间是2分26秒,不是每次都是这么多,但是相差不大。接下来开始我们的优化之旅。

1.升级Android Gradle Plugin 至3.0.0

Android gradle plugin 3.0.0版本更新了很多新的功能,已解决了很多性能问题。升级3.0.0时需要同时升级gradle版本和添加一个maven库.

首先升级gradle-wrapper.properties中的distributionUrl

distributionUrl=\
  https\://services.gradle.org/distributions/gradle-4.0-milestone-1-all.zip

同时添加google提供的maven库地址

buildscript {
    repositories {
        ...
        // You need to add the following repository to download the
        // new plugin.
        maven {
          url 'https://maven.google.com'
        }
    }

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

当然升级过程并不仅仅只有这两步,剩下的过程中你需要解决你在sync当中遇到的问题,因为gradle升级了大版本,有一些新的功能使用变化。我就说说我所解决的问题:

  1. 第一个问题是依赖使用方式的变化,3.0.0中引入了implementation 和api两种新的依赖方式,至于这两种方式的区别后面会讲,新的插件会强制依赖本地其他module时使用implementation方式
  2. 第二个问题是新的插件和最新的Butterknife插件8.7+版本会冲突,编译时会报错

    Unable to find method 'com.android.build.gradle.api.BaseVariant.getOutputs()Ljava/util/List

    github上已经有了这个issue,暂时的解决办法是降至8.4版本

  3. 第三个问题,我们项目中有用到BlockCanaryExt插件,这个插件也会编译报错,先注释掉,等待作者解决。并且我们观察到BlockCanary插件会严重影响我们的编译效率,光是插件任务的耗时就是在30s以上,因此我们如果能去掉或者进行条件编译的话,应该会优化不小。


image.png

来看下,在升级Android Gradle plugin ,以及移除了BlockCanayEx插件之后,我们的full-build性能:


image.png

可以看到编译时间下降了几乎有一分钟。这其中就包括去除BlockCanary插件之后优化的时间


image.png

2. 避免Legacy Multidex

Google针对64k方法数的问题推出了MultiDex的解决方法,但是不同的api版本上,multidex的做法是不一样的,在api21以上,因为Android采用了新的运行时ART,会在安装的时候将所有的classesN.dex合并成一个.oat包。你需要做的就是在编译脚本中加一行 multiDexEnabled true。但是在api以下,你需要引入multidex support library. 而且在编译过程中,编译脚本要话很长时间决定哪些class要放入primary dex中,哪些放入secondary dex中。在api21以下,这叫做legacy multidex。
开发中,我们可以避免legacy multidex带来的编译性能消耗


image.png

我们引入新的product flavor: development.因此我们的编译方式就变成了:

./gradlew clean app:assembleDevelopmentDebug --profile


image.png

编译时间又下降了接近40s,形势喜人。

3. 尽量少的引入资源


image.png

这样我们在开发环境下只引入英文资源和xxhdpi下的资源,减小打包时间,从下面的profile中可以看到,full-build时间减少了1秒,还是有一定效果的。


image.png

4. 禁用Png cruncher

aapt会对你的png做处理,让他们的size变得更小,这一步叫做png-cruncher,减小apk的体积。但是在开发模式下,这些并不重要,因此在开发模式下我们可以禁止png cruncher


image.png

devBuild这个project属性需要我们在build的时候穿进去

./gradlew clean app:assembleDevelopmentDebug -PdebBuild --profile

如果是通过Android studio 进行编译,可以在preferences里面指定


image.png

优化之后的build时间几乎没变,但是略微降了1秒


image.png

5. 避免 crashLytics 的alwaysUpdateBuildId

James还提到了crashLytics潜在的影响编译时间的问题,说在应用插件fabric后,每次编译crashLytics都会生成新的buildId.导致编译变慢,我们的项目也用到了crashLytics, 因此有必要进行优化

buildTypes {
        debug {
            signingConfig signingConfigs.debug
            ext.alwaysUpdateBuildId = false
        }
}

优化之后的效果很明显,full-build时间下降了6s左右, 看来CrashLytics确实影响了编译性能


image.png

6. 使用gradle.cache

gradle3.5版本之后,推出了新的cache机制,和Android studio2.3版本引入的BuildCache不一样,这个新的cache机制会缓存每个任务的输出,而build cache只会pre-dexed的外部库。Google暂时还没有对这个机制做过多的优化,但是建议我们先开启这个机制。

org.gradle.caching = true

来看看开启了cache机制的profile


image.png

在开启了cache之后,我发现我们的build时间确实减少了,但是好像没有想象中的多,难道cache机制没有生效吗?

7. 禁用动态的BuildConfig

至此,能做的优化措施大部分都做了,我们也取得了相当不错的效果,full-build时间从2m25s下降为34s,超过4x的速度提升。但同时我发现一个现象,如果我改一行代码或者资源,执行一次增量build

./gradlew app:assembleDevelopmentDebug -PdevBuild --profile

耗时也要16s时间,这就很奇怪了,理论上来说,不该一行代码,应该秒编啊,为了搞清楚这个问题,我们加入--info编译选项,找到更多的信息

./gradlew app:assembleDeveopmentDebug -PdevBuild --info | grep -A 3 Executing

加上--info选项时,如果执行了某个task,会解释其原因,为什么会执行,我们可以根据Executing关键字来找到编译慢的原因


image.png


发现generateXXXBuildConfig 和XXXjavaWithjavac任务被执行,原因是buildConfig的itemValues发生了变化。目光转向build.gradle

buildConfigField "long", "BUILD_TIMESTAMP", getTimeStamp() + "L"

我们对BuildConfig中的一项输入做了动态变化,getTimeStamp会获取当前的时间,导致每次build时这个值都会发生变化。当然在release环境下,这个值确实有用,但是在开发中,就没必要了。它会导致重新生成BuildConfig.java, 继而导致 javac, dex, package,sign等一系列的任务重新执行,需要对其优化

buildConfigField "long", "BUILD_TIMESTAMP",
                        project.hasProperty("devBuild")?"000000000":getTimeStamp() + "L"

这样如果是开发环境,每次这个值都是一样的。再来看看优化之后的profile


image.png

full-build的时间居然达到了惊人的12s, 这个时候再来运行增量编译,不改一行代码。


image.png

1.8s,这才是我们想要的、正常的效果。

因此要避免在开发环境下使用动态的BuildConfig选项,因为那样会影响的编译速度和开发效率,不仅仅是BuildConfig,还有versionCode,动态的versionCode会引起AndroidManifest文件的修改,继而引起资源文件的重新打包,影响编译效率。

简直神乎其技,通过7步优化,我们将full-build的时间从146s降低到了12s,优化之后速度提升了12倍,飞一般的构建感觉即将到来,就是这个feel,倍儿爽!


image.png

Google IO 分享上还有提到其他的优化tips,包括禁用multi-APK,使用instant run等, multi-APK我们并没有用到,instant run也没有用,就没有给大家详细阐述。感兴趣的可以去youtube上学习。

分享的下部分是负责AS构建系统的Jerome分享3.0插件带来的变化,以及这些变化如何影响Android构建的速度,我会在下篇文章中给大家介绍,希望大家喜欢,大家也可以关注我的公众号,随时关注我的新分享


呆萌狗和求疵喵
码农日记