应用程序 构建模块
by Nikita Kozlov
由Nikita Kozlov
模块化如何加快您的Android应用的构建时间 (How modularization can speed up your Android app’s built time)
Developers will continue to add new features throughout an application’s lifetime. More code means not only longer build times — it means longer incremental build time.
开发人员将在应用程序的整个生命周期中继续添加新功能。 更多的代码不仅意味着更长的构建时间,还意味着更长的增量构建时间。
For teams with big projects, waiting for new builds could eventually take up 10-15% of their workday. This not only wastes precious developer time — it also makes test-driven development extremely tedious, which hurts overall code quality.
对于具有大型项目的团队,等待新版本最终可能会占用其工作日的10-15%。 这不仅浪费了宝贵的开发人员时间,而且还使测试驱动的开发变得非常乏味,从而损害了整体代码质量。
Splitting an application into modules could be a solution to this problem. I was tempted to just split our codebase by feature, by layer, or in some other quick and obvious way. But first, I decided to create an experiment and collect some data, so I could make a better-informed decision. This article will explore the results I collected and my findings.
将应用程序拆分为模块可以解决此问题。 我很想按照功能,层或其他一些快速而明显的方式拆分我们的代码库。 但首先,我决定创建一个实验并收集一些数据,以便做出更明智的决定。 本文将探讨我收集的结果和发现。
Before we dive into my experiment, I’d like to talk about the theory behind all this, and explain how we can decrease incremental build time.
在进行实验之前,我想谈谈所有这些背后的理论,并说明如何减少构建时间。
一点理论 (A bit of theory)
When you create an Android application you have to have at least one application module, it is a module that applied the Gradle application plugin in his build.gradle file:
创建Android应用程序时,您必须至少有一个应用程序模块,该模块是在其build.gradle文件中应用了Gradle应用程序插件的模块:
apply plugin: 'com.android.application'
As a result of building this module you will get an .APK file.
构建此模块的结果是,您将获得一个.APK文件。
One application module cannot depend on another. It can only depend on a library. It is the module that applied the Gradle library plugin:
一个应用程序模块不能依赖于另一个。 它只能依赖于库。 这是应用Gradle库插件的模块:
apply plugin: 'com.android.library'
As a result of publishing such a module you will get an .AAR file (Android Archive Library). Comparing to .JAR file .AAR has some Android-related things, for example, resources and Manifest.
发布此类模块的结果是,您将获得一个.AAR文件(Android存档库)。 与.JAR文件相比.AAR具有一些与Android有关的东西,例如资源和清单。
Building process for any library or application module can be roughly split into five phases, that could be represented with a certain Gradle tasks:
任何库或应用程序模块的构建过程可以大致分为五个阶段,可以用某些Gradle任务来表示:
Preparation of dependencies. During this phase Gradle check that all libraries this module depends on are ready. If this module depends on another one, that module would be built as well.
准备依赖项。 在此阶段,Gradle检查此模块依赖的所有库是否已就绪。 如果该模块依赖于另一个模块,则也将构建该模块。
Merging resources and processing Manifest. After this phase resources and Manifest are ready to be packaged in the result file.
合并资源并处理清单。 在此阶段之后,可以将资源和清单准备好打包到结果文件中。
Compiling. This phase started with Annotation Processors, in case you use them. Then source code is compiled into byte code. If you are using AspectJ, weaving also happens here.
编译中。 如果您使用注释处理器,则此阶段从注释处理器开始。 然后将源代码编译为字节代码。 如果您使用的是AspectJ,则此处也会进行编织。
Postprocessing. All Gradle tasks with a “transform” prefix are part of this phase. Most important ones are:
transformClassesWithMultidexlist
andtransformClassesWithDex
. They produce .DEX files.后期处理。 所有带有“ transform”前缀的Gradle任务都是该阶段的一部分。 最重要的是:
transformClassesWithMultidexlist
和transformClassesWithDex
。 他们产生.DEX文件。Packaging and publishing. For libraries this stage means creating an .AAR file in the end, for applications — .APK.
包装和出版。 对于图书馆,此阶段意味着最后为应用程序-.APK创建.AAR文件。
As I mentioned, our goal is to minimize incremental build time. It’s difficult to speed up all the phases in one experiment, so I decided to focus on the longest ones. For a project with a single module, those were the compiling and postprocessing phases. The merging resources and processing manifest phase can also sometimes be time-consuming, but if the Java code is untouched, than incremental build is quite fast.
如前所述,我们的目标是最大程度地减少构建时间。 在一个实验中很难加快所有阶段的速度,因此我决定专注于最长的阶段。 对于具有单个模块的项目,这些阶段是编译和后处理阶段。 合并资源和处理清单阶段有时也很耗时,但是如果不修改Java代码,则增量构建会非常快。
We all know, that Gradle runs task in consequent builds only if input isn’t the same. It also doesn’t rebuild a module if it wasn’t changed. That leads to the following hypothesis:
我们都知道,只有在输入不相同的情况下,Gradle才会在后续版本中运行任务。 如果未更改,它也不会重建模块。 这导致以下假设:
“The Incremental build time for a project with multiple modules is faster than for a single-module project, because only modified modules are recompiled.”
“具有多个模块的项目的增量构建时间比单模块项目要快,因为仅重新编译了修改后的模块。”
All right, lets find out whether this is true!
好吧,让我们找出这是否正确!
实验设置 (Experiment Setup)
Project uses Gradle plugins v2.2.2. Minimal Android SDK is 15, that covers most of the devices according to API level dashboard. All modules have a dependency on Butterknife, to make project a bit more “alive”, since all projects has external dependencies.
项目使用Gradle插件v2.2.2。 最小的Android SDK为15,根据API级仪表板 ,它涵盖了大多数设备。 所有模块都依赖于Butterknife ,从而使项目更加“活跃”,因为所有项目都具有外部依赖关系。
In all variants application module is called “app”, while library modules are called “app2”, “app3” and so on.
在所有变体中,应用程序模块称为“ app” ,而库模块称为“ app2” , “ app3” ,依此类推。
Each variant has in total around 100 packages 15,000 classes 90,000 methods. Assembled that is two DEX files, almost three. Generated code is dummy, to keep all the methods in the .APK file, minifying and shrinking were disabled.
每个变体总共有大约100个包15,000个类90,000个方法。 组装的是两个DEX文件,几乎是三个。 生成的代码是虚拟的,为了将所有方法都保留在.APK文件中,禁用了缩小和缩小功能。
All measurements was done with Gradle build-in profiler. To use it you just need to add “--profile” in the end of the command, like this:
所有测量均使用Gradle内置轮廓仪完成。 要使用它,您只需要在命令末尾添加“ --profile”,如下所示:
./gradlew assembleDebug --profile
As a result you will get a .HTML file with measurements.
结果,您将获得一个带有测量值的.HTML文件。
For each setup I repeated each measurement 4 to 15 times, to make sure that my results were reproducible.
对于每种设置,我将每次测量重复4至15次,以确保我的结果可重复。
Java代码生成器 (Java Code Generator)
Writing all 15,000 classes by hand is time consuming, so I wrote a simple code generator in Python. It is available in Gist. Below you can find a schema of the code it generates.
手工编写所有15,000个类都很耗时,因此我用Python编写了一个简单的代码生成器。 它在Gist中可用。 在下面,您可以找到其生成的代码的架构。
As you can see, every next generated method called the previous one, and every first method of the next class called the last method of the previous class. That makes code more coupled, and increased compilation time.
如您所见,每个下一个生成的方法称为上一个方法,而下一个类的每个第一个方法称为上一个类的最后一个方法。 这使得代码更加耦合,并增加了编译时间。
纯Java模块 (Pure Java modules)
I didn’t make a special experiments for pure Java modules. But I played with them a bit, and I can say that usually they are faster then Android ones. That happens because during build less tasks are executed, for example, there is no resources to merge.
我没有对纯Java模块进行特殊的实验。 但是我和他们玩了一点,我可以说通常他们比Android的快。 之所以会发生这种情况,是因为在构建期间执行的任务较少,例如,没有资源可以合并。
If you’re interested in results for pure Java modules, please write a comment. Or you can clone the project from GitHub and modify it according to your needs. But don’t forget that actual results depend on hardware. To be able to compare your results, please repeat some experiments in your environment as well.
如果您对纯Java模块的结果感兴趣,请写评论。 或者,您可以从GitHub克隆项目并根据需要进行修改。 但是请不要忘记,实际结果取决于硬件。 为了能够比较您的结果,请在您的环境中也重复一些实验。
到处都是小损失 (Small losses here and there)
It’s no surprise that doing something in parallel does slow down the build. Even having a second project open in Android Studio can make build 5–10% slower. Listening to music, watching YouTube, or browsing the internet increases the build time a lot! I personally saw a speeding up of a build process by up to 30% after just closing everything except Android Studio.
并行执行操作会减慢构建速度,这也就不足为奇了。 即使在Android Studio中打开第二个项目,也会使构建速度降低5-10%。 听音乐,看YouTube或浏览互联网会大大增加构建时间! 在关闭Android Studio以外的所有功能后,我个人看到构建过程最多可加快30%。
All experiments were done with only necessary browser tabs and a single Android Studio project open.
所有实验都是在仅必要的浏览器选项卡和单个Android Studio项目打开的情况下完成的。
让我们做实验 (Let’s do the experiment)
初始状态 (Initial State)
As a starting point I took a project with a single module that contains all 15 000 classes. For this setup incremental build time is 1m 10s.
首先,我参加了一个包含单个模块的项目,该模块包含所有15 000个类。 对于此设置,增量构建时间为1m 10s 。
3个模块 (3 Modules)
First step is splitting one module into three: one application module and two library ones. Application module depends on libraries, but library modules are independent from one another. Each module has about 5 000 classes and 30 000 methods.
第一步是将一个模块分为三个模块:一个应用程序模块和两个库模块。 应用程序模块依赖于库,但是库模块彼此独立。 每个模块大约有5 000个类和30 000个方法。
If changes are made only in application module, then build time is about 35 seconds. Almost 30 seconds win compare to initial state. But when one of library module is changed, even if application module is untouched, incremental build time grows to 1m 50s. 40 seconds longer!
如果仅在应用程序模块中进行更改,则构建时间约为35秒。 与初始状态相比,将近30秒获胜。 但是当更换一个库模块时,即使不修改应用程序模块,增量构建时间也将增长到1m 50s 。 再长40秒!
Let’s take a look into profile report and see what took so long:
让我们看一下配置文件报告,看看花了这么长时间:
In the screenshots above, you can see that most of the time was spent on building the library module. You might also notice that for library modules, both debug and release tasks were executed. Gradle wasted time executing two sets of tasks instead of one! This is why it took an extra 40 seconds longer than the single-module project.
在上面的屏幕截图中,您可以看到大部分时间都花在了构建库模块上。 您可能还会注意到,对于库模块,调试和发布任务均已执行。 Gradle浪费时间执行两组任务,而不是一组! 这就是为什么比单模块项目花费额外的40秒时间的原因。
We can avoid this and make this build even faster than our initial 1m 10s by splitting our code into modules.
通过将代码分成模块,我们可以避免这种情况,并使构建速度比最初的1m 10s还要快。
But that’s not the only problem. Let’s look into how our application module depends on library modules:
但这不是唯一的问题。 让我们来看看如何将我们的 应用程序模块取决于库模块:
dependencies { compile project(path: ':app2') compile project(path: ':app3')}
There’s an important problem in the code above: if a library is added like this, then the application always depends on the release variant, independent of it’s own build type. It also doesn’t matter which variant is chosen in Android Studio. Here’s what the Gradle Plugin User Guide says about all this:
上面的代码中有一个重要的问题:如果这样添加库,则应用程序将始终依赖于发行版本,而与它自己的构建类型无关。 在Android Studio中选择哪个变体也无关紧要。 这是《 Gradle插件用户指南 》关于所有这些内容的内容:
By default a library only publishes its release variant. This variant will be used by all projects referencing the library, no matter which variant they build themselves. This is a temporary limitation due to Gradle limitations that we are working towards removing.
默认情况下,库仅发布其发行版本。 不管它们自己构建哪种变体,所有引用该库的项目都将使用此变体。 由于我们正在努力消除Gradle的限制,因此这是暂时的限制。
Luckily, it’s possible to change the variant our application depends on.
幸运的是,可以更改应用程序所依赖的变体。
First, add the following code to libraries’ build.gradle file. It will allow library to publish debug variant as well:
首先,将以下代码添加到库的build.gradle文件中。 它也将允许库发布调试变量:
android { defaultConfig { defaultPublishConfig 'release' publishNonDefault true }}
Second, application module should depend on library ones like this:
其次,应用程序模块应依赖于这样的库:
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")}
Now the debug variant of our application depends on the debug variant of the libraries, and its release depends on their release. So lets make some changes to the module app2 and rebuild it. After these changes, we can check our profile report again:
现在,我们的应用程序的调试变量取决于调试 库的变体,其发布取决于它们的发布。 因此,让我们对模块app2进行一些更改并重新构建它。 完成这些更改后,我们可以再次检查我们的个人资料报告:
The most significant difference is the absence of :app2:packageReleaseJarArtifact, which saves us about 15 seconds. In addition, the time is reshuffled a bit between rest of the tasks, and we end up with 1m 32s. That is 18 seconds faster then before, but still 22 seconds slower then our initial configuration. For changes only in the application module, build time stays almost the same — 36 seconds vs 35 seconds in our previous configuration.
最显着的区别是缺少:app2:packageReleaseJarArtifact,它为我们节省了约15秒的时间。 此外,其余任务之间的时间也重新调整了一点,结果是1m 32s 。 比以前快18秒,但比我们的初始配置慢22秒。 对于仅在应用程序模块中进行的更改,构建时间几乎保持不变-36 秒 ,而我们之前的配置为35秒 。
Unfortunately I didn’t found a proper explanation for why it builds both flavors. I hope that once this Gradle limitation is resolved, then this problem will just go away as well. The same issue is discussed in AOSP Issue Tracker.
不幸的是,我没有找到适当的解释来解释为什么它同时具有两种风味。 我希望,一旦解决了Gradle的限制,那么这个问题也将消失。 AOSP问题跟踪器中讨论了相同的问题。
I’m also planning to spend some time experimenting with Gradle tasks to find a workaround for this problem. One possible way is to exclude all release tasks for debug builds.
我还计划花一些时间尝试Gradle任务,以找到解决此问题的方法。 一种可能的方法是排除调试版本的所有发布任务。
5个模块 (5 Modules)
Obviously, build time depends on the amount of code. If you reduce amount of code by half then the build should also be roughly two times faster. If instead of 3 modules project is split in 5, then build time should be approximately 40% faster.
显然,构建时间取决于代码量。 如果将代码量减少一半,那么构建速度也应该快大约两倍。 如果将项目分成3个模块而不是3个模块,则构建时间应快40%。
It is almost true.
几乎是真的。
If changes are made only in application module then incremental build time is approximately 24s. For changes in a library module incremental build takes 50s. Compare to initial 1m 10s that is already a win. But I have a few more tricks at my disposal.
如果仅在应用程序模块中进行更改,则增量构建时间约为24 s。 对于库模块中的更改,增量构建需要50 s 。 相比之下,最初的1m 10s已经是一个胜利。 但是我还有其他一些技巧可以使用。
减少应用程序模块的大小 (Reduce Size of The Application Module)
Independently of what module is changed, application module will be recompiled every time. So reducing it’s size makes total sense. Ideally, it should just assemble whole application from a separate modules and might also provide a splash screen, because splash screen often depends on lots of features.
与更改哪个模块无关,应用程序模块每次都会重新编译。 因此,减小它的大小是很有意义的。 理想情况下,它应该仅从单独的模块组装整个应用程序,并且还可能提供启动屏幕,因为启动屏幕通常取决于许多功能。
That is how idea about 3+1 and 5+1 configurations was born. In both cases project has a small application module that depends on 3 and 5 library modules accordingly. All library modules are independent from one another and has equal sizes. Let’s see what that gives us:
这就是关于3 + 1和5 + 1配置的想法诞生的方式。 在这两种情况下,项目都有一个小的应用程序模块,该模块相应地依赖于3和5个库模块。 所有库模块彼此独立,并且大小相等。 让我们看看这能给我们带来什么:
We can observe further drop in incremental build time. Even with changes in library module 5+1 configuration is build almost twice as fast as an initial single-module project. This is a decent progress.
我们可以观察到增量构建时间的进一步减少。 即使更改了库模块5 + 1配置,其构建速度也几乎是初始单模块项目的两倍。 这是一个体面的进步。
为什么项目实际上取决于Butterknife? (Why does project actually depends on Butterknife?)
This is a point where I have to make a confession. There was one very strong reason to add a dependency on Butterknife.
这是我必须承认的一点。 有一个非常强烈的理由要增加对Butterknife的依赖。
In the initial configuration incremental compilation takes 45s out of 1m 10s, but if Butterknife is removed then project is compiled in 15s only! Three times faster! Whole incremental build without Butterknife is 40s.
在初始配置中,增量编译从1m 10s中花费45s ,但是如果移除Butterknife,则仅在15s内编译项目! 快三倍! 没有Butterknife的整个增量构建是40s 。
Is it a problem in the library?
图书馆有问题吗?
As it appeared — no. Without Butterknife project compiles so fast because of actual Java incremental compilation, which is disabled for projects that use Annotation processors. You can find related issues in Gradle Jira, in AOSP Issue Tracker, it is also tracked in Gradle design docs. If you will take a closer look into the issue from ASOP Issue Tracker, one of the comment says:
看起来好像不对。 如果不使用Butterknife,则由于实际的Java增量编译,项目编译会如此之快,而对于使用注释处理器的项目而言,则禁用该编译。 您可以在Gradle Jira的AOSP问题跟踪器中找到相关的问题,也可以在Gradle 设计文档中对其进行跟踪。 如果您从ASOP问题跟踪工具中仔细研究该问题,则其中一项评论说:
“Annotation processors is not yet supported with incremental java compilation. It will depend on changes in Gradle.
“增量Java编译尚不支持注释处理器。 这将取决于Gradle的变化。
We disabled it for project that apply com.neenbedankt.android-apt, so it's no longer a significant issue.”
我们为应用com.neenbedankt.android-apt的项目禁用了它,因此不再是一个重大问题。”
That is why build just becomes slower without a notification.
这就是为什么在没有通知的情况下构建速度变慢的原因。
Personally I won’t remove Annotation processors from the whole project. I find libraries like Dagger and Butterknife useful. But having a couple of modules without them could be a good idea, that would make their builds so much faster!
我个人不会从整个项目中删除注释处理器。 我发现像Dagger和Butterknife之类的库很有用。 但是拥有几个没有它们的模块可能是一个好主意,这将使它们的构建速度更快!
另一个技巧–提高API级别 (One More Trick — Rump Up API Level)
Compilation is not the only thing that slows down build process. Producing .DEX files could also be time consuming. Especially if an application is above DEX Limit. Using multidex configuration increases build time, build system need to decided which classes go to which .DEX file. With introduction of Android Runtime the way how Android OS works with application that has multiple DEX files has changed. This is what Android Studio documentation says about it:
编译不是唯一拖慢构建过程的事情。 生成.DEX文件也可能很耗时。 特别是当应用程序超出DEX限制时。 使用multidex配置会增加构建时间,构建系统需要确定哪些类进入哪个.DEX文件。 随着Android Runtime的引入,Android OS与具有多个DEX文件的应用程序一起工作的方式已经改变。 这是Android Studio文档所说的:
“Android 5.0 (API level 21) and higher uses a runtime called ART which natively supports loading multiple DEX files from APK files. ART performs pre-compilation at app install time which scans for
classesN.dex
files and compiles them into a single.oat
file for execution by the Android device.”“ Android 5.0(API级别21)及更高版本使用了称为ART的运行时,它本身支持从APK文件加载多个DEX文件。 ART在应用安装时执行预编译,该预扫描将扫描
classesN.dex
文件,并将它们编译为单个.oat
文件,以供Android设备执行。”
That leads to decrease in build time. The reason is that each module produces its own DEX files, that are included into APK without modification. If you take a look into the tasks that run during build, you will notice that transformClassesWithMultidexlist
is no longer executed. Also compilation itself became faster. You can find more information and instructions how to make a flavor that uses API 21 here.
这导致构建时间减少。 原因是每个模块都会生成自己的DEX文件,这些文件无需修改即可包含在APK中。 如果查看构建过程中运行的任务,您会注意到transformClassesWithMultidexlist
不再执行。 编译本身也变得更快。 您可以在此处找到有关如何制作使用API 21的风味的更多信息和说明。
实现最快的构建配置。 (Fastest Build Configuration Achieved.)
Using API 21 for debug is an easy gain for every project. I tried it for 5+1 configuration and results were amazing:
对于每个项目,使用API 21进行调试都是一件容易的事。 我尝试将其用于5 + 1配置,结果令人赞叹:
Even for changes in library module incremental build time was only 17 seconds! But take into account that all modules are independent from one another. Once dependency between modules is introduced, build time increases from 17s to 42s (last row in the table above)!
即使对于库模块的更改,增量构建时间也仅为17秒 ! 但是要考虑到所有模块都是相互独立的。 一旦引入了模块之间的依赖关系,构建时间将从17s增加到42s (上表中的最后一行)!
以测试驱动方式开发库模块 (Developing Library Module in a Test Driven Way)
One of the main reasons why test-driven development (TDD) is difficult for single-module project is build time. TDD encourage to run test often. Running tests multiple times in a minute is a normal practice. But when build takes a minute or two, working in Test Driven way couldn’t be very productive.
对于单模块项目而言,测试驱动开发(TDD)很难的主要原因之一是构建时间。 TDD鼓励经常进行测试。 一分钟内多次运行测试是正常做法。 但是,当构建花费一两分钟时,以“测试驱动”的方式进行工作就不会很有效。
With introduction of modules that problem is automagically resolved. Building a single module in the last configuration took only 9s! It make possible to run tests as often as needed.
通过引入模块,该问题得以自动解决。 在最后的配置中构建单个模块仅需9秒钟 ! 它使得可以根据需要频繁地运行测试。
结论 (Conclusion)
First and most important, the hypothesis was correct, modularizing project can significantly speed up build process, but not for all configurations.
首先也是最重要的一点是,假设是正确的,模块化项目可以显着加快构建过程,但并非针对所有配置。
Second, if splitting is done in a wrong way, then build time will be drastically increased, because Gradle build both, release and debug version of library modules.
其次,如果以错误的方式进行拆分,则构建时间将大大增加,因为Gradle会构建库模块的发行版和调试版。
Third, working in test-driven way is much easier for a project with multiple modules, because building a small library module is way faster then the whole project.
第三,对于具有多个模块的项目,以测试驱动的方式进行工作要容易得多,因为构建小型库模块要比整个项目快得多。
Forth, doing many things in parallel slows down the build. So having more powerful hardware is a good idea.
第四,并行执行许多操作会减慢构建速度。 因此,拥有更强大的硬件是一个好主意。
Below you can find results of all experiments described in this article:
您可以在下面找到本文描述的所有实验的结果:
Please vote for mentioned issues. Resolving any of those would be a huge step towards faster builds. Below you can find all links:
请对提到的问题进行投票。 解决这些问题将是迈向更快构建的巨大一步。 您可以在下面找到所有链接:
All code is published on GitHub.
所有代码都发布在GitHub上 。
Nikita Kozlov (@Nikita_E_Kozlov) | TwitterThe latest Tweets from Nikita Kozlov (@Nikita_E_Kozlov): “https://t.co/wmGSJ7snW1"twitter.com
Nikita Kozlov(@Nikita_E_Kozlov)| Twitter 来自Nikita Kozlov(@Nikita_E_Kozlov)的最新推文:“ https://t.co/wmGSJ7snW1” twitter.com
Thank you for you time reading this article. If you like it, don’t forget to click the ? below.
感谢您抽出宝贵的时间阅读本文。 如果喜欢,不要忘记单击“ ?”。 下面。
应用程序 构建模块