盒子
盒子
文章目录
  1. 开门撞山:
  2. 正餐:
    1. 技巧1:使用最新版本的gradle插件
    2. 技巧2:避免使用legacy multidex
    3. 技巧3:关闭multi apk
    4. 技巧4:使用最少的资源打包
    5. 技巧5:关闭aapt对png的优化
    6. 技巧6:使用instant run
    7. 技巧7:避免写一些不必要的更改
    8. 技巧8:类库不要使用动态的版本
    9. 技巧9:查看jvm使用的内存是否合适
    10. 技巧10:打开gradle新的缓存机制
    11. 技巧11:使用新的依赖机制
    12. 实例

[大餐]加快gradle构建速度

声明:博文未经授权一律不允转载

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

开门撞山:

AS中gradle构建一般可以分为三种,

1.从0开始构建,Full Build;

2.修改源代码后构建,之前构建过,Incremental build java change;

3.修改资源文件后构建,之前构建过,Incremental build resource change。

从这三方面其实都可以有切入点来进行提速,gradle插件3.0做了大改,优化了很多东西,包括gradle依赖系统以及缓存系统,这些都是直接影响构建速度的因素,整理了有10+个小技巧可以加快我们的构建速度,下面请听我一一道来。

正餐:

技巧1:使用最新版本的gradle插件

buildscript {
    repositories {
        maven { url 'https://maven.google.com' }
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0-alpha1'
    }
    ...
}

看到google.com之后,相信你应该知道要拿个“梯子”的,所以我说升级as最方便嘛,而现在gradle的版本已经更新到alpha1+了(现在是2017年06月12日),根据谷歌官方只要是3.0+,也就是以2017I/O大会的版本开始就可以。

技巧2:避免使用legacy multidex

当app方法数量超过64k的时候就会编译失败,早期版本的构建系统按如下方式报告这一错误:

Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536

较新版本的 Android 构建系统虽然显示的错误不同,但指示的是同一问题:

trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.

使用multidex分包处理是谷歌官方给我们提供的解决方案,由于分包被内置支持在5.0以及之后,当我们需要兼容的版本跨越5.0也就是api21的时候,比如说14–23,那么我们需要使用兼容库:

com.android.support:multidex:1.0.0

使用兼容库能让我们的低版本的也进行分包处理,具体如何操作gradle需要配置什么Application需要如何设置等不是本文重点,请各位自行谷歌、百度。

根据谷歌官方描述,使用兼容库会明显拖慢构建,也就是如果你的调试机器版本低于5.0,又进行了分包处理,那么当你点击那个绿色构建按钮▶或者使用命令行gradlew构建的时候,那你可以先去吃一顿饭先了(Just kidding!)。

所以我们在开发调试的时候尽量使用高版本的手机5.0+,等功能完成了,后面做兼容低版本的时候再使用低版本的手机进行调试。可能有人会说,我在开发功能的时候如果不实时调试低版本的兼容问题后期可能有问题,关于这一点,我想说的是一点是经验问题,经验足的人就是能够知道你这段代码写出来可能的结果,这是内功,不可言状,却又确实存在的能力,我见过这样的人,你没写出来他就能够知道运行的结果,另一点是我们需要相信google的兼容团队给我们的兼容方案是能够解决绝大部分的兼容问题的,我指的兼容方案是指使用appcompat、support等之类的兼容库。

技巧3:关闭multi apk

如果你的项目需要编译出多个density或者多个abi(CPU架构)的APK,那么这也是开发构建的时候不需要做的操作,因为在你手上调试的机器只是一种density一种abi,因此我们在构建的时候可以将multi-apk的构建给去除。可以进行如下配置:

splits {
    density {
        //enable true
        enable false
        ...
    }
    abi {
        //enable true
        enable false
        ...
    }
}

但是显然这趟配置影响全局配置,后期还可能忘记改回来,更灵活一点的方式可以这样:

android {
    ...
    splits {
        ...
    }
    if(project.hasProperty('HaloDevBuild')){
        splits.abi.enable = false;
        splits.density.enable = false;
    }
    ...
}

或者,可以更加细致一点:

android {
    ...
    splits {
        ...
    }
    if(project.hasProperty('HaloDevBuild')){
        splits.density.enable = false
        splits.abi {
            enable true
            reset()
            include 'arm64-v8a' //select ABIs to build APKs for
            universalApk false //whether generate an additional APK that contains all the ABIs
        }
        aaptOptions.cruncherEnabled = false
    }
    ...
}

注意project没有s,不是projects。并且这个判断的代码必须置于splits块的下面,否则就可能设置完之后就又被覆盖回来了
“HaloDevBuild”属性的字符串我们可以自定义,在gradle写好这个判断之后,我们就可以在命令行编译了,命令如下:

./gradlew 项目名:变体 -PHaloDevBuild

比如:

gradlew app:assembleDevelopmentDebug -PHaloDevBuild

又或者可以构建顺带安装了:

gradlew demo:installdevelopmentDebug -PHaloDevBuild

-P之后表示要传递的属性。当然我们如果需要在as中点击绿色构建按钮▶编译,那就要设置一下每次都要传入属性:

demo0

另外,我们可以这样获取Android手机CPU/ABI型号:

String CPU_ABI = android.os.Build.CPU_ABI;//过时了
String CPU_ABIs[] = android.os.Build.SUPPORTED_ABIS;

技巧4:使用最少的资源打包

和上面的思路一样,我们开发调试的时候只需要用到一种语言和一套资源文件。所以我们打包的时候不需要将所有的东西都放到这个调试的apk中。我们可以采用定义变体的方式来完成,注意下面的resConfigs属性指定了要保留的素材资源文件和语言资源文件。

android {
    ...
    flavorDimensions "dev"
    productFlavors {
        development {
            dimension "dev"
            resConfigs "en", "xxhdpi"
            ...
        }
    }
}

技巧5:关闭aapt对png的优化

aapt打包时候对资源图片的处理也会耗时,这里主要针对PNG,我们可以暂时关闭这种处理,还是用到上面的技巧,gradle配置如下:

android {
    ...
    if(projects.hasProperty('HaloDevBuild')){
        splits.abi.enable = false;
        splits.density.enable = false;
        aaptOptions.cruncherEnabled = false
    }
    ...
}

当然如果我们将PNG转为webP也可以是一种优化方案。as2.3+支持直接转换,右键直接png图片选则convert to webP。当然webP是有版本要求的,所以请注意其用法,只有4.0及以上的android手机支持webP,不过当然想要低版本的支持webP可以引入以下这个裤,另,更多webP详情非本文重点请自行百度了解。

https://github.com/alexey-pelykh/webp-android-backport

技巧6:使用instant run

多使用instant run,根据谷歌的描述,as3.0和2.0的instant run有巨大的差别,做了很多的优化,instant run要求调试的机器必须要在api21或者以上,而不是你的项目最小版本要在21或者以上。还有一点就是使用instant run的时候,如果我们首次构建是从0开始的,那么会比不使用instant run慢一点,因为需要为后面的instant run的冷热交换去准备一些额外的东西,然而如果后面一直都使用instant run,那么首次构建的延缓的时间就能很快被挽回来。

技巧7:避免写一些不必要的更改

有些开发者为了方便会直接使用时间戳来作为每次构建的版本号(或者是自己的一套计算算法),像这样:

def buildDataTime = new Date().format('yyMMddHHmm').toInteger()
android {
    ...
    defaultConfig {
        versionCode buildDateTime
        ...
    }
}

这个一定程度上方便了我们,但是对于gradle构建系统来说,这不是好事。由于每次版本号都有修改,这就直接导致清单文件Manifest的更改。而清单文件的修改势必会趋势构建系统重新审视整个项目情况,这会增加很多额外的时间,这在开发调试的时候根本没有必要。

还是利用上面的技巧,我们可以这样:

def buildDataTime = project.hasProperty('HaloDevBuild')? 100 : new Date().format('yyMMddHHmm').toInteger()
android {
    ...
    defaultConfig {
        versionCode buildDateTime
        ...
    }
}

技巧8:类库不要使用动态的版本

项目中可能会这样配置:

dependencies {
    ...
    compile 'com.android.support:appcompat-v7:+'
}

这个加号+,导致gradle每次构建都要检查新的版本。即使你希望每次都用最新但是还是不好,因为库可能已经有很大的变化了,但是你是基于旧版开发的,所以为了安全不要这样设置。其实这和我们coding还是一样的道理,我们码代码的时候肯定会考虑如何能让别人的修改不影响到我们自己的,因此这个加号使不得,使不得。

技巧9:查看jvm使用的内存是否合适

demo1

我一般设置为2g,这个根据项目的不同可能会有不同效果,所以可以试着稍微调整一下。

技巧10:打开gradle新的缓存机制

我们可以通过一个设置使用这种新的缓存机制,如下:

//在gradle.properties中+一行
org.gradle.caching=true

文章开头也提到过,Gradle 3.5(现在20170612的gradle版本已经4.0了)新的机制,可以缓存所有任务的输出。这个和as2.3时候介绍的build 缓存不一样,build 缓存只缓存预dex的外部libs。而任何时候的构建缓存其实都是可复用的,比如切换分支之后,还可以用其他分支时候的构建缓存。

当然,根据谷歌官方描述,这个设置的背后还有很多需要完善的地方,使用的同时,我们可以有更多后期优化的期待。

技巧11:使用新的依赖机制

按照官方的描述,gradle3.0之前,构建系统的依赖关系被认为是一种完全错误的方式,举一个例子:

app依赖了库libX,库libX又依赖库libY。

app:

compile 'libX'

libX:

compile 'libY'

如果在app中调用了libY的方法,这个时候如果libY中的方法做了改变,那么由于libY被和libX和app都有依赖被依赖关系,这时候如果构建libY势必会导致libX和app也重新构建。

这应该是个完全错误的依赖方式,正确的方式应该是既然app只依赖了libX那就不应该知道libY中的api,然而这在构建系统3.0(gradle插件)之前都是这样存在的,因此3.0做了改变,引入了新的依赖形式,api和implementation来替换compile。

  • implementation:表示我依赖你,但是你的api不会被我重新分享出去了。
  • api:表示我依赖你,但是你的api会被我重新分享出去了。

那么在看上面的例子就变成了:

app:

implementation 'libX'

libX:

api 'libY'

由于存在之前的这种依赖传递的错误机制,因此我们项目中很可能就出现了这种依赖传递之后导致一个小修改就把所有库都构建一遍的情况,因此我们花一些时间将一些库的compile都替换成api和impementation对我们是有好处的,一来有效缩短构建时间,而来真正搞清楚项目中依赖的情况,那些api需要依赖传递,哪些不需要。

当然我们还是可以继续使用compile,不过这种方式已经是过时的了,我们只要更新了最新插件,那么新建了项目就会发现所有的compile都会被换成了implementation。相关的修改还有androidTestImplementation和testImplementation,对应的是原来的androidTestCompile和testCompile。

实例

根据上述技巧,我写了一个小demo:

https://github.com/halohoop/SpeedUpGradleBuild2

  • Stay hungry stay foolish!
支持一下
扫一扫,支持Halohoop