前言
伴随着知乎业务的飞速发展,近一年多时间,知乎的 Android 团队由十多人的小团队发展至五十多人的大团队,并且还在不断的壮大中。
虽然我们常常说人多力量大,但是有时候人多也未必是件好事,譬如经典计算机软件著作「人月神话」中就提到在某些情况下 1 1 也是有可能小于 2 的。(杜撰的,如有雷同…)
为了让 1 1 大于 2,移动平台团队做了一些工作,不断提升工程师的研发效率,降低各个团队相互干扰,减少重复无用功,支撑业务稳定前行。
下面就就其中 CI/CD 方向跟大家说一下。
组件化方面做的努力
Android 组件化方案 已经运转了近一年半的时间,令人欣喜的是其已经达到了我们当初的预期。即:不同 Android 团队之间,可以通过组件仓库制造代码壁垒,分而治之; 同时其来带的效果也是显著的,即:无论是研发效率还是编译速度都有了不少提升。 但是祸福相依,有得必有失。组件化也不例外,譬如:
- 先前代码全在一个仓库,组件化之后,代码跨了多个仓库,代码提交的 CodeReview 很不方便;
- 一般修改某个组件的流程是去组件仓库提交代码,合入代码后,发布新的组件包,最后在主工程中使用这个新版组件包,打出测试包。也就是代码测试发生在组件代码在合入后;
- 先前单个仓库的时候,主仓库有轮流的 merge 工程师,定期把 release 的代码 merge 到 develop 上面,多个组件后,仓库数量膨胀,如何不依赖组件管理人的细心程度,确保每个仓库的 release 代码能够合入到 develop 之中,也是一个问题。
跨组件的 CodeReview
知乎 Android 端的组件化,是使用如下的文件控制的:
代码语言:javascript复制// versions.gradle
component.answer.version = '1.2.3'
主工程读取这些 version 信息,然后再依赖这些组件, 使用类似于这样的语句:
代码语言:javascript复制compile com.zhihu.android:answer:${answer.version}
一般而言,我们比如升级了某一个组件,在主工程上面看到的改动就只有类似于这样:
代码语言:javascript复制// versions.gradle
component.answer.version = '1.2.5'
- component.answer.version = '1.2.3'
至于中间带来什么东西,只能靠工程师自己去翻仓库了,这个很不合理的。
其实由于每个版本都有一个 tag 对应,接着 gitlab 上面已经提供了一个方便浏览的页面
代码语言:javascript复制https://git.repo.guest.where.zhihu.com/Android/Ad/compare/<from-commit>...<to-commit>
我们直接输出出来即可。效果如下:
过去没有 tag 的时候,真的是一个个 commit 去搜,现在的孩子真幸福 =v=
联合打包
我们需要一个能够在多个组件提交代码之时,就能打出相应的测试包。
代码语言:javascript复制gradle 可以通过下面方法源码依赖某一个工程
include 'moduleA'
project('moduleA').rootDir = '/path/of/module/A'
gradle 版本依赖某一个工程
dependencies {
implementation 'com.well.zhihu:moduleJoinUs:2.3.3'
}
我们这里是使用一个配置文件,配置需要源码依赖或版本依赖的组件。
如下图:
可以看到这次打包是联合了 ModuleA 以及 ModuleB 组件提交的代码打包。
这里面有个细节,我们在每次开始编译的时候加了「begin to build」以及 job 版本号(图中的 3537),是为了跟最后生成的包的 job 版本号匹配的。
为了让测试的同学知道,这个包是在哪个代码状态下打出来的(打包所获取的组件代码是当前 MergeRequest 提交的代码,担心在打包的过程中,又提交新的代码,这样生成的包就不是当前代码状态的了,让测试同学误解)
分支合并的问题
世界上最冤的 bug 不是字符串的值为 “null”,而是我已经在 release 上修了,但是代码没有合入到 develop。如下图:
bug 是不存在的
如果是 bug 不严重的话,可能就只是浪费测试以及开发资源。但是遇上什么 downtime,紧急修复,忘记合入,则会是新的 downtime,又一次紧急修复。而人总有可能犯错。
知乎这边的做法是定期自动提这样的 merge-request: 「次最新 release => 最新 release」以及「最新 release => develop」(也就是上文的 release-1.2 => release-1.3 release-1.3 => develop )我们会在需要合并的时候定期提醒工程师合并代码,尽量减少工程师的工作。
其他
还有一些细枝末节的,譬如:
有些业务组件的发布流程与主工程同步,在主工程拉分支的时候,也会拉出一个对应的 release 分支,一般自动拉分支的组件都会有自动合并分支的功能。
创建 lint 服务,组件工程只要配置一下,提交代码的时候,都会跑一次 lint,报告贴在 merge request 中,作为 CodeReview 的材料。
包大小监控
业务增长很快带来的另一个问题,是包大小也增长地很快。
包大小减少之前组内 Javascript 工程师 @Peter Porker 做过一次,效果显著,但是无奈,包内增大席卷重来,所以除了直接减少包大小,一套可以无需人为地遏制包大小的增长,或者监控包大小的增长情况的方案,尤为重要。
我们这里做了两件事,一个是使用 gradle githook 的方式,限制某些不规范的提交(譬如过大的资源文件等),二,实时监控代码提交的时候带来的包增长,生成易读的报告。
限制不规范的提交
不规范的提交包括:资源过大,提交的资源是 png 而不是优化过的 webp,一些低像素的资源也提交过去(-hdpi,-mdpi 现今的设备基本上不会用到这些资源)
githooks 中,可以往 commit-msg 中写一些脚本,检查当前提交的文件内,是否出现上述问题(可以用下列方式获取到当前提交文件: git diff --cached --name-only --diff-filter=d
)。
这里有一个问题,git hooks 一般不跟版本走,也就是说很难提交到仓库,然后让别人 down 下来,去覆盖本地的 hooks 文件。想要做到这一点,这就需要外界脚本的帮助。
知乎这边 Android 的开发流程很依赖 gradle,我们的做法是 先把 hooks 里面的所有文件存放在某个仓库里面,然后在 gradle 中植入这些代码:download 这些 hooks 文件,然后覆盖复制到本地的 .git/hooks/ 下。篇幅的问题,代码就不贴了 = =)download 的方法,我是用 git archive 。
最后把这些逻辑写入到 gradle plugin 中。由于所有组件工程都会依赖这个 plugin,这样所有组件工程都会装上 hook,所有的代码的提交都会被你限制到(我给他取名 Ozymandias =v=
安装 git hooks 的效果图:(其实文字都是自己打印出来的,所以 「效果图」谈不上 = =)
美术有点差,见谅 - -
commit-msg 代码检查:
欲擒故纵 =v=
实时监控代码提交,生成相应报告
实际上,这些是不能 cover 住很多情况的,而且有些加入的资源,最后不一定会加入到 apk 包中(譬如 proguard 掉的部分)检查包增长,打个包出来,自然就知道了。
我们这边做的是:
- 每次合并代码之后,记录一下最新包的包大小以及包内信息,譬如 develop.detail release-1.2.3.detail
- 每次提 merge-request 往 develop/release 合的时候,打一个「假设已经」合入之后的包,获取它的包大小以及包内信息,跟历史纪录对比一下,即可以知道这次改动带来的变化
实现的效果如下:
确实细粒度到类或者包,可能会更好
包内信息我是 unzip 之后,逐一用 du 生成大小以及文件名的信息,交给 python 脚本进行比对的。大致的代码是:(由于文件过多,取最大的前 100 个)
代码语言:javascript复制// 统计包大小信息
TOTAL_SIZE=`stat -c %s ${package}`
SIZE_IN_MB=`echo "scale=2;${TOTAL_SIZE} / 1024 / 1024" | bc`
// 统计包内文件信息
unzip "$package" -d "build/apk"
find build/apk -type f | xargs du -k | sort -n | tail -n 100
| tee "$file_info"
// 生成的包内信息如下:
28 build/apk/res/raw/how.mp3
32 build/apk/res/drawable-night-xxhdpi-v8/are.webp
32 build/apk/res/drawable-xxhdpi-v4/you.webp
至于对比,只要写个 python 脚本读取该文件,以 name 为 key 的字典即可。
至于对比,只要写个 python 脚本读取该文件,以 name 为 key 的字典即可。
特殊团体的监控
移动平台团队维护的代码,由于调用方过多,稍有不慎,就出问题。所谓不受监督的权力容易滋生问题,所以平台组的成员需要出一套机制监督平台组的运转 ╮(╯_╰)╭
目前的是:
- 平台组内的 CodeReview 由另一个平台组成员 其他团队人员组成。CodeReviewer 是随机指派的,当然为了CodeReview 效果更好(总不能把做想法的工程师 review 首页的代码 领域不同 CodeReview 效果可能不大好 (@李明亮等100万人: 诶 会有问题吗) ( = =)泥奏凯)这边就是通过看这次改动里面的文件的修改记录(git log), 查到最新的经办人是谁,交给他。
- 平台组的代码提交 MergeRequest Open - Merged - Close 事件都会通知到群里面的人 定期每一个迭代都会生成「在这个迭代内平台组的所有提交」的报告,供业务方查看。
就酱. Thanks for reading.
来源:知乎,链接:https://zhuanlan.zhihu.com/p/49542869
近期好文推荐:
DevOps 国际峰会 2019 · 北京站完整实录(附PPT)
程序员自己写测试了,还要测试人员做什么?
Jenkins 中如何实现参数联动构建