知乎 Android 客户端 CI/CD 方面的实践

前言

伴随着知乎业务的飞速发展,近一年多时间,知乎的 Android 团队由十多人的小团队发展至五十多人的大团队,并且还在不断的壮大中。

虽然我们常常说人多力量大,但是有时候人多也未必是件好事,譬如经典计算机软件著作「人月神话」中就提到在某些情况下 1 + 1 也是有可能小于 2 的。(杜撰的,如有雷同…)

为了让 1+1 大于 2,移动平台团队做了一些工作,不断提升工程师的研发效率,降低各个团队相互干扰,减少重复无用功,支撑业务稳定前行。

下面就就其中 CI/CD 方向跟大家说一下。

组件化方面做的努力

Android 组件化方案 已经运转了近一年半的时间,令人欣喜的是其已经达到了我们当初的预期。即:不同 Android 团队之间,可以通过组件仓库制造代码壁垒,分而治之; 同时其来带的效果也是显著的,即:无论是研发效率还是编译速度都有了不少提升。 但是祸福相依,有得必有失。组件化也不例外,譬如:

  1. 先前代码全在一个仓库,组件化之后,代码跨了多个仓库,代码提交的 CodeReview 很不方便;
  2. 一般修改某个组件的流程是去组件仓库提交代码,合入代码后,发布新的组件包,最后在主工程中使用这个新版组件包,打出测试包。也就是代码测试发生在组件代码在合入后;
  3. 先前单个仓库的时候,主仓库有轮流的 merge 工程师,定期把 release 的代码 merge 到 develop 上面,多个组件后,仓库数量膨胀,如何不依赖组件管理人的细心程度,确保每个仓库的 release 代码能够合入到 develop 之中,也是一个问题。

跨组件的 CodeReview

知乎 Android 端的组件化,是使用如下的文件控制的:

// versions.gradle
component.answer.version = '1.2.3'

主工程读取这些 version 信息,然后再依赖这些组件, 使用类似于这样的语句:

compile com.zhihu.android:answer:${answer.version}

一般而言,我们比如升级了某一个组件,在主工程上面看到的改动就只有类似于这样:

// versions.gradle
+ component.answer.version = '1.2.5'
- component.answer.version = '1.2.3'

至于中间带来什么东西,只能靠工程师自己去翻仓库了,这个很不合理的。

其实由于每个版本都有一个 tag 对应,接着 gitlab 上面已经提供了一个方便浏览的页面

https://git.repo.guest.where.zhihu.com/Android/Ad/compare/<from-commit>...<to-commit>

我们直接输出出来即可。效果如下:

过去没有 tag 的时候,真的是一个个 commit 去搜,现在的孩子真幸福 =v=

联合打包

我们需要一个能够在多个组件提交代码之时,就能打出相应的测试包。

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 掉的部分)检查包增长,打个包出来,自然就知道了。

我们这边做的是:

  1. 每次合并代码之后,记录一下最新包的包大小以及包内信息,譬如 develop.detail release-1.2.3.detail
  2. 每次提 merge-request 往 develop/release 合的时候,打一个「假设已经」合入之后的包,获取它的包大小以及包内信息,跟历史纪录对比一下,即可以知道这次改动带来的变化

实现的效果如下:

确实细粒度到类或者包,可能会更好

包内信息我是 unzip 之后,逐一用 du 生成大小以及文件名的信息,交给 python 脚本进行比对的。大致的代码是:(由于文件过多,取最大的前 100 个)

// 统计包大小信息
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 的字典即可。

特殊团体的监控

移动平台团队维护的代码,由于调用方过多,稍有不慎,就出问题。所谓不受监督的权力容易滋生问题,所以平台组的成员需要出一套机制监督平台组的运转 ╮(╯_╰)╭

目前的是:

  1. 平台组内的 CodeReview 由另一个平台组成员 + 其他团队人员组成。CodeReviewer 是随机指派的,当然为了CodeReview 效果更好(总不能把做想法的工程师 review 首页的代码 领域不同 CodeReview 效果可能不大好 (@李明亮等100万人: 诶 会有问题吗) ( = =)泥奏凯)这边就是通过看这次改动里面的文件的修改记录(git log), 查到最新的经办人是谁,交给他。
  2. 平台组的代码提交 MergeRequest Open - Merged - Close 事件都会通知到群里面的人 定期每一个迭代都会生成「在这个迭代内平台组的所有提交」的报告,供业务方查看。

就酱. Thanks for reading.

来源:知乎,链接:https://zhuanlan.zhihu.com/p/49542869

近期好文推荐:

DevOps 国际峰会 2019 · 北京站完整实录(附PPT)

程序员自己写测试了,还要测试人员做什么?

Jenkins 中如何实现参数联动构建

原文发布于微信公众号 - DevOps时代(DevOpsTimes)

原文发表时间:2019-07-10

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券