大公司如何做 APP:背后的开发流程和技术

7,183 阅读19分钟

我记得五六年前,当我在 Android 开发领域尚处初出茅庐阶段之时,我曾有一个执念——想看下大公司在研发一款产品的流程和技术上跟小公司有什么区别。公司之大,对开发来说,不在于员工规模,而在于产品的用户量级。只有用户量级够大,研发过程中的小问题才会被放大。当用户量级够大,公司才愿意在技术上投入更多的人力资源。因此,在大公司里做技术,对个人的眼界、技术细节和深度的提升都有帮助。

我记得之前我曾跟同事调侃说,有一天我离职了,我可以说我毕业了,因为我这几年学到了很多。现在我想借这个机会总结下这些年在公司里经历的让我印象深刻的技术。

1、研发流程

首先在产品的研发流程上,我把过去公司的研发模式分成两种。

第一种是按需求排期的。在评审阶段一次性评审很多需求,和开发沟通后可能删掉优先级较低的需求,剩下的需求先开发,再测试,最后上线。上线的时间根据开发和测试最终完成的时间确定。

第二种是双周迭代模式,属于敏捷开发的一种。这种开发机制里,两周一个版本,时间是固定的。开发、测试和产品不断往时间周期里插入需求。如下图,第一周和第三周的时间是存在重叠的。具体每个阶段留多少时间,可以根据自身的情况决定。如果需求比较大,则可以跨迭代,但发布的时间窗口基本是固定的。

截屏2023-12-30 13.00.33.png

有意思的是,第二种开发机制一直是我之前的一家公司里负责人羡慕的“跑火车”模式。深度参与过两种开发模式之后,我说下我的看法。

首先,第一种开发模式适合排期时间比较长的需求。但是这种方式时间利用率相对较低。比如,在测试阶段,开发一般是没什么事情做的(有的会在这个时间阶段布置支线需求)。这种开发流程也有其好处,即沟通和协调成本相对较低。

注意!在这里,我们比较时间利用率的时候是默认两种模式的每日工作时间是相等的且在法律允许范围内。毕竟,不论哪一种研发流程,强制加班之后,时间利用率都“高”(至少老板这么觉得)。

第二种开发方式的好处:

  1. 响应速度快。可以快速发现问题并修复,适合快速试错。
  2. 时间利用率高。相比于按需求排期的方式,不存在开发和测试的间隙期。

但这种开发方式也有缺点:

  1. 员工压力大,容易造成人员流失。开发和测试时间穿插,开发需要保证开发的质量,否则容易影响整个迭代内开发的进度。
  2. 沟通成本高。排期阶段出现人力冲突需要协调。开发过程中出现问题也需要及时、有效的沟通。因此,在这种开发模式里还有一个角色叫项目经理,负责在中间协调,而第一种开发模式里项目经理的存在感很低。
  3. 这种开发模式中,产品要不断想需求,很容易导致开发的需求本身价值并不大。

做了这么多年开发,让人很难拒绝一个事实是,绝大多数互联网公司的壁垒既不是技术,也不是产品,而是“快速迭代,快速试错”。从这个角度讲,双周迭代开发机制更适应互联网公司的要求。就像我们调侃公司是给电脑配个人,这种开发模式里就是给“研发流水线”配个人,从产品、到开发、到测试,所有人都像是流水线上的一员。

2、一个需求的闭环

以上是需求的研发流程。如果把一个需求从产品提出、到上线、到线上数据回收……整个生命周期列出来,将如下图所示,

需求闭环.drawio.png

这里我整合了几个公司的研发过程。我用颜色分成了几个大的流程。相信每个公司的研发流程里或多或少都会包含其中的几个。在这个闭环里,我说一下我印象比较深刻的几个。

2.1 产品流程

大公司做产品一个显著的特点是数据驱动,一切都拿数据说话。一个需求的提出只是一个假设,开发上线之后效果评估依赖于数据。数据来源主要有埋点上报和舆情监控。

1. 数据埋点

埋点数据不仅用于产品需求的验证,也用于推荐算法的训练。因此,大公司对数据埋点的重视可以说是深入骨髓的。埋点数据也经常被纳入到绩效考核里。

开发埋点大致要经过如下流程,

  • 1). 产品提出需要埋的点。埋点的类型主要包括曝光和点击等,此外还附带一些上报的参数,统计的维度包括用户 uv 和次数 pv.
  • 2). 数据设计埋点。数据拿到产品要埋的点之后,设计埋点,并在埋点平台录入。
  • 3). 端上开发埋点。端上包括移动客户端和 Web,当然埋点框架也要支持 RN 和 H5.
  • 4). 端上验证埋点。端上埋点完成之后需要测试,上报埋点,然后再在平台做埋点校验。
  • 5). 产品提取埋点数据。
  • 6). 异常埋点数据修复。

由此可见,埋点及其校验对开发来说也是需要花费精力的一环。它不仅需要多个角色参与,还需要一个大数据平台,一个录入、校验和数据提取平台,以及端上的上报框架,可以说成本并不低。

2. 舆情监控

老实说,初次接触舆情监控的时候,它还是给了我一点小震撼的。没想到大公司已经把舆情监控做到了软件身上。

舆情监控就是对网络上关于该 APP 的舆情的监控,数据来源不仅包括应用内、外用户提交的反馈,还包括主流社交平台上关于该软件的消息。所有数据在整合到舆情平台之后会经过大数据分析和分类,然后进行监控。舆情监控工具可以做到对产品的负面信息预警,帮助产品经理优化产品,是产品研发流程中重要的一环。

3. AB 实验

很多同学可能对 AB 实验都不陌生。AB 实验就相当于同时提出多套方案,然后左右手博弈,从中择优录用。AB 实验的一个槽点是,它使得你代码中同时存在多份作用相同的代码,像狗皮膏药一样,也不能删除,非常别扭,最后导致的结果是代码堆积如山。

4. 路由体系建设

路由即组件化开发中的页面路由。但是在有些应用里,会通过动态下发路由协议支持运营场景。这在偏运营的应用里比较常见,比如页面的推荐流。一个推荐流里下发的模块可能打开不同的页面,此时,只需要为每个页面配置一个路由路径,然后推荐流里根据需要下发即可。所以,路由体系也需要 Android 和 iOS 双端统一,同时还要兼容 H5 和 RN.

mdn-url-all.png

在路由协议的定义上,我们可以参考 URL 的格式,定义自己的协议、域名、路径以及参数。以 Android 端为例,可以在一个方法里根据路由的协议、域名对原生、RN 和 H5 等进行统一分发。

2.2 开发流程

在开发侧的流程里,我印象深的有以下几个。

1. 重视技术方案和文档

我记得之前在一家公司里只文档平台就换了几个,足见对文档的重视。产品侧当然更重文档,而对研发侧,文档主要有如下几类:1). 周会文档;2).流程和规范;3).技术方案;4).复盘资料等。

对技术方案,现在即便我自己做技术也保留了写大需求技术方案先行的习惯。提前写技术方案有几个好处:

  • 1). 便于事后回忆:当我们对代码模糊的时候,可以通过技术方案快速回忆。
  • 2). 便于风险预知:技术方案也有助于提前预知开发过程中的风险点。前面我们说敏捷开发提前发现风险很重要,而做技术方案就可以做到这点。
  • 3). 便于全面思考:技术方案能帮助我们更全面地思考技术问题。一上来就写代码很容易陷入“只见树木,不见森林”的困境。

2. Mock 开发

Mock 开发也就是基于 Mock 的数据进行开发和测试。在这里它不局限于个人层面(很多人可能有自己 Mock 数据开发的习惯),而是在公司层面将其作为一种开发模式,以实现前后端分离。典型的场景是客户端先上线预埋,而后端开发可能滞后一段时间。为了支持 Mock 开发模式,公司需要专门的平台,提供以接口为维度的 Mock 工具。当客户端切换到 Mock 模式之后,上传到网络请求在后端的网关直接走 Mock 服务器,拉取 Mock 数据而不是真实数据。

这种开发模式显然也是为了适应敏捷开发模式而提出的。它可以避免前后端依赖,减轻人力资源协调的压力。这种开发方式也有其缺点:

  • 1). 数据结构定义之后无法修改。客户端上线之后后端就无法再修改数据结构。因此,即便后端不开发,也需要先投入人力进行方案设计,定义数据结构,并拉客户端进行评审。
  • 2). 缺少真实数据的验证。在传统的开发模式中,测试要经过测试和 UAT 两个环境,而 UAT 本身已经比较接近线上环境,而使用 Mock 开发就完全做不到这么严谨。当我们使用 Mock 数据测试时,如果我们自己的 Mock 的数据本身失真比较严重,那么在意识上你也不会在意数据的合理性,因此容易忽视一些潜在的问题。

3. 灰度和热修复

灰度的机制是,在用户群体中选择部分用户进行应用更新提示的推送。这要求应用本身支持自动更新,同时需要对推送的达到率、用户的更新率进行统计。需要前后端一套机制配合。灰度有助于提前发现应用中存在的问题,这对超大型应用非常有帮助,毕竟,现在上架之后发现问题再修复的成本非常高。

但如果上架之后确实出现了问题就需要走热修复流程。热修复的难点在于热修复包的下发,同时还需要审核流程,因此需要搭建一个平台。这里涉及的细节比较多,后面有时间再梳理吧。

4. 配置下发

配置下发就是通过平台录入配置,推送,然后在客户端读取配置信息。这也是应用非常灵活的一个功能,可以用来下发比如固定的图片、文案等。我之前做个人开发的时候也在服务器上做了配置下发的功能,主要用来绕过某些应用商店的审核,但是在数据结构的抽象上做得比较随意。这里梳理下配置下发的细节。

  • 首先,下发的配置是区分平台特征的。这包括,应用的目标版本(一个范围)、目标平台(Android、iOS、Web、H5 或者 RN)。
  • 其次,为了适应组件化开发,也为了更好地分组管理,下发的配置命名时采用 模块#配置名称 的形式。
  • 最后,下发的数据结构支持,整型、布尔类型、浮点数、字符串和 Json.

我自己在做配置下发的时候还遇到一个比较棘手的问题——多语言适配。国内公司的产品一般只支持中文,这方面就省事得多。

5. 复盘文化

对于敏捷开发,复盘是不可或缺的一环。有助于及时发现问题,纠正和解决问题。复盘的时间可以是定期的,在一个大需求上线之后,或者出现线上问题之后。

3、技术特点

3.1 组件化开发的痛点

在大型应用开发过程中,组件化开发的意义不仅局限于代码结构层面。组件化的作用体现在以下几个层面:

  • 1). 团队配合的利器。想想几十个人往同一份代码仓库里提交代码的场景。组件化可以避免无意义的代码冲突。
  • 2). 提高编译效率。对于大型应用,全源码编译一次的时间可能要几十分钟。将组件打包成 aar 之后可以减少需要编译的代码的数量,提升编译效率。
  • 3). 适应组织架构。将代码细分为各个组件,每个小团队只维护自己的组件,更方便代码权限划分。

那么,在实际开发过程中组件化开发会存在哪些问题呢?

1. 组件拆分不合理

这在从单体开发过渡到组件化开发的应用比较常见,即组件化拆分之后仍然存在某些模块彼此共用,导致提交代码的时候仍然会出现冲突问题。冲突包含两个层面的含义,一是代码文件的 Git 冲突,二是在打包合入过程中发布的 aar 版本冲突。比较常见的是,a 同学合入了代码到主干之后,b 同学没有合并主干到自己的分支就打包,导致发布的 aar 没有包含最新的代码。这涉及打包的问题,是另一个痛点问题,后面再总结。

单就拆分问题来看,避免上述冲突的一个解决办法是在拆分组件过程中尽可能解耦。根据我之前的观察,存在冲突的组件主要是数据结构和 SPI 接口。这是我之前公司没做好的地方——数据结构仓库和 SPI 接口是共用的。对于它们的组件化拆分,我待过的另一家公司做得更好。他们是如下拆分的,这里以 A 和 B 来命名两个业务模块。那么,在拆分的时候做如下处理,

模块:A-api
模块:A
模块:B-api
模块:B

即每个业务模块拆分成 api 和实现两部分。api 模块里包含需要共享的数据结构和 SPI 接口,实现模块里是接口的具体实现。当模块 A 需要和模块 B 进行交互的时候,只需要依赖 B 的 api 模块。可以参考开源项目:arch-android.

2. 打包合入的痛点

上面我们提到了一种冲突的情况。在我之前的公司里,每个组件有明确的负责人,在每个迭代开发的时候,组件负责人负责拉最新 release 分支。其他同学在该分支的开发需要经过负责人同意再合入到该分支。那么在最终打包的过程中,只需要保证这个分支的 aar 包含了全部最新的代码即可。也就是说,这种打包方式只关心每个 aar 的版本,而不关心实际的代码。因为它最终打包是基于 aar 而不是全源码编译。

这种打包方式存在最新的分支代码没有被打包的风险。一种可行的规避方法是,在平台通过 Git tag 和 commit 判断该分支是否已经包含最新代码。此外,还可能存在某个模块修改了 SPI 接口,而另一个模块没有更新,导致运行时异常的风险。

另一个公司是基于全源码编译的。不过,全源码编译只在最终打包阶段或者某个固定的时间点进行,而不是每次合入都全源码编译(一次耗时太久)。同时,虽然每个模块有明确的负责人,但是打包的 aar 不是基于当前 release 分支,而是自己的开发分支。这是为了保障当前 release 分支始终是可用的。合并代码到 release 分支的同时需要更新 aar 的版本。但它也存在问题,如果合并到 release 而没有打包 aar,那么可能导致 release 分支无法使用。如果打包了 aar 但是此时其他同学也打包了 aar,则可能导致本次打包的 aar 落后,需要重新打包。因此,这种合入方式也是苦不堪言。

有一种方法可以避免上述问题,即将打包和合入事件设计成一个消息队列。每次合入之前自动化执行上述操作,那么自然就可以保证每次操作的原子性(因为本身就是单线程的)。

对比两种打包和合入流程,显然第二种方式更靠谱。不过,它需要设计一个流程。这需要花费一点功夫。

3. 自动化切源码

我在之前的一家公司开发时,在开发过程中需要引用另一个模块的修改时,需要对另一个模块打 SNAPSHOT 包。这可行,但有些麻烦。之前我也尝试过手动修改 settings.gradle 文件进行源码依赖开发。不过,太麻烦了。

后来在另一个公司里看到一个方案,即动态切换到源码开发。可以将某个依赖替换为源码而只需要修改脚本即可。这个实践很棒,我已经把它应用到独立开发中。之前已经梳理过《组件化开发必备:Gradle 依赖切换源码的实践》.

3.2 大前端化开发

1. React Native

如今的就业环境,哪个 Android 开发不是同时会五六门手艺。跨平台开发几乎是不可避免的。

之前的公司为什么选择 React Native 而不是 Flutter 等新锐跨平台技术呢?我当时还刻意问了这个问题。主要原因:

  • 1). 首先是 React Native 相对更加成熟,毕竟我看了下 Github 第一个版本发布已经是 9 年前的事情了,并且至今依旧非常活跃。
  • 2). React Native 最近更新了 JavaScript 引擎,页面启动时间、包大小和内存占用性能都有显著提升。参考这篇文章《干货 | 加载速度提升15%,携程对RN新一代JS引擎Hermes的调研》.
  • 3). 从团队人才配置上,对 React Native 熟悉的更多。

React Native 开发是另一个领域的东西,不在本文讨论范围内。每个公司选择 React Native 可能有它的目的。比如,我之前的一家公司存粹是为了提效,即一次开发双端运行。而另一家公司,则是为了兼顾提效和动态化。如果只为提效,那么本地编译和打包 js bundle 就可以满足需求。若要追求动态化,就需要搭建一个 RN 包下发平台。实际上,在这个公司开发 RN 的整个流程,除了编码环节,从代码 clone 到最终发布都是在平台上执行的。平台搭建涉及的细节比较多,以后用到再总结。对于端侧,RN 的动态化依赖本地路由以及 RN 容器。

2. BFF + DSL

DSL 是一种 UI 动态下发的方案。相比于 React Native,DSL 下发的维度更细,是控件级别的(而 RN 是页面级别的)。简单的理解是,客户端和后端约定 UI 格式,然后按照预定的格式下发的数据。客户端获取到数据之后渲染。DSL 不适合需要复杂动画的场景。若确实要复杂动画,则需要自定义控件。

工作流程如下图中左侧部分所示,右侧部分是每个角色的责任。

DSL workflow.drawio.png

客户端将当前页面和位置信息传给 DSL 服务器。服务器根据上传的信息和位置信息找到业务接口,调用业务接口拉取数据。获取到数据后根据开发过程中配置的脚本对数据进行处理。数据处理完成之后再交给 DSL 服务器渲染。渲染完成之后将数据下发给客户端。客户端再根据下发的 UI 信息进行渲染。其中接口数据的处理是通过 BFF 实现的,由客户端通过编写 Groovy 脚本实现数据结构的转换。

这种工作流程中,大部分逻辑在客户端这边,需要预埋点位信息。预埋之后可以根据需求进行下发。这种开发的一个痛点在于调试成本高。因为 DSL 服务器是一个黑盒调用。中间需要配置的信息过多,搭建 UI 和编写脚本的平台分散,出现问题不易排查。

总结

所谓他山之石,可以攻玉。在这篇文章中,我只是选取了几个自己印象深刻的技术点,零零碎碎地写了很多,比较散。对于有这方面需求的人,会有借鉴意义。