Android 组件化开发原理和配置
在Application的不断发展过程中,我们开发者要不断地增加新特性。更多的代码就意味着更长的build时间和更长的增量build时间。在工程较大的项目中,build时间要占到10%~15%的工作时间。这不仅是浪费时间,也是测试驱动工作方式(TDD)比较困难的原因。
把Application分成多个modules可以解决这个问题。在根据功能、层级或者其他方式进行module切分Application之前,我进行了一些试验并收集一些数据。这篇文章就是分享一下我收集的实验数据。
在开始实验之前,我们先了解下理论知识,解释一下做什么可以减少增量build时间。
理论知识
我们创建一个Android Application,至少要包含一个application module,并且在build.gradle中设置application 的gradle 插件:
apply plugin: 'com.android.application'
当编译这种module时将生成.apk文件。
一个application module不可以依赖另一个application module,只可以依赖library,就是配置 gradle library 插件的module:
apply plugin: 'com.android.library'
发布这样的module时,得到的将是一个.aar文件,与.jar文件相比,aar文件可以包含一些android相关的东西:比如资源文件和manifest文件。
编译一个application module或者library文件,大致可以分为gradle task代表的五个阶段:
-
Preparation of dependecies 在这个阶段gradle检测module依赖的所有library是否ready。如果这个module依赖于另一个module,则另一个module也要被编译。
-
** Merging resources and processing Manifest** 在这个阶段之后,资源和Manifest文件被打包。
-
** Compiling** 在这个阶段处理编译器的注解,源码被编译成字节码。如果使用了AspectJ,也是在这个阶段进行处理。
-
Postprocessing 所有带 “transform”前缀的task都是这个阶段进行处理的。其中比较重要的是“transformClassesWithMultidexlist”和“transformClassesWithDex”,这个阶段生成.dex文件。
-
Packaging and publishing 这个阶段library生成.aar文件,application生成.apk文件。
我们的目标是降低增量build时间。是实验中很难提高所有阶段的速度,所以我们决定集中注意力在最耗时的阶段。对于只有一个module的project来说,第三和第四个阶段是最耗时的,第二个阶段有时也比较耗时, 但如果java文件没有改动,则增量build常快。
我们知道,gradle在输入不一致时才运行task,当没有改动时也不重新build module,根据这个理论我们可以假设:
多组件project增量build比较快,是因为只有改动过的module才需要重新编译
我们来验证一下是否正确!
实验
Project使用gradle 2.2.2 版本,支持的最低版本是Android 15,但是这已经足够覆盖大部分Android设备了。为了让Project更真实,所有的module都依赖butterknife
,因为正常情况下,所有的project都会依赖其他library。
Application module命名为 "app", 其他library命名为"app2", "app3"等等。
每个module有大约100个package,15000 class, 90000个方法,这样大概有两个dex文件,或者三个。所以的方法保存在 .apk文件中,关闭minifying和shrinking。
所有的测试可以使用gradle 的profiler,只需要在命令之后加“ --profile”即可:
./gradle assembleDebug --profile
最终生成一个.html文件。
每个过程我们重复了4~15次,确保结果的正取性。
java代码自动生成
徒手写15000个class太耗时,所以我用python写了一个代码生成脚本。脚本地址:Gist。下边是生成代码的一个图解:
pure java module
我没有给纯java代码module做实验。但是我认为这种情况下会更快一些,build过程中,编译任务较少如没有资源合并任务时,耗时较少。
如果你对纯代码module比较感兴趣,可以在下边评论。或者你可以clone代码并根据需要进行修改:gihub。但是要注意,编译时间与编译环境是有关系的,所以要在自己的编译环境进行多次实验,从而对结果进行比较。
其他
在实验时如果同时执行一些其他任务,会增加build耗时,即使是Android studio同时打开两个project时,编译时间会增加5%~10%。听音乐,看YouTube,或者浏览网页也会增加不少编译时间。当我关掉除了Android studio以外的程序时,编译速度增快了30%。
以下所有的实验中,只开了Android studio和几个必要的网页。
开始实验
初始状态
开始时我建立了包含一个module的project,包含了15000个文件。此时增量build时间是1m 10s。
3个modules
第一步把一个module分为三个module:一个application module和两个library module。Application module依赖于library,两个library相互独立,每个module有大约5000个文件和30000个方法。
如果只有application中有更新,则编译时间是35s,这比初始状态快了30s。但是如果library module有更新,即使另一个library module没有改动,增量build时间会增加到50s,比初始状态多了40s。
我们通过profile 报告来看看一下:
当(“app2”有更新时的增量build,由于module app依赖于 app2,所以也进行来重编译)从上边的图表中我们可以library module耗时较多。我们注意到,对于两个library module,debug和release的task都执行了。gradle执行了两组任务而不是一组。与只有一个module相比这种情况额外耗费了40s。
如果我们可以避免这种情况则编译时间与只有一个module的情况不会变慢,增加一些调整可能还会快于1m 10s。
而且这不是唯一的问题,我们来看一下module依赖:
dependencies {
compile project(path: ':app2')
compile project(path: ':app3')
}
上边的代码有一个问题:如果像上边这样添加依赖,则application依赖于library的release版本,这与Android studio中的设置无关。Gradle Plugin User Guide中有说明:
默认情况下library只发布其release版本,并被所有依赖于library的project依赖,这与project 的build type无关。这是我们即将移除的一个临时限制。
幸运的是,我们可以更改application的版本依赖。
首先,在build.gradle
中增加如下代码,这会使library module同时编译debug版本:
android {
defaultConfig {
defaultPublishConfig 'release'
publishNonDefault true
}
}
第二步, application module设置依赖如下:
dependencies {
debugCompile project(path: ':app2', configuration: "debug")
releaseCompile project(path: ':app2', configuration: "release")
debugCompile project(path: ':app3', configuration: "debug")
releaseCompile project(path: ':app3', configuration: "release")
}
此时debug版本的application将会依赖debug版本的library。我们在app2中做一些改动,重编译看一下效果:
最明显的区别是没有了 :app2:packageReleaseJarArtifact
,这节约了大约15秒。再加上一些其他的更新,最终时间为1m 32s,这比之前快来18秒,但是依然比初始配置慢22s。如果只有application module中有更新,则编译时间与之前差不多34s vs 35s。
我没有找到两个build type(release 和debug)都编译的合理解释,只希望之前提到的gradle限制移除后,这个问题可以同时解决,在这里可以找到一些issue: AOSP Issue Tracker。我也计划花一些时间找到解决问题的其他方法,一个可能的办法是当编译debug版本时不执行所有的release task。
5个module
很明显,编译时间依赖于代码数量。如果减少一半的代码,则编译时间可能会降低为之前的一半。如果把3个module变为原来的5个module,编译时间将会减少大约40%左右。
如果只有application中有代码更新,则增量build时间只有24s,如果library中有代码更新,则增量build时间为50s,这比初始的1m 10s快了,但是我还有一些其他的技巧。
减少Application module的大小
不论哪一个module发生了更改,application module每次都会重新编译。所以减小Application module很有必要。理想情况下,可以使用一个单独对module整合整个应用,可以提供启动界面,因为启动界面通常要依赖很多特性。
这就是3+1,5+1配置的思路,在两种配置中,都有一个较小的application module依赖于3个或者5个module,其他module相互独立,并且大小相同,实验结果如下:
我们可以看到有效降低了增量build时间。即使是library module中发生了更新,5+1的配置也比初始状态快了一半。这是一个不错的进步。
关于Butterknife
这里需要说明一下,为什么要加入butterknife 依赖。
在初始配置中 incremental compilation耗时1m 10s中的45s,但是如果移除butterknife则编译只需要15s,提速三倍。整个不包含butterknife的增量build过程只需要40s。
这是library的问题吗?
这是因为project中不允许Annotation processors 的增量build。可以在这里看到相关的issue Gradle Jira, AOSP Issue Tracker,design docs。有一个comment是这样的:
Annotation precessors 不支持java增量build,它依赖于gradle中的变化。
我们在配置了com.neenbedankt.android-apt
的project中禁用它,因此它不是一个重要的问题。
个人觉得不应该在整个工程中移除Annotation processors,Dagger和Butterknife非常有用,但是可以设置某些module不依赖它们,这样可以提高增量build速度。
其他技巧-设置API版本
编译并不是唯一影响build时间的因素,生成DEX文件同样耗时。特别是当超过DEX 的限制时。使用multidex配置会增加build时间,build时需要判断class需要放置到哪个DEX文件中。参考 Android Studio documentation 中关于Android运行时Android系统对于用于多个dex的application的处理:
Android 5.0 之后 ART支持从APK文件中加载DEX文件。ART会在app安装时提前编译,扫描
classN.dex
文件,编译为单独的.oat
文件。
这减少了build时间,原因是每个module生成自己的DEX文件,然后不经修改直接放置到APK中。如果我们看一下build过程,则可以看到transformClassesWithMultidexlist
并不会执行。编译过程速度加快,在 这里可以查看更多信息。
更快的编译配置
配置在debug中使用API 21,我测试了5+1 的情况,结果如下:
即使是library中改动增量build时间只有17s。但是当所有module依赖于一个module,当被依赖的module更新时,增量build时间从17s增加到42s。
使用测试驱动方式(Test Driven Way)开发 Library Module
测试驱动式(TDD)开发的难点就在于single module的build时间。TDD需要不断运行test,在一分钟内多次运行test是基本的实践方式。但是当build需要耗时一分钟或者更长时,则TDD没有很好的效果。
在最终的配置中运行一个module只需要耗时9s,所以可以不断运行test。
总结
首先,最重要的,组件化project可以显著提高build速度,但是需要合理配置。
其次,如果module拆分不合理,则build时间可能增加,因为gradle会build release和debug两个版本。
再次,组件化用于TDD非常有效,因为一个小的moudle build时间非常短。
第四, 同时执行很多任务很减慢build速度,所以可以适当提升硬件配置。
总结项目地址:GitHub
本文为译文,原文地址How modularisation affects build time of an Android application
欢迎关注公众号wutongke,每天推送移动开发前沿技术文章:
wutongke推荐阅读: