腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

腾小云导读

作为一个天然跨渠道的产品,腾讯会议从榜首行代码开始,团队就坚持同源同构的思维,即同一套架构,同一套代码,服务一切场景。曩昔一年,腾讯会议,迭代优化了 20000 个功用,安稳支撑了数亿用户,其客户端仅上层事务逻辑代码就超 100 万行,经过优化,现在在 Windows 渠道上的编译时刻最快缩短到 10秒,成为行业 C++ 跨渠道项目的标杆。本文将具体介绍背面的优化逻辑,希望给业界同行供给参阅。

看目录,点收藏

1 编译加快有哪些方向?

2 怎么高雅的预编译 Module 产品?

2.1 构建在哪履行

2.2 怎么增量发布产品

2.3 预编译产品上传到何处

2.4 怎么运用预编译产品

3 发布 Module 产品

4 运用Module产品

4.1 匹配产品

4.2 CMake产品替换源码编译

4.3 主动Generate

4.4 半主动Generate

4.5 IDE显现源码

5 断点调试

5.1 Android产品替换

5.2 成也Maven,败也Maven

5.3 Android Studio显现产品源码

6 万物皆可增量

7 构建参数

8 总结

01、 编译加快有哪些方向?

咱们知道,编译是将源代码经过编译器的预处理、编译、汇编等进程,生成能够被核算机直接履行的机器码的进程,而这个进程是非常耗时的。

惯例的开发东西如 xcode、gradle 为了提高功率都会自带编译缓存的功用,即将上一次编译的成果缓存起来,关于没有修正的代码再次编译就直接运用缓存。

但此这些缓存文件一般存在于本地,更新代码后不免需求一次重编,生成新的编译缓存。在会议这样一个上百人的团队里,修正提交非常频频,更新一次代码所需求重编的代码量往往是非常巨大的。特别是一些被深度依靠的头文件被修正,往往等价于需求全量编译了。

尽管也有一些东西能够支撑云端同享编译缓存,如 gradle 的 remote build cache,可是对 C++ 部分并没有 cache,而且计划也不能跨渠道通用。

腾讯会议阅历了结构 3.0 的模块化改造后,原本一整块代码依照事务拆分出了若干个小模块,开发需求修正代码逐步会集在模块内部。这为咱们的编译加快供给了新思路:每个事务模块之间是不存在依靠联系的,那么开发没有修正的模块是否能够免编译呢?

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

那么想要模块免编译,可行的计划有两种:

  • Module 独立运转,即不依靠其他 module 事务代码,恣意一个 module 能够单独调试运转,这样就不需求编译其他 module 了;

  • Module 预编译,将一切 module 预先编译好,本地开发直接下载预先编译的 module 产品,在编译完整app的时分直接组装即可,相似云端缓存的概念。

从长远来看,假如 Module 独立运转必定是最优的,可是现阶段比较难完成,尽管会议的模块代码没有彼此依靠,但事务功用间的彼此依靠仍是较高,模块要独立运转很难跑通完整功用;而 Module 预编译计划在会议项目中的可行性更高,不需求改动事务逻辑。

02、 怎么高雅的预编译 Module 产品?

那么既然要预编译一切的 module,就需求有一个机器主动构建 module 产品,并上传构建产品到云端。本地编译时从云端拉取预先编译好的产品来加快APP 编译。

那么,这儿有几个问题需求承认:

1.构建在哪里履行;

2.怎么增量发布产品;

3.预编译产品上传到何处;

4.怎么运用预编译产品

2.1 构建在哪履行

首要,产品构建需求一台机器主动触发,很天然会想到继续集成(Continuous Integration,简称 CI)机器,这儿咱们挑选了 Coding CI 来主动触发构建。首要来看看会议的开发形式:

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

会议的开发形式是从主分支拉子分支开发新需求,开发完结后再合入骨干。那么 CI 应该在哪条流水线构建 module 产品呢?需求为每条流水线都构建吗?

假如在 master 流水线构建:那么开发分支一旦 module 代码有修正,module 缓存就失效了。只改动一两个 module 还好,但假如是 module 悉数失效(比方 module 依靠的公共接口有改动),那增量构建的逻辑就形同虚设了。

假如在 feature/bugfix 流水线构建:开发分支那么多,每个分支流水线都跑一次 module 构建,时刻、库房的存储本钱都激增。而且,每个分支的构建产品彼此独立,本地假如想要用产品加快编译,就必须得先发动流水线跑一次,等预编译产品构建完结了才能够运用。这关于拉一个 bugfix 分支修正两行代码就修复一个 bug 的场景来说是不可接受的。

2.1.1 有没有愈加高雅的方法呢?

这儿首要剖析一下 module 之间的的联系,咱们的module 彼此之间代码是没有依靠的,module 共同依靠一些根底代码,咱们称之为 module API。其实,大多数状况下,module 构建出来的产品,关于其他分支来说,只需module API 没有改动便是能够复用的。那怎么最大化的利用上这些产品呢?这取决于怎么办理 module 产品的版别号,只需分支代码有可用的版别号就能够复用产品。

比方,从 master 拉一个 bugfix 分支,只需求改几行代码,就能够直接用master 的版别号就行了。假如是 feature 分支,一开始也是从 master 承继module 的版别号,跟着功用开发的进行,当咱们修正了 module A 的代码,再手动更新一下 module A 的版别号,最后跟着 feature 一同合入 master。

这样一套流程好像可行,仅仅实践操作起来会给开发者带来一定的运用本钱,因为需求开发者手动办理 module 的版别号。

试想一下,假如有两个 feature 分支都一起修正了同一个 module 的代码,那他们都会去更新这个 module 的版别号,MR 的时分就会发生版别抵触:

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

这时就必须是 feature A merge feature B 的代码,然后从头更新 Module B的版别号再构建。或许一个 module 代码抵触还好,但假如是 module API 的修正了呢?那便是一切 module 都需求更新了,这是非常机械且令人头疼的!

某闻名大佬曾说过:“但凡重复的作业,我都希望交给机器来做”,这种显着重复的机械的作业,能否直接交给机器完结呢?

经过剖析不难发现,在构建参数一致的状况下,module 产品的版别号和咱们的代码是一一对应的,即只需 module 代码有修正那咱们就应该更新这个module 的版别号。那咱们有没有什么已有的东西契合这个属性来当作版别号呢?有!Git commit ID 不便是吗?

在咱们的认知里,commit ID 好像是反映整个项目一切代码的版别,但需求给每个 module 树立各自的版别记载,commit ID 能满足吗?熟悉 git 的人应该知道,git 能够经过指定参数来获取特定目录的提交记载。咱们能够以此为突破口,获取每个 module 的 commit ID 作为 module 的版别号:

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

这样,只需求输入 module 的代码目录,就能够推算出这个 module 代码对应的预编译产品版别号,那咱们就无需自己来办理版别号了,交给 git 办理:

  • module 发布时,依据 module 目录得到 commit ID 作为版别号上传产品;

  • 本地拉取产品时,依据相同的规矩推算出 module 对应的版别号直接下载。

如此,就省掉了繁琐的版别号维护流程。也正因为 module 版别号是 commit ID,不同分支只需 module 的代码没有变,那 commit ID 也就不会变,不同分支间的 module 产品也就能够复用。

因而,咱们能够只需在 master 或许 master&feature 分支触发产品的构建,就能掩盖绝大多数分支。

2.2 怎么增量发布产品

承认了运用 CI 来构建产品后,然后能够经过代码提交来主动触发 CI 发动。但为了防止糟蹋构建机资源,并不需求每次都构建发布一切模块,仅增量的发布修正过的模块即可。

那怎么判别模块是否修正过呢?与获取 module 版别号的方法相似,咱们能够运用指令:git diff — 来找出本次构建有修正的模块。

2.3 预编译产品上传到何处

CI 构建出来的产品,需求一个库房来保存,“善解人意”的腾讯软件镜像源为咱们供给了各种库房:Maven、Generic、CocoaPods。Android 有 Maven,上传、下载、办理版别非常便当,可惜其他渠道并不支撑。依照腾讯会议的一向思路,就得考虑跨渠道的方法来上传、下载产品。

终究咱们挑选了原始的腾讯云 Generic 库房。相较于其他镜像库房,Generic 库房具有以下优势:

  • 操作更自在,HTTP/HTTPS 协议上传、下载、删除

  • 支撑自行办理上传文件途径

  • 存储空间无限制

所以,咱们自界说了一套产品打包、存储的标准,将各端构建好的产品,自己造轮子完成上传、下载、校验、解压装置等功用。正所谓:“造轮子一时爽,一向造轮子一向爽”。

2.4 怎么运用预编译产品

为了让开发者无学习本钱的运用预编译产品,产品的匹配和编译切换最好是无感的。即开发者并不需求主动装备,编译时脚本会主动匹配可用的预编译产品来构建 APP。

以 module A 为例,主动匹配产品的大致流程如下:

1.经过 git diff 查询当时的 changes list

2.有修正到 module A 的代码,脚本就主动切换到 module A 的源码编译;

3.没有修正 module A 的代码,则主动挑选 module A 的产品构建。

大致方向承认了,接下来便是具体的实施细节了。

03、 发布 Module 产品

首要,发布产品需求承认的是预编译产品结构是怎样的。为了脚本逻辑能够跨渠道,咱们将每个模块输出的产品一致命名标准为:xx_module_output.zip,也便是各渠道将自己每个 module 的产品打包到一个 zip 包中。可是 zip 包并不能反映产品的具体信息,比方对应的版别号、时刻等,因而还需求一个 manifest 文件来汇总一切产品的相关信息。那么一个版别的代码对应的产品有:

  • xx_module_output.zip:xx_module****的编译产品,一共会有n个xx_module_output.zip(n为模块个数);

  • base_manifest.json:当时版别的一切 module 产品信息,包括 module 名字、版别号等。

而 base_manifest.json 里保存的信息结构如下:

{
  "modules": [
    {
      "name": "account", // 模块名称
      "version": "7b2331b7e9", // 模块版别号,即模块commit ID
      "time": "1632624109"    // 模块版别时刻,即模块commit时刻戳
    },
    {
      "name": "audio",
      "version": "7b2331b7e9",
      "time": "1632624109"
    },],
  "appVersion": "7b2331b7e9", // 代码库git commit ID
  "appVersionTime": "1632624109"// 代码库commit时刻戳}

然后,在有代码提交时,主动触发 CI 发动 Module 发布脚本,经过 git diff 找到提交的 change list,然后从 changes list 来判别哪些 module 有改动,对改动的 module 重编发布:

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

流程看起来并不杂乱,但实践操作上确有很多问题值得推敲。比方:咱们知道git diff 是一个比照指令,既然是比照就会有一个基准 commit ID 和方针commit ID,方针 commit 就取当时最新的 commit 就好了。但基准 commit应该取哪个呢?是上一个提交吗?来看下面这个开发流程:

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

开发者从 master 拉取了一个分支修复 bug,本地发生了两次 commit但没有 push,最后走 MR 流程合入骨干。整个进程发生了三个 commit,假如直接运用最近一次的 commit 来 diff 发生成果,那么 diff 的 commit 是最后的那次 merge commit,成果正好是这次 bugfix 的一切改动记载。也就不用管前面的 commit a、commit b 了,这样看起来运用最近一次 commit diff 好像没有问题。

但假如这次编译被越过或许失利了,那么下一次的 MR 还只重视本次 MR 的提交内容,中心越过的代码提交就很或许一向没有对应构建产品了。

因而,咱们是经过查找最近一次有 base_manifest.json 文件与之对应的merge commit 来作为基准 commit。 即从当时 commit 开始,回溯之前一切的merge commit,假如能够找到merge commit对应的base_manifest.json,那就阐明这次 merge commit 是有发布 module 的,那么就以它为基准核算 diff。当然,咱们并不会无限制的往前回溯,在测验回溯了 n 次后仍然没有找到,则以为没有发布。

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

其次,要怎么 diff 特定 module 代码呢?

前面提到 git diff 能够经过参数指定目录,依据这个特性,传入特定的 module 目录,就能够核算特定 module 的 change list 了:

git diff targetCommitId baseCommitId -- path/to/module/xxx_module#获取module的diff(v1)

那么,只需包含这个 module 本身的代码目录途径就能够了吗?

答案是不够的。因为 module 还会依靠其他的接口代码,如 module API 的,接口的改动也会影响到 module 的编译成果,因而还需求包含 module API 的目录才行。所以获取 module diff 就变成下面这样:

git diff targetCommitId baseCommitId -- path/to/module/xxx_module path/to/module-api #获取module的diff (v2)

另外,在 module 目录中,有些无关的文件并不影响编译成果(比方其他端的UI代码),在核算 diff 时咱们需求将其排除,怎么做到呢?也是经过途径,与之前不同的是需求在途径前面加”:!“。比方以下指令以Android 为例,咱们需求将其他端的 UI 代码排除掉,那么获取某个 module 的 diff 指令终究就变成了这样:

git diff targetCommitId baseCommitId --name-only-- path/to/module/xxx_module path/to/module-api :!path/to/module/xxx_module/ui/Windows :!path/to/module/xxx_module/ui/Mac :!path/to/module/xxx_module/ui/iOS #获取module的diff (final)

相同的,在发布 module 时,需求供给一个版别号,前面现已提到,能够运用module 的 commit ID 作为版别号。那么要怎么获取 module 的 commit ID呢?git 指令都支撑传入参数,那么经过 git log – 设置 module 相关目录,即可得到这个 module 的 commit ID。

不难发现,git log 指令的应该和 diff 是一致的,那么咱们能够得出获取 module version 的指令:

git diff targetCommitId baseCommitId --name-only-- path/to/module/xxx_module path/to/module-api :!path/to/module/xxx_module/ui/Windows :!path/to/module/xxx_module/ui/Mac :!path/to/module/xxx_module/ui/iOS #获取module的diff (final)

承认了 diff 与获取 module 版别号的算法,发布流程根本就能够走通了,接下来便是怎么运用产品。

04、运用Module产品

首要需求承认的是,当时的代码要怎么判别 CI 是否有发布与之匹配的 module产品。

4.1 匹配产品

前面咱们提到在发布产品时,是经过回溯查找每个commit对应的base_manifest.json 来承认最近一次发布的 commit。那么匹配当时可用的产品也是相似的逻辑,经过回溯来找到最近有发布的 commit,整个 module 增量构建的流程如下:

  • 经过回溯 commit ID 找到最近一次发布的 base_manifest.json。

  • 若终究没有找到这个 base_manifest.json,则证明当时版别没有 module 产品,一切 module 需求源码编译;

  • 若能够找到此文件,文件中记载了预编译 module 的产品信息(版别、时刻戳等)列表,假如能在产品列表中找到这个 module,那么就能够获取这个module 对应的产品;

  • 得到 base_manifest.json 里的产品信息后,还需求运用产品的版别号 diff 判别出当时 module 是否有代码修正,承认无修正的状况则运用产品打包 App,有修正则运用 module 源码编译。

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

这儿判别 module 是否修正的 diff 算法与发布产品时相似,以产品的版别号为base commit、设置 module 的目录履行 git diff 指令,来得到module 的 change list。

产品匹配下载成功后,便是运用预编译产品来替换源码编译了。本着无运用本钱的准则,咱们希望替换进程能够脚本主动化完结,不需求开发者关怀和介入就能无缝切换。

4.2 CMake产品替换源码编译

会议的跨渠道层代码运用 C++ 完成,并选用 CMake 来安排工程结构的,所以C++ 代码的产品替换,需求从 CMake 文件下手。

首要,C++ 预编译的产品是动态/静态库,这些关于 CMake 来讲便是library,能够经过 add_library() 或许 link_directories() 函数将其作为预编译库增加进来,以动态库 xx_plugins 为例,增量脚本会依据匹配的产品,会生成一个use_library_flag.cmake 文件,用来符号射中增量的库:

# use_library_flag.cmake
set(lib_wemeet_plugins_bin "/Users/jay/Dev/Workspaces/wemeet/app/app/.productions/Android/libraries/wemeet_plugins")
set(lib_wemeet_sdk_bin "/Users/jay/Dev/Workspaces/wemeet/app/app/.productions/Android/libraries/wemeet_sdk")
...
set(lib_xxx patch/to/lib_xxx)

咱们在运用 xx_plugins 的方法上做了改动:

  • 射中增量时,经过 add_library 导入这个预编译的产品作为 library,lib_app link 预编译库;

  • 未射中增量时,经过 add_subdirectory 增加 xx_plugins 的源码目录,lib_app link 源码库;

那么,增量产品射中后要完成产品/源码的切换,是不是只需求从头生成use_library_flag.cmake 这个文件就能够了呢?

先来看看 CMake 的运用流程,主要分为 generate 和 build 这两个进程:

  • generate – 依据 cmake 脚本中的装备承认需求编译的源码文件、链接库等,生成适用于不同构建体系(makefile、ninja、xcode 等)的工程文件、编译指令。

  • build – 运用 generate 生成的编译指令履行编译

关于 Android 来说,cmake 是属于 gradle 办理的一个子编译体系,在构建Android 的时分 gradle 会履行 cmake generate 和 build。

但关于 Xcode和 Visual Studio,cmake 修正之后是需求手动履行 generate的,原因是因为点击 IDE 的 build 按钮后仅仅是履行 build 指令,IDE 不会主动履行 cmake generate。

那么,增量射中的产品列表有更新时,需求开发者手动履行 generate 一次才能更新工程结构,切换到咱们预期的编译途径。但这样开发者就需求关怀增量产品匹配状况是否有改动,增加了运用本钱。

有没有方法将这个进程主动化呢?

4.3 主动Generate

技能前提:

cliutils.gitlab.io/modern-cmak…

cmake 供给了运转其他程序的能力,既包含装备时,也包含构建时。关于Windows端咱们能够刺进一段脚本,在编译前做主动 generate 检测:

if(WIN32)
# https://cliutils.gitlab.io/modern-cmake/chapters/basics/programs.html
    add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/running_command_at_build_time_generated.trickier"
      COMMAND node ${CMAKE_CURRENT_SOURCE_DIR}/build_project/auto_generate_proj.js
        ${LIB_OS_TYPE}
        ${windows_output_bin_dir}
        ...
        )
    add_custom_target(auto_generate_project ALL
    DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/running_command_at_build_time_generated.trickier")
endif()

脚本会比照上一次构建的产品射中模块,当射中模块的列表有改动时,则发动子进程调用cmd窗口履行 Windows 的 generate:

const cmd = `start cmd.exe /K call ${path.join(winGeneratePath, generateStr)}`;
child_process.exec(cmd, {cwd: winGeneratePath, stdio: 'inherit'});
process.exit(0);

注意: 运用 node 触发 cmd 履行 generate 脚本时,需求运用 detached 进程的方法,并使需求进程 sleep 满足时刻以等待脚本履行结束。

4.4 半主动Generate

关于 iOS 和 OS X 渠道,也能够 在 xcode 的 Pre-actions 环节刺进一段脚本,来检测模块的射中列表是否有改动:

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

但因为 xcode 本身检测到工程结构改动会主动停止编译,因而咱们经过弹窗提示开发者,当检测到射中产品的模块现已更改时,需求手动 generate 更新工程结构。

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

4.5 IDE显现源码

产品/源码切换编译的问题处理了之后,咱们也发现了了新的问题:在xx_plugins 射中增量产品时,发现 IDE 找不到 xx_plugins 的源码了!

这是因为前面改造 CMakeLists.txt 脚本时,射中增量的状况下,并不会去履行 add_subdirectory(xx_plugins),那 IDE 天然不会索引 xx_plugins 的源码了,这明显是非常影响开发体会的。要处理这个问题就必须射中增量时也履行 add_subdirectory(xx_plugins) 增加源码目录,可增加了源码目录就会去编译它,那么能够让它不编译吗?

答案是必定的!咱们来看看 cmake –build 的文档:

Usage: cmake --build <dir> [options] [-- [native-options]]
Options:
  <dir>          = Project binary directory to be built.
  --parallel [<jobs>], -j [<jobs>]
                 = Build in parallel using the given number of jobs.
                   If <jobs> is omitted the native build tool's
                   default number is used.
                   The CMAKE_BUILD_PARALLEL_LEVEL environment variable
                   specifies a default parallel level when this option
                   is not given.
  --target <tgt>..., -t <tgt>...
                 = Build <tgt> instead of default targets.
  --config <cfg> = For multi-configuration tools, choose <cfg>.
  --clean-first = Build target 'clean' first, then build.
                   (To clean only, use --target 'clean'.)
  --verbose, -v = Enable verbose output - if supported - including
                   the build commands to be executed.
  -- = Pass remaining options to the native tool.

文档中提到 cmake build 指令是能够经过–target 参数来设置需求编译的target,而 target 则是经过 add_library 界说的代码库,默认状况下 cmake会 build 一切的 target。但假如咱们指定了 target 后,那么 cmake 就只会编译该 target 及 target 依靠的库。

在会议项目中 lib_app 依靠了其他一切的增量库,属于依靠联系中的顶层library,因而咱们的 build 指令能够加上参数–target lib_app,那么:

  • 当 xx_plugins 未射中增量时,因为 lib_app 依靠了 xx_plugins 源码库,cmake 会一起编译 lib_app 与 xx_plugins;

  • 当 xx_plugins 射中增量时,lib_app 依靠 xx_plugins 的是预编译库,cmake就只会编译 lib_app了。

因而,咱们能够进一步改造CMakeLists.txt,让add_subdirectory(xx_plugins)始终履行:

# CMakeLists.txt
include(use_library_flag.cmake)
...
# 引进wemeet_plugins源码目录
add_subdirectory(wemeet_plugins)
if(lib_wemeet_plugins_bin) # 射中增量
  # 则导入该lib
  add_library(prebuilt_wemeet_plugins SHARED IMPORTED)
  set_target_properties(${prebuilt_wemeet_plugins}
                        PROPERTIES IMPORTED_LOCATION
                        ${lib_wemeet_plugins_bin}/${LIB_PREFIX}wemeet_plugins.${DYNAMIC_POSFIX}) # DYNAMIC_POSFIX: so、dll、dylib
  set(shared_wemeet_plugins prebuilt_wemeet_plugins) # 设置为预编译库
else() # 未射中增量
  set(shared_wemeet_plugins wemeet_plugins) # 设置为源码库
endif()
...
# 引用wemeet_plugins
target_link_libraries(wemeet_app_sdk PRIVATE ${shared_wemeet_plugins})

这样在 xx_plugins 射中增量的状况下,开发者也能够继续用 IDE 愉快的阅览、修正源码了。

05、断点调试

运用增量产品代替源码编译一起会带来的另一个问题:lldb 的断点调试失效了!

要处理这个问题,首要要知道 lldb 二进制匹配源码断点的规矩:lldb 断点匹配的是源码文件在机器上的绝对途径!(win 端没有用 lldb 调试器没有这个问题,只需 pdb 文件和二进制放在同级目录就能够主动匹配)

那么,在机器 A 上编译的二进制产品 bin_A 因为源码文件途径和本地机器B上的不一样,在机器 B 上设置的断点,lldb 就无法在二进制 bin_A 中找到与之对应位置。

那有方法能够处理这个问题吗?在 lldb 内容有限的文档咱们发现这样一个指令:

settings settarget.source-map/buildbot/path/my/path

其作用便是将本地源码途径与二进制中的代码途径做一个映射,这样lldb就能够正确找到代码对应的断点位置了。那么“药”找到了,怎么“服用”呢?

首要,咱们会有多个库别离编译成二进制发布,而且由所以增量发布,各个库的构建机器的途径或许都不一样,因而需求为每个库都设置一组映射联系。好在source-map是能够支撑设置多组映射的,因而咱们的映射指令演变成了:

settings set target.source-map /qci/workspace_A/path_to_lib1 /my_local/path_to_lib1
      /qci/workspace_B/path_to_lib2 /my_local/path_to_lib2
      /qci/workspace_C/path_to_lib3 /my_local/path_to_lib3
      ...
      /qci/workspace_X/path_to_libn /my_local/path_to_libn

指令有了,何时履行呢?需求手动履行吗?仍然仍是无运用本钱的准则,咱们希望脚天性主动化完结这些繁琐的工作。

了解 lldb 的开发者想必都知道“~/.lldbinit”这个装备文件,咱们能够在履行增量脚本的时分,把 source-map 装备增加到“~/.lldbinit”中,这样 lldb 发动的时分就会主动加载,可是这儿的装备是在用户目录下,会对一切 lldb 进程生效。为了防止对其他库房/项目代码调试形成影响,咱们应该缩小装备的作用范围,xcode 是支撑项目等级的 .lldbinit 装备,也便是能够将装备放到 xcode 的项目根目录:

# Mac端的.lldbinit放到Mac的xcode项目根目录:
app/Mac/Src/App/Application/WeMeetApp/.lldbinit
# iOS端的.lldbinit放到iOS的xcode项目根目录:
app/iOS/Src/App/Application/WeMeetApp/.lldbinit

Android Studio(简称 AS)就没有这么人道化了,并不能主动读取项目根目录的 .lldbinit 装备,但能够在 AS 中手动装备一下 LLDB Startup Commands:

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

手动装备尽管形成了一定运用本钱,但还好只需求装备一次。

5.1 Android产品替换

Android 中的子模块因为包含了 Java 代码和资源文件,预编译的产品就不是动态库/静态库了,产品替换得从 gradle 下手。

前面文章有提到,为了更好的跨渠道,咱们挑选了 Generic 库房来存储增量构建的产品。熟悉Android 的开发者都知道,Android 渠道集成预编译产品的方法有两种:

  • 本地文件集成,如 aar、jar 文件

  • maven 集成

假如挑选本地文件集成,那么咱们就需求将模块源码打包成 aar 文件,但会遇到一个问题:若模块选用 maven 集成的方法依靠了三方库,是不会包含在终究打包的 aar 文件中的,这就会导致产品集成该模块时丢掉了一部分代码。而Google 推荐的集成方法都是 maven 集成,因为 maven 产品中的 pom.xml文件会记载模块依靠的三方库,便当办理版别抵触以及重复引进等问题。

那么怎么在 Generic 库房中运用 maven 集成呢?Generic 库房其实便是一个只供给上传、下载的文件存储服务器,文件上传、下载均需自行完成,因而,每一个模块产品的文件内容各端能够自行界说,终究打包成一个紧缩包上传到库房即可。那么关于 Android 端咱们能够将每个模块产品依照本地maven库房的文件格局进行打包发布:

app/.productions/Android/repo/com/tencent/wemeet/module/chat/
├── f4d57a067d
│ ├── chat-f4d57a067d-prebuilt-info.json
│ ├── chat-f4d57a067d.aar
│ ├── chat-f4d57a067d.aar.md5
│ ├── chat-f4d57a067d.aar.sha1
│ ├── chat-f4d57a067d.aar.sha256
│ ├── …
│ ├── chat-f4d57a067d.pom
│ ├── chat-f4d57a067d.pom.md5
│ ├── chat-f4d57a067d.pom.sha1
│ ├── chat-f4d57a067d.pom.sha256
│ └── chat-f4d57a067d.pom.sha512
├── maven-metadata.xml
├── maven-metadata.xml.md5
├── maven-metadata.xml.sha1
├── maven-metadata.xml.sha256
└── maven-metadata.xml.sha512

以上便是模块 chat 以 maven 格局进行发布产品的文件列表,能够看到该库房中只有一个版别(版别号:f4d57a067d)的产品,也便是说每个版别的增量产品其实便是一个 maven 库房,咱们将产品下载下来解压后,经过引进本地maven库房的方法增加到项目中来:

// build.gradle
repositories {
    maven { url "file://path/to/local/maven" }
}
dependencies {
    implementation 'com.tencent.wemeet.module:chat:f4d57a067d'
}

集成方法敲定了,那要怎么主动切换呢?gradle 本身便是脚本,那么咱们能够在增量脚本履行后,依据脚本的履行成果,射中产品的模块则以 maven 方法依靠,未射中的则以源码依靠。为了简化运用方法,咱们界说了一个 projectWm函数:

// common.gradle
gradle.ext.prebuilts = [:]
// setup prebuilt result
...
ext.projectWm = { String name ->
    String projectName = name.substring(1) // remove ":"
  if (gradle.ext.prebuilts.containsKey(projectName)) { // 射中增量产品
    def prebuilt = gradle.ext.prebuilts[projectName]
    return "com.tencent.wemeet.module:${projectName}:${prebuilt.version}"
  } else { // 未射中增量产品
    return project(name)
  }
}
// build.gradle
apply from: 'common.gradle' // 引进通用装备
...
dependencies {
    implementation projectWm(':chat')
  ...
}

projectWm 里边封装了替换源码编译的逻辑,这样咱们只需求将一切的project(“:xxx”)改成projectWm(“:xxx”) 即可,运用便当简单。

处理完替换的问题,就能够愉快的运用增量产品了?

5.2 成也Maven,败也Maven

尽管 maven 的依靠办理给咱们带来了便当,但关于产品替换源码编译的场景,也带了新的问题。看这样一个 case,有 A、B、C 三个模块,他们的依靠联系如下:

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

前面的 projectWm 计划,关于模块A这种单一模块能够很好的处理问题,但关于模块 B 依靠模块 C 这种杂乱的依靠联系却不适用。比方模块 B 射中增量、模块 C 未射中时,因为 B 运用 projectWm 替换成了 maven 依靠,而模块 C 会因为模块的 maven 产品中 pom.mxl 界说的依靠联系给带过来,也便是模块 C也会是 maven 依靠,而无法变成源码依靠。咱们必须将模块 B 附带的 maven依靠中的模块 C 再替换源码!经过查阅 gradle 文档,咱们发现 gralde 供给了dependencySubstitution功用能够将 maven 依靠替换成源码,用法也非常简单:

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

所以咱们能够将为射中的 module C 再替换成源码编译:

configurations.all {
    resolutionStrategy.dependencySubstitution.all { DependencySubstitution dependency ->
        if (dependency.requested instanceof ModuleComponentSelector) {
            def group = dependency.requested.group
            def moduleName = dependency.requested.module
            if (group == 'com.tencent.wemeet'|| (group.startsWith('application') && dependency.requested.version == 'unspecified')) {
                def prebuilt = gradle.ext.prebuilts[moduleName]
                if (prebuilt == null) { // module未射中
                    def targetProject = findProject(":${moduleName}")
                    if (targetProject != null) {
                        dependency.useTarget targetProject
                    }
                }
            }
        }
    }
}

替换好了本以为万事大吉了,但实践编译运转时,跟着射中状况的变化,常常偶发的失利:Could not resolve module_xx:

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

究其原因,仍是上面的替换没有起作用,替换的源码模块找不到,莫非 gradle供给的API有问题?经过仔细阅览文档,发现这样一段话:

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

意思便是Dependency Substitution 仅仅帮你把依靠联系改变过来了,但实践上并不会将这个 project 增加到构建流程中。所以咱们还得将源码编译的 project 手动增加到构建中:

// build.gradle
dependencies { implementation project(":xxx") }

那接下来的问题便是怎么找到编译 app 终究需求源码编译的 module,然后增加到 app 的 dependencies{}依 赖中。这就要求得拿到一切 module 的依靠联系图,这个并不困难,在gradle configure之后就能够经过解析configurations 获取。但问题是咱们必须得在 gradle configure 之前获取依靠联系,因为在 dependencies{} 中增加依靠是在 gradle configure 阶段生效的。

问题进入了堕入死循环,这样一来,咱们并不能经过 gradle的configure 成果获取依靠联系,得另辟蹊径。

前面提到过,maven 会将模块产品依靠的子模块写到 pom.xml 文件中,尽管pom.xml 里记载的依靠并不全,可是咱们能够将这些依靠联系拼凑起来:

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

假设咱们有上图的一个依靠联系,module A、B、C 都射中了增量,D、E 未射中,为了防止“Could not resolve module_xx”的编译错误,咱们需求将module D、E 增加到 app 的 dependencies{} 中,那么脚本中怎么承认呢:

  • app 在 configure 前能够读取 configurations 得倒 app 依靠了 module A、B;

  • 因为 module B 射中了增量,因而能够经过 B 的 pom.xml 文件找到 B 依靠了C、D;

  • 而 D 未射中增量,因而能够承认需求将 D 增加到 app 的的 dependencies{}中;

  • 同理,咱们能够经过 B → C 依靠链,拿到 C 的 pom.xml中记载的对E的依靠,从而承认E需增加到 app 的的 dependencies{} 中。

源码替换的流程,到这儿大致就走通了,不过除此之外还有其他替换相关的细节问题(如版别号一致、本地 aar 文件的依靠替换等),这儿就不继续展开讲了。

5.3 Android Studio显现产品源码

与 cmake 相似,射中产品的模块因为变成了 Maven 依靠,也会遇到 AS 无法正确索引源码的问题。

首要,AS 中加载的源码是在 Gradle sync 阶段索引出来的,而咱们用产品替换源码编译仅需求在 build 的时分生效。那是否能够在 sync 阶段让 AS 以为一切模块都未射中,去索引模块的源码,仅在真正 build才 做实践的替换呢?

答案是必定的,但问题是怎么判别 AS 是在 sync 或 build 呢?gradle 并没有供给 API,但经过剖析 gradle.startParameter 参数能够发现,AS sync 其实是发动了一个不带任何 task参数的gradle指令,而且将systemPropertiesArgs中的“idea.sync.active“参数设置为了 true,所以咱们能够以此为依据来区分 gradle 是否在 sync:

// settings.gradle 
gradle.ext.isGradleSync = gradle.startParameter.systemPropertiesArgs['idea.sync.active'] == 'true'
ext.projectWm = { String name ->
    String projectName = name.substring(1) // remove ":"
  if (gradle.ext.isGradleSync) { // Gradle Sync
    return project(name)
  }
    // 增量替换源码...
}

06、万物皆可增量

以上讲的是事务 module 的增量流程,会议的事务 module 之间是没有依靠联系的,结构比较清晰。那其他依靠联系更杂乱的子工程呢?

经过总结 module 的增量规律咱们发现,一个子工程要完成增量化编译,需求处理的一个核心问题是判别这个是否需求重编。而 module 是经过 git diff – 来判别,则是前面提到的 module 相关的代码途径:

  • 模块内本身的代码

  • 模块依靠的的接口代码

因而,这儿能够延伸一下,即承认了子工程的源码及其依靠的接口途径后,都能够经过这套流程来发布、匹配增量产品,完结增量化的接入。

07、构建参数

前面有说到,当构建参数一致的状况下,产品版别和代码版别是一一对应的。但实践状况是,咱们常常也需求修正构建参数,比方编译 release、debug 版别编译的成果往往会有很大差异。

那么关于构建参数不一致的场景,增量构建的产品要怎么匹配呢?

这儿引进 variant**(变体)**的概念,即编译的产品会因构建参数不同有多种组合,每一种参数组合构建出来的产品咱们称之为其间一种变体。尽管参数组合或许有n种,可是常用的组合或许就只有几种。每一种组合都需求从头构建和存储对应产品,本钱成倍增加,要掩盖一切的组合明显不太现实。

既然常用的组合就那么几种,那么只掩盖这几种组合射中率就根本达标了。不同构建参数组合的产品之间是不通用的,所以存储途径上也应该是彼此阻隔的:

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

上图示例中,兼容了 package type(debug、release 等)和publish channel(app、private、oversea s等)两种参数组合,实践场景或许更多,咱们能够依据需求进行定制。

08、总结

到这儿,现已叙述了腾讯会议运用增量编译加快编译的大致原理,其核心思维便是尽量少编译、按需编译。在本地能够匹配到远端预先编译好的产品时,就替代本地的源码编译以节省时刻。

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

接下来看下整体优化作用:

在悉数射中增量产品的状况下,因为省去了大量的代码编译,全量编译功率也大幅提升:

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

注:以上数据为2022年3月本地实测数据,实践耗时或许因机器装备不同而不一致。

截止到现在,会议代码全量编译耗时已超30min+,但因为选用模块化增量编译,在射中增量的状况下作用是安稳的,即编译耗时不会跟着代码量的增长而继续增加。

增量编译带来的功率提升是明显的,但现阶段也有一些不足之处:

1.产品射中率优化:现阶段产品射中率还不够高,当修正了公共头文件时容易导致射中率下降,但这种修正能够进一步细分,如当新增接口时,其实并不影响依靠它的模块射中。

2.主动获取依靠:现在工程依靠的联系是用装备文件人工维护的,因而会呈现依靠联系更新滞后的状况。后续能够测验从cmake、gradle等东西中获取依靠,主动更新装备。

以上是本次共享悉数内容,欢迎咱们在谈论区共享沟通。假如觉得内容有用,欢迎转发~

-End-

原创作者|何杰、郭浩伟、吴超、杜美依、田林江

技能责编|何杰、郭浩伟、吴超、杜美依、田林江

腾讯会议10秒编译百万代码|鹅厂编译加速标杆案例公开

跟着产品得到商场初步认可,事务开始规模化扩张。提速的压力从事务传导到研发团队,后者开始快速加人,希望功率能跟上事务的脚步,乃至带动事务开展。但不如人意的是,实践的研发效能反而或许与希望各走各路——事务越开展,研发越跑不动。其实影响开发功率的要素远远不止代码编译耗时过长!

你在开发进程中常常遇到哪些杂乱耗时的头痛问题呢?

欢迎在公众号谈论区聊一聊你的问题。在4月13日前将你的谈论记载截图,发送给腾讯云开发者公众号后台,可收取腾讯云「开发者春季限制红包封面」一个,数量有限先到先得。咱们还将选取点赞量最高的1位朋友,送出腾讯QQ公仔1个。4月13日中午12点开奖。快约请你的开发者朋友们一同来参加吧!

阅览原文