写点什么

知乎 Android 客户端 CI/CD 方面的努力

  • 2020-03-26
  • 本文字数:3378 字

    阅读完需:约 11 分钟

知乎 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.gradlecomponent.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 的材料。

包大小监控

业务增长很快带来的另一个问题,是包大小也增长地很快。


包大小减少之前组内 Java script 工程师


@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.mp332  build/apk/res/drawable-night-xxhdpi-v8/are.webp32  build/apk/res/drawable-xxhdpi-v4/you.webp
复制代码


至于对比,只要写个 python 脚本读取该文件,以 name 为 key 的字典即可。

特殊团体的监控

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


目前的是:


平台组内的 CodeReview 由另一个平台组成员 + 其他团队人员组成。CodeReviewer 是随机指派的,当然为了 CodeReview 效果更好(总不能把做想法的工程师 review 首页的代码 领域不同 CodeReview 效果可能不大好 (


  1. @李明亮等100万人: 诶 会有问题吗) ( = =)泥奏凯)这边就是通过看这次改动里面的文件的修改记录(git log), 查到最新的经办人是谁,交给他。

  2. 平台组的代码提交 MergeRequest Open - Merged - Close 事件都会通知到群里面的人

  3. 定期每一个迭代都会生成「在这个迭代内平台组的所有提交」的报告,供业务方查看。


就酱. Thanks for reading.


2020-03-26 19:001164

评论

发布
暂无评论
发现更多内容

Developer 转型记:一个开发平台的“魔力”

华为云开发者联盟

华为 AI 开发者 开发者工具 华为云

单向链表合并算法

走过路过飞过

架构师培训 -08总结 数据结构算法,网络通信协议,非阻塞网络 I/O,数据库原理

刘敏

面试官问:僵尸进程和孤儿进程有了解过吗

Java小咖秀

Linux 学习 面试 进程 经验

该学一学了!零基础入门Docker

程序员的时光

Docker

信创舆情一线--50多家科技公司源代码泄露

统小信uos

判了!中科大博士写游戏外挂赚了12万获刑,被抓才知道帮团队赚了300万……

程序员生活志

游戏开发 游戏 游戏外挂 新闻

Java SSM 框架常见面试题

老大哥

Java

视频丨包不同的沙雕敏捷之砸锅卖铁买兰博

华为云开发者联盟

程序员 运维 敏捷 敏捷开发 技术人

一次线上JVM Young GC调优,搞懂了这么多东西!

南方有乔木兮

第八周总结

Acker飏

JVM详解之:HotSpot VM中的Intrinsic methods

程序那些事

Java JVM GC

百万并发「零拷贝」技术系列之经典案例Netty

码农神说

Java Netty 零拷贝

Java中的模板设计模式,太实用了!

BUZHIDAO

Java

MySQL的索引基础知识

guoguo 👻

要都练基本功

架构师

架构师训练营week08 作业

GunShotPanda

使用Spring Validation优雅地校验参数

Java课代表

springboot

一图看懂华为云DevCloud如何应对敏捷开发的测试挑战

华为云开发者联盟

微服务 敏捷开发 测试 云服务 华为云

求组队,PK华为HMS全球应用创新大赛!

InfoQ_e92167c73263

android

架构师训练营第8周学习总结

TH

架构师训练营week08 学习总结

GunShotPanda

判断两个链表是否合并

Acker飏

知识点梳理:聊聊iOS SDK数据采集那点事儿

易观大数据

第8周回顾

慵秋

【解构系统设计面试】什么是系统设计?以及如何设计一个新鲜事系统?

罗远航

系统设计

揭秘淘宝平台广告策略,拆解最佳投放实践

华为云开发者联盟

数据分析 广告 用户增长 淘宝 电商

轻量级BI应用-Superset实践

Jackchang234987

BI 数据产品

2行代码搞定一个定时器!

简爱W

第八周总结

LEAF

BFC "苦"前端久矣!

catcoolion

CSS 大前端

知乎 Android 客户端 CI/CD 方面的努力_文化 & 方法_郑小则_InfoQ精选文章