这篇文章让小伙伴们久等了。
一年多以来,关于嵌入式开发学习路线、规划、看什么书等问题,被问得没有一百,也有大几十次了。但是无奈自己对这方面了解有限,所以每次都没法交代,搞得实在不好意思。
但是办法总归是有的,正如前篇文章所聊,虽然我自己不从事这些方向,但寻思了一下,我的同学和朋友中,多少还是有一些从事相关方向并且在一线(或曾在一线)工作过的,所以我就请他们帮忙一起梳理这几个我不熟悉领域的学习路线,这次嵌入式开发就是其中之一。
这篇文章之所以拖这么久,因为沟通实在太费时间了,毕竟大佬们都挺忙,况且梳理总结这件事情本身就比较繁琐,所以等到现在才发出来。
Android 项目一般使用 gradle 作为构建打包工具,而其执行速度慢也一直为人所诟病,对于今日头条 Android 项目这种千万行级别的大型工程来说,全量编译一次的时间可能高达六七分钟,在某些需要快速验证功能的场景,改动一行代码的增量编译甚至也需要等两三分钟,这般龟速严重影响了开发体验与效率,因此针对 gradle 编译构建耗时进行优化显得尤为重要。
在今日头条 Android 项目上,编译构建速度的优化和恶化一直在交替执行,18 年时由于模块化拆分等影响,clean build 一次的耗时达到了顶峰 7 分 30s 左右,相关同学通过模块 aar 化,maven 代理加速,以及增量 java 编译等优化手段,将 clean build 耗时优化到 4 分钟,增量编译优化到 20~30s 。但是后面随着 kotlin 的大规模使用,自定义 transform 以及 apt 库泛滥,又将增量编译速度拖慢到 2 分 30s ,且有进一步恶化的趋势。为了优化现有不合理的编译耗时,防止进一步的恶化,最近的 5,6 双月又针对编译耗时做了一些列专项优化(kapt,transform,dexBuilder,build-cache 等) 并添加了相关的防恶化管控方案。 从 4.27 截止到 6.29 ,整体的优化效果如下:
由于 18 年左右客户端基础技术相关同学已经对今日头条 Android 工程做了许多 gradle 相关的优化,且这些优化是近期优化的基础,因此先挑选几个具有代表性的方案进行介绍,作为下文的背景同步。
gradle 工程往往会在 repositories 中添加一些列的 maven 仓库地址,作为组件依赖获取的查找路径,早期在今日头条的项目中配置了十几个 maven 的地址,但是依赖获取是按照 maven 仓库配置的顺序依次查找的,如果某个组件存在于最后一个仓库中,那前面的十几个仓库得依次发起网络请求查找,并在网络请求返回失败后才查找下一个,如果项目中大多组件都在较后仓库的位置,累加起来的查找时间就会很长。
今日头条项目进行了多次组件化和模块化的重构,分拆出了 200 多个子模块,这些子模块如果全都 include 进项目,那么在 clean build 的时候,所有子模块的代码需要重新编译,而对于大多数开发人员来说,基本上只关心自己负责的少数几个模块,根本不需要改动其他模块的代码,这些其他 project 的配置和编译时间就成为了不必要的代价。
对于以上子模块过多的解决方案是:将所有模块发布成 aar ,在项目中全部默认通过 maven 依赖这些编译好的组件,而在需要修改某个模块时,通过配置项将该模块的依赖形式改为源码依赖,做到在编译时只编译改动的模块。但是这样做会导致模块渐渐的又全部变为源码依赖的形式,除非规定每次修改完对应模块后,开发人员自己手动将模块发布成 aar ,并改回依赖形式。这种严重依赖开发人员自觉,并且在模块数量多依赖关系复杂的时候会显得异常繁琐,因此为了开发阶段的便利,设计了一整套更完整细致的方案:
通过上述改造,将源码模块切换成 aar 依赖后,clean build 耗时从 7,8 分钟降低至 4,5 分钟,收益接近 50%,效果显著。
在非 clean build 的情况下,更改 java/kotlin 代码虽然会做增量编译,但是为了绝对的正确性,gradle 会根据一些列依赖关系计算,选择需要重新编译的代码,这个计算粒度比较粗,稍微改动一个类的代码,就可能导致大量代码重新执行 apt, 编译等流程。
由于 gradle 作为通用框架,其设计的基本原则是绝对的正确,因此很容易导致增量编译失效,在实际开发中,为了快速编译展示结果,可以在编译正确性和编译速度上做一个折中的方案:
以上方案(下文全部简称为 fastbuild) 虽然在涉及常量修改,方法签名变更方面 存在一定的问题(常量内联等),但是能换来增量编译从 2 分多降低至 20~30s,极大的提升编译效率,且有问题的场景并不常见,因此整体上该方案是利大于弊的。
通过上文介绍的几个优化方案和其他优化方式,在 18 年时,今日头条 Android 项目的整体编译速度(clean build 4~5min, fast 增量编译 20~30s)在同量级的大型工程中来说是比较快的 ,然而后期随着业务发展的需求,编译脚本添加了很多新的逻辑:
这些逻辑的引入,使得增量编译耗时恶化到 2 分 30s,即使采用 fastbuild,改动一行代码编译也需要 1 分 30s 之多,开发体验非常差。而下文将着重描述最近一段时间对上述问题的优化过程。
今日头条工程经过多次模块化,组件化重构后, app 模块(NewsArticle)的大部分代码都已经迁移到子模块(上文已经介绍过子模块可以采用 aar 化用于编译速度优化,app 模块只剩下一个壳而已。
但是从 build profile 数据(执行 gradle 命令时添加 --profile 参数会在编译完成后输出相关 task 耗时的统计文件) 中发现到一个异常 case:明明只有 2 个类的 app 模块 kapt(annotationProcessor 注解处理) 相关耗时近 1 分钟。
通过进一步观察,虽然 app 模块拆分后只有 2 个简单类的代码,但是却用了 6 种 kapt 库, 且实际生效的只是其中 ServiceImpl 一个注解 (内部 ServiceManager 框架,用于指示生产 Proxy 类,对模块之间代码调用进行解耦)。如此一顿操作猛如虎,每次编译却只生成固定的两个 Proxy 类,与 53s 的高耗时相比,投入产出比极低。
把固定生成的 Proxy 类从 generate 目录移动到 src 目录,然后禁止 app 模块中 kapt 相关 task ,并添加相关管控方案(如下图: 检测到不合理情况后立刻抛出异常),防止其他人添加新增的 kapt 库。
通过上文介绍在 app 模块发现的异常的 kapt case, 进而发现在工程中为了方便,定义了一个 library.gradle ,该文件的作用是定义项目中通用的 Android dsl 配置和共有的基础依赖,因此项目中所有子模块均 apply 了这个文件,但是这个文件陆陆续续的被不同的业务添加新的 kapt 注解处理库,在全源码编译时,所有子模块都得执行 library 模块中定义的全部 6 个 kapt ,即使该模块没有任何注解相关的处理也不例外。
而上述情况的问题在于:相比纯 java 模块的注解处理,kotlin 代码需要先通过 kaptGenerateStub 将 kt 文件转换成为 java ,让 apt 处理程序能够统一的面向 java 做注解扫描和处理。但是上面讲到其实有很多模块是根本不会有任何实际 kapt 处理过程的,却白白的做了一次 kt 转 java 的操作,源码引入的模块越多,这种无意义的耗时累加起来也非常可观。
为了能够弄清楚到底有哪些子模块真正用到了 kapt ,哪些没用到可以禁用掉 kapt 相关 task ,对项目中所有子模块进行了一遍扫描:
使用上述方案,通过全源码打包最终扫描出来大概是 70+模块不会进行任何 kapt 的实际输出,且将这些不会进行输出的 kapt,kaptGenerateStub 的 task 耗时累加起来较高 217s (由于 task 并发执行所以实际总时长可能要少一些).
获取到不实际生成 kapt 内容的模块后,开始对这些模块进行细粒度的拆分,让它们从 apply library.gradle 改为没有 kapt 相关的 library-api.gradle ,该文件除了禁用 kapt 外,与 library 逻辑一致。
但是这样做算是在背后偷偷做了些更改,很可能后续新来的同学不知道有这种优化手段,可能新增了注解后却没有任何输出且找不到原因,而优化效果最好是尽量少给业务同学带来困扰。为了避免这种情况,便对这些 library-api 模块依赖的注解做隔离优化,即:把这些模块依赖的注解库全部 自动 exclude 掉,在尝试使用注解时会因获取不到引用(如下图所示),第一时间发现到依赖被移除的问题。
另一方面在编译出现错误时,对应 gradle 插件会自动解析找不到的符号,如果发现该符号是被隔离优化的注解,会提示将 library-api 替换成 library,尽可能降低优化方案对业务的负面影响。
最后对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!
这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司20年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
上述【高清技术脑图】以及【配套的面试真题PDF】可以点击我的腾讯文档免费获取
本文在开源项目中已收录,里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。
面试题集合/面经、及系列技术文章等,资源持续更新中…
当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。