android 模块化编译,Android加速编译终极方案---模块化

f7f00e76da2564567911177f59410700.png

作为一个文艺的程序猿,配图当然是要用自己拍的了,哈哈啊哈哈哈哈

“所谓Android开发,五年开发三年编译。”——鲁迅(鲁迅:mmp...)

本文主要讨论关于Android代码模块化后,对于编译速度提升。其中涉及详细的测试过程和数据分析,以及遇到问题的解决。本文为翻译整理文章,感叹于作者的探索精神。

Nikita Kozlov

原文链接

预备知识点

App Module

每创建一个Android工程都会包含至少一个Application module。模块根目录build.gralde文件中会引入插件:apply plugin: 'com.android.application',这时编译项目会生成apk文件。application module不能被其他项目依赖,可独立运行。

Library Module

将插件改为:apply plugin: ‘com.android.library',该模块就成为了一个Library Module。Library Module可被其他工程依赖,被依赖后编译生成aar文件,模块不可以独立运行。编译生成的aar,和jar相比差别是多了res文件和manifest文件。

Android Studio项目构建过程

Android Studio(后简称AS)项目构建过程可以分为五个部分,分别由一个gradle 任务来代替:

准备依赖:这个阶段Gradle检查所有module依赖的库,module中依赖的其他module也被同事编译。

合并资源,处理manifest文件:该阶段资源文件和manifest文件准备就绪。

编译:该过程如果项目中使用了编译时注解,则由注解处理器开始。然后源码被编译成字节码。如果项目使用到AspectJ,织入也发生在该阶段。

后期处理:所有gradle任务中带transform前缀的在这个阶段执行。其中最重要的两个任务是 transformClassesWithMultidexlist和transformClassesWithDex。他们是用来生成dex文件的。

打包发布:所有平台文件打包为aar,生成apk。

本文最终讨论如何减少编译时间,一次性对所有阶段进行加速是很难,所以我们把注意力集中在最耗时的阶段。对于单模块项目,最耗时的阶段发生在编译和后期处理。资源合并阶段有时也同样很耗时,但如果java代码未发生改变,增量编译也是很快的。

Gradle运行任务的前提是输入发生改变,如果模块未发生改变是不会重新编译的,那么我们就有了如下猜测:

“ 多模块模式下的增量编译要比单模块快,因为只有被修改的模块才会被重新编译 ”

接下来我们就来证明上面的假设。

1、实验前准备

测试项目gradle版本2.2.2,最小android兼容版本为15(该版本兼容了大多数安卓设备),所有模块都依赖butterknife,这样使项目更接近真实情况。同时所有项目都有外部依赖。

项目中主工程为app,子模块为 app1,app2...

每个项目都有100个包,15K个类,9W个方法,参与生成的dex文件在2-3个。由于自动生成的代码是假的,为了保证代码和资源能完整打包进apk,关闭了minifying和shrikning功能。

所有测试结果都通过Gradle build-in profiler保存。在控制台输入如下命令可得到html格式的测试结果:./gradlew assembleDebug --profile

每次测试数据都执行4到15次,以保证测试结果真是可再现。

测试代码生成

手写15000个类,那是不可能的!!!所以实验代码是用Python脚本自动生成的,生成代码见Gist,生成的代码结构如下:

880546338b07ba912b33a6dd492489de.png

生成测试代码结构

上面代码的调用过程形成了一种剪不断理还乱的代码结构(就像你自己写一毛一样),从而可以真实模拟增加的编译时间。

关于纯Java模块

本实验并未针对纯Java模块进程测试,纯Java模块运行效率肯定高于Android模块。因为没有的资源的整合,编译其运行的任务更少。

如果你对纯Java模块测试结果感兴趣,请留言告诉我。或者你可以下载本实验项目,根据你的需求修改即可。此外,测试结果还取决于硬件水平,为了使结果更具可比性,请多次重复实验。

排除干扰因素

毫无疑问,并发的进行编译测试,同样会减缓编译速度。尽管只同时开两个项目,也会使编译速度减缓5-10%。此外,测试的同时,听音乐、上网、看视频同样也会对编译速度有明显的影响。就个人体会,关闭除AS以外其他进程会使编译速度加快差不多30%。

最后,以下所有实验均是在只开启浏览器和AS的情况下完成的。

2、开始实验

初始单模块

起始状态,编译包含15K个类的单模块项目耗时1m10s。

三个模块

将单模块项目拆解为3个模块,一个主模块,两个库模块,主模块依赖于两个库模块,两个库模块相互独立。每个模块有5K个类,共3W个方法。

如果修改主模块代码,编译时间大概35s左右。比初始状态节省近30s。但其中一个库模块发生修改,尽管主模块未改变,编译时间却增加到了1m50s,长了近40s!!!

以下是编译时间测试日志:

e46211643c567fb6e41813367ec71285.png

app2

fe16734c9841c4db517cd630b24b68fd.png

app

从以上截图中可以发现,大多数时间耗在了库模块的编译上。而且两个库模块,Debug版和Release版的编译任务都执行了。Gradle编译时间全耗在两个版本库模块的任务执行上了。这就是为什么相比单模块项目,时间长了近40s。

通过项目模块拆分加快了编译速度(只修改App Module的情况下)。

还有一个问题,且看以下主工程依赖两个库模块的方式:

dependencies {

compile project(path: ':app2')

compile project(path: ':app3')

}

以上代码有个重要的问题:像这样的方式依赖库模块,不管主工程是正式环境还是其他环境,

主工程依赖的总是库的正式版。Gradle用户指南上提到:

默认情况下,依赖库只发布正式版。对于所有依赖库,不管编译环境是哪个,均只发布正式版的库。这是一个临时的限制,我们可以将其限制移除。

我们也可以更改其依赖的版本。

首先在build.gradle文件中添加如下代码,使其允许依赖库发布debug版本:

android {

defaultConfig {

defaultPublishConfig 'release'

publishNonDefault true

}

}

其次,将依赖库的版本做如下更改:

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")

}

现在,我们就可以在不同的编译环境下依赖不同的库版本了。此时修改模块app2并重新编译。经过更改,我们再查看编译时间测试日志:

96f612ac15305dfc1f784fce6f7b8193.png

app2

c8545c92ba6581e449daba20172c16f1.png

app

结果中,最大的差别就是:app2中的packageReleaseJarArtifact任务不见了,节省了15s编译时间。此外,其他任务的执行时间也有了变化,最终耗时1m32s。比之前快了18s,但仍然比初始状态慢22s。对于更改主工程的情况,时间几乎没变,35s比36s。

但我仍然没有找到执行两种任务(Debug和Release)的合理解释。期初希望一次性解决Gradle限制的问题,但目前似乎并没有解决。同样的问题在AOSP 问题追踪中也有被讨论到。

我打算再花点时间在Gradle任务测试上,找到问题得解决办法。

五个模块

显而易见,编译时间取决于模块代码量。模块代码量减少一半,编译时间也相应提高一倍。如果3个模块被拆解成5个模块,编译时间也会加快近40%,事实也确实如此:

dc48dfa9d9831e90505545c055dee45f.png

结果对照图

如果只改变主工程代码,编译时间大概在24s。更改库模块代码,编译时间则需要50s。和开始单模块的1m10s相比,已经是快了。但仍然还有一些加快编译的办法。

减小主工程代码量后的n+1结构

和库模块项目比,主工程每次都需要编译,所以尽可能减小主工程代码量,对加速编译也有显著意义。理想状态下,主工程应该只提供启动页相关功能,其他功能都依赖于各个模块,因为启动页常需要依赖于各个模块的功能。

这种情况下,就诞生了3+1和5+1的项目架构。两种架构下,都是一个小规模的主工程依赖于3-5个库模块。所有库模块彼此独立,规模相同。且看如下测试结果:

8487b60c60d300f6de3c36fadc6feb50.png

img07.jpeg

我们可以观察到编译时间进一步减少。即使在5+1架构下,更改发生在库模块,编译时间和初始的单模块相比,时间同样减少了一半。

3、为什么测试项目要基于ButterKnife?

这里有个重点需要强调。依赖ButterKnife是有重要原因的。初始的单模块项目中,整个运行过程的1m10s里,有45s是增量编译的耗时。但如果移除了butterknife,增量编译的时间只有15s。快了3倍。整个项目在没有ButterKnife的情况下,运行时间就会缩短到40s。

是ButterKnife出了问题吗?

项目中在没有ButterKnife的情况下编译变快的原因是Java增量编译,但是对于注解处理器,他是不支持的。相关问题的讨论在这里你可以看到Gradle Jira、AOSP Issue Tracker、Gradle Design Doc。在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."

项目中使用到的apt被禁用了,所以增量编译是没有意义的。

这就是变慢的原因。

但我个人并没有弃用注解处理器,因为诸如Dagger、ButterKnfie(都有用到注解处理器)真的太好用了。但可以选择在部分模块中不用这些使用到注解处理器的库,这样模块编译就更快了。

4、提高兼容版本,加速编译

除编译耗时以外,.dex文件生成同样耗时,尤其在方法数超出限制,采取multidex配置时,系统需要决定在哪个dex文件中放那些类。多Dex结构的应用,其运行方式也发生了变化,其中AS开发文档中介绍到:

Android 5.0以版本系统上使用的ART支持从Apk中本地加载dex文件。ART在安装时扫描每个dex文件并编译成一个oat文件,以供在Android设备上执行。

这也导致了编辑时间的增加。原因是每个module都生成自己的dex文件,然后被打包进apk文件。如果你观察它的构建过程,你就会注意到transformClassesWithMultidexlist不再执行。同时编译速度也加快。更多定制flavor的配置信息你可以在这里找到。部分定制编译环境的代码如下:

android {

defaultConfig {

...

}

productFlavors {

dev {

// Enable pre-dexing to produce an APK that can be tested on

// Android 5.0+ without the time-consuming DEX build processes.

minSdkVersion 21

}

prod {

// The actual minSdkVersion for the production version.

minSdkVersion 14

}

}

}

5、最快的编译结构配置

5d813d9f1881fb3ab4a4ccfba938ea44.png

img08.jpeg

基于Android 5.0设备开发,修改module时,编译效率显著提高。

6、模块化后,更方便测试

对单Module项目进行测试很困难的一个主要原因也是编译时间。TDD鼓励频繁测试,时常要一分钟内频繁的运行测试用例。长时间编译导致这种测试方式很低效。多模块开发使得这种问题自然而然的被解决。

总结

模块化可以有效提高编译效率。如果模块以错误的方式拆分,编译时间会增加,因为编译同时产生正式包和测试包。多模块模式下,测试更容易进行。同时,团队合作时,方便并行开发。

参考

https://code.google.com/p/android/issues/detail?id=52962

https://issues.gradle.org/browse/GRADLE-3259

https://code.google.com/p/android/issues/detail?id=200043

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值