找回密码
 会员注册
查看: 16|回复: 0

哔哩哔哩Android打包优化与云编译

[复制链接]

2万

主题

0

回帖

7万

积分

超级版主

积分
72245
发表于 2024-10-6 23:10:11 | 显示全部楼层 |阅读模式
本期作者夏秋垒移动技术部工程效率组资深开发工程师01?背景介绍B站使用大仓模式进行源码依赖管理,大仓模有优势也有挑战,截止目前为止 Android 仓库子模块有620+,开发人员150+。本地开发存在编译慢、机器发热、卡死、阻塞开发等问题。介于此前移动端已有庞大的 CI 构建集群,我们探索出一种新的开发编译方式——云编译。02?原理简介俗话说性能不够硬件来凑,得益于公司的高配置服务器资源,我们移动端可以很方便地使用云端资源提高编译速度。通过 git 同步本地与远端代码基点,结合 diff 文件还原本地开发中产生的差异改动,然后编译出与本地无差别的 APK 产物。03?开荒时代使用 Docker 自定义的 Android 构建镜像,可快速复制多个打包机器实例。开发同学本地使用提供的云编译命令行工具执行编译动作,命令行工具开始计算 commit、生成 diff,然后合成打包请求发送到远端。远端收到编译请求以后,开始解析指令、下载(同步)代码、应用补丁、执行编译、返回执行结果与编译产物。编译成功后,本地下载编译产物(APK文件),然后安装并启动。整个流程与本地开发的差别为本地的代码需要同步到远程,打包的操作放在远程,远程执行成功后需下载产物。新开发方式有优势也有不足。优势:环境正确性支持并行并发编译速度加速解放本地机器(专注于逻辑编写)支持全源码编译 (非快遍模式,不使用缓存,全部使用源码)不足:增加学习成本部分任务需要本地编译,用于代码索引本地增量编译失效,仅使用远端缓存打包机升级与维护成本增加机器完全随机分配,会有竞争、等待情况出现缓存利用率不高,编译速度有提高的空间04?持续优化与VIP模式前期构建实例数为10个,可满足一部分人使用,一段时间后大家觉的这种模式还不错,相对于本地编译,编译速度还是有明显的提升。随着使用人数开始增多,开始出现机器竞争、机器繁忙、任务等待等问题。大家吐槽调侃希望可以开通 VIP 模式,独占某一台机器或者提高任务优先级。原先的架构,客户端与服务器之间只有一层 SLB 做反向代理,进行随机转发。前后两次打包任务可能分配的是不同机器,导致需要重新下载代码,增加打包时间,也无法复用上一次的增量编译缓存。于是我们针对原先的架构模式,做了以下调整,并对打包流程和速度进行了优化。优化打包速度,首先必须掌握整个打包流程与机制;其次需要衡量维度以及数据统计记录,方便后期数据对比,指导优化方向;最后为了满足日常问题的排查,需要一个管理后台记录打包日志、监控实例状态、修改配置与维护。?4.1?流程分析打包流程主要包括打包环境准备、服务启动、任务执行。以下针对各个阶段列举具体的优化措施。编译环境: 一般来说不经常改变,除非大版本升级、SDK 升级、流程改变等。Docker 镜像制作可以参考 Docker 官方文档来做参考,不过国内网络情况都懂的,最好使用网络代理或者镜像,来加速镜像制作时间。服务启动: 优化期间,发布频率较高。因为服务绑定 Docker 镜像,每次发布都需要重启 Docker 实例,导致一些缓存丢失,最好减少重启次数并增加缓存预热。执行任务: 流程固定,Gradle 有完整的生命周期,有很大的优化空间。?4.2?优化措施增加实例数量与提高并发前期每个服务配置为 10C50G,可以保证单人独占,效果明显。随着使用人数增多,会出现机器繁忙问题,增加机器数量与提供并发量迫在眉睫。后期改成高低配两种服务,10C50G 为单人模式,30C100G 为多人模式,最大可以支持3人并发打包,多人模式也可以共享缓存,加速效果明显。网络代理Docker 镜像下载,Android SDK & NDK, Gradle,Maven 等可以使用国内源或者公司内部源来加速,效果显著。举例,项目中一般会有 Gradle 各个版本下载,可以放在公司内部存储,内网速度一般为千兆网络,下载速度较快。避免实例重启默认 Docker 实例会直接启动打包,每次更新服务都需要更新 Docker 镜像版本,服务实例重启会导致所有的代码、编译缓存、SDK 等丢失。对打包速度较大,所以尽可能的减少服务重启次数。服务热更新但是如果遇到线上问题,发布版本是不可以避免,减少服务重启明显不科学。通过流程优化,使服务支持热更新,从而避免了重启Docker 实例,相关缓存也不会丢失,可继续使用。打包预热服务启动时,可以先挂起,不对外提供服务,系统内部进行预热处理,如预先下载或者更新,执行打包若干次,等预热完成后,再进行打包,也打包速度会相对较高的提升。代码仓库预热相对于 CI 服务,每次编译都会拉取代码,然后再进行编译。但是云编译不不适合此方式,B站大小仓代码总量大概为5个G,按照千兆网络来算,全部拉取也需要几分钟。可使用 git 提供 worktree 的模式,可以预先拉去所有代码,当需要打包时,可以快速切换代码。保留工作目录云编译根据用户名、机器设备号、本地工作目录三个维度计算一个hash,映射远程工作目录路径,这样每次可以快速还原本地代码,执行打包操作,完成后不删除代码供下次使用。Gradle Remote CacheGradle 提供一种 Remote Cache 机制,需要一个缓存服务器,第一次编译完成后,上传到缓存服务器,再次打包,如果代码没有修改,可以直接使用下载并使用缓存。智能调度与运维云编译提供管理后台与网关,可以根据用户打包频次,合理分配机器。用户每次执行打包,都会分配到指定机器的指定目录,提高缓存使用率,避免机器出现抢占情况。网络优化项目开发一般为 debug 模式,APK 是未经优化的大小约为150M,开发同学使用的是 MacBookPro,大部分使用的是 Wi-Fi(百兆网络),则下载需要15s左右。切换成有线网络(千兆网络),则下载只需1-2s即可。?4.3?优化结果随机模式:冷机打包 5-10min, 热机打包 3-7min,平均打包速度 5min。VIP模式:冷机打包 4-8min, 热机打包 1.5-4min,平均打包速度 3min,极端情况20s可出包。?4.4?系统展示打包记录实例列表在线日志05?分布式编译?5.1?需求分析随着业务发展子模块变多,部分任务执行时间越来越长,影响整体编译时长,编译时间具有劣化的趋势。常见耗时任务有 DexBuild 与 DexMerge,如下图所示为某次首次冷编译(本机无缓存),其中 dex 相关任务时长约占 1/3。再次编译的时候,DexBuild 有明显的下降,但是 DexMerge 任然需要不少的时间。从官网的编译流程图来,dex文件就是从jar或class文件通过指令转换而来,同时 Android Sdk 中也提供 d8 命令来手动执行。云编译系统是一个编译集群,每次一个编译记录只能占用一台主机,是否可以把一些比较耗时长的任务拆分到其他空闲机器协同来编译,然后再回传编译结果。以下为 AGP 中源码,通过传入的参数进行赋值准备,最后执行 D8.run(), 而 D8.run()?可以在SDK d8 工具中找到。package com.android.builder.dexing; // 部分代码有删减处理,不代表全部源码final class D8DexArchiveBuilder extends DexArchiveBuilder { ? ? ? ?@Override ? ?public void convert( ? ? ? ? ? ?@NonNull Stream input, ? ? ? ? ? ?@NonNull Path output, ? ? ? ? ? ?@Nullable DependencyGraphUpdater desugarGraphUpdater) ? ? ? ? ? ?throws DexArchiveBuilderException { ? ? ? ?D8DiagnosticsHandler d8DiagnosticsHandler = new InterceptingDiagnosticsHandler(); ? ? ? ?try { ? ? ? ? ? ?D8Command.Builder builder = D8Command.builder(d8DiagnosticsHandler); ? ? ? ? ? ? ? ? ?// .... ? ? ? ? ? ?// 部分代码有删减处理,不代表全部源码 ? ? ? ? ? ?// .... ? ? ? ? ? ?if (dexParams.getWithDesugaring()) { ? ? ? ? ? ? ? ?builder.addLibraryResourceProvider(dexParams.getDesugarBootclasspath().getOrderedProvider()); ? ? ? ? ? ? ? ?builder.addClasspathResourceProvider(dexParams.getDesugarClasspath().getOrderedProvider()); ? ? ? ? ? ? ? ?if (dexParams.getCoreLibDesugarConfig() != null) { ? ? ? ? ? ? ? ? ? ?builder.addSpecialLibraryConfiguration(dexParams.getCoreLibDesugarConfig()); ? ? ? ? ? ? ? ? ? ?if (dexParams.getCoreLibDesugarOutputKeepRuleFile() != null) { ? ? ? ? ? ? ? ? ? ? ? ?builder.setDesugaredLibraryKeepRuleConsumer( ? ? ? ? ? ? ? ? ? ? ? ? ? ?new FileConsumer(dexParams.getCoreLibDesugarOutputKeepRuleFile().toPath())); ? ? ? ? ? ? ? ? ? ?} ? ? ? ? ? ? ? ?} ? ? ? ? ? ? ? ?if (desugarGraphUpdater != null) { ? ? ? ? ? ? ? ? ? ?builder.setDesugarGraphConsumer(new D8DesugarGraphConsumerAdapter(desugarGraphUpdater)); ? ? ? ? ? ? ? ?} ? ? ? ? ? ?} else { ? ? ? ? ? ? ? ?builder.setDisableDesugaring(true); ? ? ? ? ? ?} ? ? ? ? ? ?D8.run(builder.build(), MoreExecutors.newDirectExecutorService()); ? ? ? ?} catch (Throwable e) { ? ? ? ? ? ?throw getExceptionToRethrow(e, d8DiagnosticsHandler); ? ? ? ?} ? ?}}可以在此增加一个 hook 点,把需要执行 DexBuild 操作的文件分发到空闲的机器上面,然后远程执行 d8 命令,执行成功回传文件,然后再放在目标位置。Hook部分代码/** * @see com.android.builder.dexing.D8DexArchiveBuilder.convert */private fun hookBuilder() { ? ?val dst = pool.get("com.android.builder.dexing.D8DexArchiveBuilder") ? ?if (dst.isFrozen) { ? ? ? ?log.error("clazz ${dst.simpleName} is frozen") ? ? ? ?return ? ?} ? ?dst.getDeclaredMethod("convert").aopReplace(object : MethodInvokeCallback { ? ? ? ?override fun invoke(self: Any, method: String, args: List) { ? ? ? ? ? ?// XbuildDexBuilder 再调用 MyD8DexArchiveBuilder ? ? ? ? ? ?XbuildDexBuilder().convert( ? ? ? ? ? ? ? ?self, ? ? ? ? ? ? ? ?args[0] as Stream, ? ? ? ? ? ? ? ?args[1] as Path, ? ? ? ? ? ? ? ?args[2] as DependencyGraphUpdater?, ? ? ? ? ? ?) ? ? ? ?} ? ?})} /** * @see com.android.builder.dexing.D8DexArchiveBuilder */public final class MyD8DexArchiveBuilder extends DexArchiveBuilder { ? ?@Override ? ?public void convert( ? ? ? ?@NonNull Stream input, ? ? ? ?@NonNull Path output, ? ? ? ?@Nullable DependencyGraphUpdater desugarGraphUpdater) ? ?throws DexArchiveBuilderException { ? ? ? ?try { ? ? ? ? ? ?// .... ? ? ? ? ? ?// 部分代码有删减处理,不代表全部源码 ? ? ? ? ? ?// .... ? ? ? ? ? ? ? ? ? ? ? ?// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ? ? ? ? ? ?DexBuildArgs args = new DexBuildArgs(dexParams, input, output, entryCount.get(), entrySize.get()); ? ? ? ? ? ?args.getEntryList().addAll(list); ? ? ? ? ? ?MyD8DexArchiveBuilderProxy.run(builder, MoreExecutors.newDirectExecutorService(), args); ? ? ? ? ? ?// D8.run(builder.build(), MoreExecutors.newDirectExecutorService()); ? ? ? ? ? ?// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ? ? ? ?} catch (Throwable e) { ? ? ? ? ? ?throw getExceptionToRethrow(e, d8DiagnosticsHandler); ? ? ? ?} ? ?}}经过测试,在常规使用情况下(非高峰,否则无空闲机器),可以有效的降低任务时长。同理 DexMerge 也可以做类似的 Hook,只不过 merge 操作是把 m 个 dex 文件合并成 n 个 dex文件。项目中的 merge 的输入文件将近1000个,并且每次修改代码,就有可能导致整个 merge 任务重新执行,无法复用缓存。基于实际场景,采用分治法,将输入文件采用取余方式分组,每组再分发到远程机器上执行,执行成功后再回传结果。Merge 操作经过分组合并有效的降低了任务执行时间,同时分组后支持自定义缓存。实际项目中分组为21个,一般情况下,开发同学只是修改局部部分文件,再次编译的时候,只有其中1-2个分组有变动,只需要重执行有过变动的分组即可,提高了执行效率。相关日志图中所示,合并的文件有971个,分成21组,其中19组使用了缓存,本次执行消耗5.8s。编译任务与远程任务5.2 其他d8本地与远程执行实操过程中发现大部分情况文件越大 dex 执行时间越长,网络传输是有损耗的,所以并不是所有的 dex 操作都值得分发到远程,只有超过一定阈值的时候,才会分发到远程。通过统计与计算 build 过程输入文件需要大于1M, merge 过程输入文件需大于 3M,满足这样条件分发到远程编译会有不小的提速收益。大文件文件分割根据上一条,dex执行时间与文件大小相关,实操过程发现部分jar文件非常大,比如R.java合并后的 jar 有将近200M, 可以通过切片方式,把一个大的jar文件分割成若干较小的文件,然后再进行 d8 处理,消耗时间会短很多。d8 优化d8 实际上是一个 shell 执行 jar文件的方式,可以通过 GraalVM 来转成本地可执行文件,也能有一定幅度的性能提升。禁止原生缓存原生 DexMerge 任务缓存命中率差,并且执行缓存过程也消耗不少时间,可以选择性设置禁止缓存。自定义共享缓存如上所述,部分原生Gradle缓存机制效果差,DexBuild 与 DexBuild 操作可以采取自定义缓存方式,远程在收到编译任务可以先判断是否有缓存,再做具体执行,同时再把执行结果缓存起来用来复用。环境隔离实操过程中,d8 编译的结果有可能会有一些异常情况,可以采取单独配置代码目录与 GradleUserHome目录,正常编译模块与分布式编译模式分开管理,方便区分以及快速降级。手动降级前期功能不稳定,需手动开启。经过一个月测试功能比较稳定,已经默认开启。如需关闭,手动主动关闭。5.3 结果经过一段时间观察,目前功能稳定,有效的的解决dex执行缓慢问题,同时整体编译速度维持在正常水平。06?功能演示本地打包命令为 ./gradlew :app:assembleDebug -q -s,云编译也类似 hub -b ":app:assembleDebug -q -s" --vip。06?未来规划云模拟器与云设备通过服务器强大的性能,模拟多个模拟器或设备,用于开发、调试、测试。云端设备可以快速复制与销毁,可以用于兼容性测试与兼容性开发。云IDE最近推出Fleet, 以及 Visual Studio Code 和 IDEA 的 Remote Development,似乎远程开发是个趋势。结合云端设备,或许也会有着不一样的开发体验。以上是今天的分享内容,如果你有什么想法或疑问,欢迎大家在留言区与我们互动,如果喜欢本期内容的话,欢迎点个“在看”吧!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-11 06:00 , Processed in 0.526493 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表