【AICon】AI 基础设施、LLM运维、大模型训练与推理,一场会议,全方位涵盖! >>> 了解详情
写点什么

有赞 Android 编译进阶之路——全量编译提效方案

  • 2020-03-15
  • 本文字数:6143 字

    阅读完需:约 20 分钟

有赞 Android 编译进阶之路——全量编译提效方案

前言:有赞移动技术沙龙刚过去不久,相信很多同学对《有赞 Android 秒级编译优化实践》的分享还记忆犹新,分享中提到了全量编译提效与增量编译提效两种方案。本期我为大家详细介绍下基于 EnjoyDependence 的全量编译提效方案。

一、项目背景

经过多年的发展,有赞零售 Android 项目代码已经达到 45W+ 的规模(phone&pad),其中 kotlin 代码占比 33%左右,在如此大规模的代码量下,编译逐步成为我们项目加速的桎梏(PC 配置:MacBook Pro i5-8G;时间:全量 15min+),严重影响了我们的开发效率,阻碍我们的发展。为了彻底解决编译慢这一业内难题,我们今年下半年基于已有的 组件化 工程,展开了编译提效的项目,EnjoyDependence 就诞生于这个阶段。

二、编译提速目标

  1. 全量编译从 15min+ 降至 3min 内

  2. 低侵入性,尽量不改造工程结构,保证工程稳定

  3. 方案稳定可靠,不能影响业务同学的开发效率

  4. 易于扩展,可以灵活对接各种已有系统

  5. 方便管理,尽可能保证低廉的学习理解成本,方便大家上手

三、全量编译提效核心——EnjoyDependence

简介:狭义上 EnjoyDependence 是集依赖管理、构建发布、编译耗时统计等功能的 Gradle 插件。广义上指代完成全量编译优化的各种组成:EnjoyDependence Gradle 插件、接入中间件、自动化脚本、EnjoyManager AS 管理插件等。如不特殊指明,EnjoyDependence 仅指代 EnjoyDependence Gradle 插件。

3.1 EnjoyDependence 特点

为了达成编译提效的目标,EnjoyDependence 经过多次优化迭代后具备了如下的特点,奠定了编译提效战役胜利的基础。


3.2 EnjoyDependence 实现原理

这一小节涉及到一些 gradle 基础知识,如有不了解的同学可以通过《Android Gradle 权威指南》和《Gradle For Android 中文版》来加深对原理的认识。

架构图

这里给大家提供一张 EnjoyDependence 的架构图,方便大家从整体到局部,由浅入深的理解 EnjoyDependence 的原理。



接下来的章节,我们从底层剖析 EnjoyDependence 的实现原理,主要包括:aar 发布、依赖管理、自动发布等内容。

aar 发布

由于我们的工程是典型的 组件化 架构,这也是我们此次编译提效的大前提。独立的模块划分使我们可以方便地针对单模块实现编译、测试、发布等常规任务。发布 是整个全量编译提效方案的基础,只有稳定可靠的 aar 发布才能保证全量 aar 构建的可靠。


正如大家平常使用 gradle 脚本发布 aar 到 maven 一样,我们的发布也是基于 Maven Plugin 来完成的。不同的是,我们为了对发布的核心流程:pom.xml 文件生成、构件收集更有掌控力,同时兼容多种 flavor,我们没有采用现成的 maven 发布,而是 hook 了 maven 发布流程,在其中嵌入了我们自己的逻辑。


project.plugins.apply(MavenPublishPlugin)    project.pluginManager.withPlugin('com.android.library', newAction<AppliedPlugin>() {@Overridevoid execute(AppliedPlugin appliedPlugin) {        addSoftwareComponents(project)}})
复制代码


privatevoid addSoftwareComponents(Project project) {...    android.libraryVariants.all { v ->...      project.components.add(newAndroidVariantLibrary(objectFactory, configurations, attributesFactory, publishConfig))}...}
复制代码


通过上述方法,我们将我们的发布逻辑和已有逻辑进行关联,从而增加一些差异化实现,方便我们扩展。其中 AndroidVariantLibrary 是我们实现 maven 发布的核心类,主要负责 pom.xml 文件生成、构件收集等功能,其类图如下:



UML 图中我已标出几个核心点,主要包括:构件收集(getArtifacts)、依赖收集(getDependencies)、过滤规则收集(getGlobalExcludes) 等功能。其中依赖、过滤规则等内容最终会体现在 pom.xml 文件中。熟识 maven 的同学应该对 pom.xml 文件不太陌生,它是 maven 依赖管理的核心文件,是 android dependencies 中各种依赖方式的基础。


<?xml version="1.0" encoding="UTF-8"?><project><groupId>com.youzan.mobile</groupId><artifactId>liba</artifactId><version>1.0.0.15-SNAPSHOT</version><packaging>aar</packaging><dependencies><dependency><groupId>androidx.appcompat</groupId><artifactId>appcompat</artifactId><version>1.1.0</version><scope>runtime</scope></dependency></dependencies></project>
复制代码


上述文件是一个示例库 liba 的 pom.xml 文件,通过它我们可以非常方便看到我们此次发布的 liba 的相关信息:groupId、artifactId、version 等大家常见的 GAV,同时我们也可以看到这个 liba 的依赖情况,其中有一个关键的节点 runtime,它指明了 liba 对 androidx.appcompat:appcompat:1.1.0 的依赖是个运行期 依赖。这样讲大家可能比较疑惑,但是当我告诉你经常用到的 implementation 其实就是个运行期依赖,你是不是会恍然大悟。


由于我们基于 Module aar(各种业务 module 构建后的产物)的编译优化仅涉及到 api & implementation 两种依赖方式,所以 AndroidVariantLibrary 类仅提供了这两种方式的 Usages,用来实现 自定义发布,主要包括 pom.xml 生成、构件收集 2 个过程,限于篇幅限制具体实现细节就不在这里赘述了。


为了方便发布,我们根据 flavor、buildType 创建了不同的 发布 Task 供业务同学调用。具体实现依托于 MavenPublish:


project.publishing {          repositories {            maven {              credentials {                username publishExt.userName // 仓库发布用户名                password publishExt.password // 仓库发布用户密码}              url urlPath // 仓库地址}}          publications {def android = project.extensions.getByType(LibraryExtension)            android.libraryVariants.all { variant ->if(variant.name.capitalize().endsWith("Debug")) {"maven${variant.name.capitalize()}Aar"(MavenPublication) {from project.components.findByName("android${variant.name.capitalize()}")                  groupId publishExt.groupId                  artifactId tempArtifactId                  version defaultVersion}} elseif(variant.name.capitalize().endsWith("Release")) {"maven${variant.name.capitalize()}Aar"(MavenPublication) {from project.components.findByName("android${variant.name.capitalize()}")                  groupId publishExt.groupId                  artifactId tempArtifactId                  version defaultVersion}}}}}
复制代码


至此,介绍了 EnjoyDependence 插件强大的发布能力,它接管了 pom.xml 文件的生成、构件的收集、任务的创建等核心流程,为我们自定义发布任务提供了极大的便利,也为我们解决各类 依赖传递 问题提供了帮助。

3.3 依赖管理

成功发布之后,本地 or 远端已经有了我们 Module 的构件(aar 形式的产物),我们如何正确使用这些产物来加快我们的编译速度是我们接下来的重点。


在 Android 依赖中,我们经常见到 implementation project(path: ‘:modules:libcommon’) 用于实现对本工程 Module 的依赖。相信很多同学都见过 implementation “com.youzan.mobile:lib common:1.0.0.15-SNAPSHOT” 这种方式的依赖,用于实现对于一个三方、二方库的依赖。


既然我们有现成的方式可以实现对构件的直接依赖,我们就可以利用同样的方法实现对某个 Module 依赖方式的控制,比如:


if(needSourceBuild) {    implementation project(path: ':modules:lib_common')} else{    implementation "com.youzan.mobile:lib_common:1.0.0.15-SNAPSHOT"}
复制代码


通过上述方式我们就可以实现源码和构件(aar)依赖的切换,通过这种方式我们可以达到免编译某个 Module 的目的,从而节省编译时间,达到编译提效的目的。这种方式可能是最省时的实现方式,但它不是最优解,它满足不了 低侵入性,尽量不改造工程主程,保证工程稳定 这个目标,所以我们需要另辟蹊径。


为了实现高内聚、低耦合、可扩展、低侵入的目标,我们基于如下模型实现了相对优雅的依赖管理。



如上模型,我们基于 Plugin 实现了依赖管理的功能,主要包括:


  • dynamicDependency 域对象创建


NamedDomainObjectContainer<DependenceResolveExt> dependencyResolveContainer = targetProject.container(DependenceResolveExt.class)    targetProject.extensions.add("dynamicDependency", dependencyResolveContainer)
复制代码


  • 依赖解析


targetProject.configurations.all { Configuration configuration ->        if (configuration.dependencies.size() == 0) {          return        }        configuration.dependencies.all { dependency ->          if (dependency instanceof DefaultProjectDependency) {            def projectName = dependency.dependencyProject.name            def dependencyResolveExt = dependencyResolveContainer.find {              it.name == projectName            }            if (dependencyResolveExt != null && !dependencyResolveExt.debuggable) {              resolveExtMap.put(dependency.dependencyProject, dependencyResolveExt)            }          }        }        println("targetProjectName:" + targetProject.getName() + "; resolveExtMap Size:" + resolveExtMap.size())      }
复制代码


  • 依赖替换


targetProject.configurations.all { Configuration configuration ->        if (!configuration.getName().contains("Test") && !configuration.getName().contains("test")) {          resolutionStrategy {            dependencySubstitution {              resolveExtMap.each { key, value ->                def defaultFlavor = value.flavor                if (targetProject.hasProperty("flavor") && targetProject.flavor != "unspecified") {                  defaultFlavor = targetProject.flavor                }                if (defaultFlavor != "" && defaultFlavor != null) {                  substitute project("${key.path}") with module("${value.groupId}:${getArtifactName(key, value.artifactId + "-" + defaultFlavor)}:${value.version}")                } else {                  substitute project("${key.path}") with module("${value.groupId}:${getArtifactName(key, value.artifactId)}:${value.version}")                }              }            }          }        }      }
复制代码


完成以上步骤后,我们基本的依赖管理能力已经具备,剩下的就是业务工程中的接入。接入方式也很简单:


dependencies {  implementation project(path: ':modules:lib_common')}
复制代码


原有逻辑不变,只需要增加一个 dynamic.gradle 脚本完成依赖管理的对接:


dynamicDependency {  lib_common {    //如果是true,则使用本地模块作为依赖参与编译,否则使用下面的配置获取远程的构件作为依赖参与编译    debuggable = isSourceBuild("lib_common")//    flavor = "pad"    groupId = "com.youzan.mobile"    artifactId = "lib_common" // 默认使用模块的名称作为其值    version = loadAARVersion("lib_common")  }}
复制代码


到目前为止,我们已经实现了发布和依赖管理这两个核心功能,业务方可以方便的使用 EnjoyDependence 实现构件发布和依赖替换,从而实现 Android 组件化工程的编译加速。其实通过已有构件来加速编译这个方案出来已久,本生没有太多亮点,如何通过已有技术来满足自己工程所需才是王道。所以,我们在推出 EnjoyDependence 后并没有结束迭代,而是逐步完善基础设施满足各种业务需要。

3.4 aar 自动发布

为了进一步解放生产力,同时提高全量编译加速的稳定性,我们决定减少人为干预,尽量通过自动化任务实现关键步骤。


为了方便对接已有的自动化平台,EnjoyDependence 提供了批量/增量发布、版本控制、忽略规则设定、优先级设定等功能,具体功能 Task 如下:



EnjoyDependence 通过一系列相互关联的 Task 完成 Module 发布,单 Module 发布主要流程如下:



在单 Module 发布任务基础上,EnjoyDependence 提供了批量发布功能:



至此,EnjoyDependence 主要功能都已介绍完毕。经过一期的优化,我们的编译速度有了明显的提升,耗时问题得到改善(25 个 module,3min 内编译完成)。为了达成 方便管理,尽可能保证低廉的学习理解成本,方便大家上手 这个目标,我们提供了 Enjoy Manager AS Plugin 来实现对 EnjoyDependence 的管理,方便大家上手,轻松开发。

三、Enjoy Manager AS Plugin

Enjoy Manager 是一个 Android Studio 插件,用于实现 EnjoyDependence 可视化管理,已在 https://plugins.jetbrains.com 发布。



通过以上面板,可以方便的实现依赖方式管理,基本不需要学习成本,上手简单,易于推广。同时,我们基于 LRU 算法实现了最近五个分支的配置保留功能,极大的降低了分支切换的配置成本。最后,我们也可以通过这个面板看到增量编译的痕迹(版本号离散分布)。

Q&A

在这次优化中,遇到几个比较值得分享的问题,在这里和大家分享下。


  1. 传递依赖引起的 Module 版本不一致的问题,如何解决?

  2. 在众多 Module 中难免有基础 Module(被其他 Module 依赖)、业务 Module 之分。各业务 Module 在编译期对同一基础 Module 的依赖可能是不同的,如果不做处理,这样在编译 APK 时会由于依赖传递的问题导致所需依赖不存在或者重复导入问题的出现。为了解决这个问题,我们需要清楚的理解编译期依赖和运行期依赖的区别。在编译时我们只需要保证编译通过,同时干涉 pom.xml 文件的生成,将基础模块的依赖过滤掉;在 APK 编译时 由 APP 指定稳定的基础 Module 依赖,确保各业务 Module 对基础 Module 的依赖由 APP 来确定,这样就可以解决此类依赖问题。

  3. 如何实现多版本号管理,即不影响 git 提交,又可以随意指定依赖版本?

  4. 对于 EnjoyDependence 来说,业务方对具体 aar 依赖的 version 是由业务方决定的,所以通过该方式业务方可以随意指定版本号。那么为了业务同学应用方便,我们在 version.properties 中指定稳定的远端版本,在 local.properties 指定本地的自定义版本,如果两者都存在,以自定义版本为准。同时由于 local.properties 是 git 忽略文件,所以它不会影响远端代码的稳定,也不会干涉其他同学的开发。

  5. 如何支持 Module 的增删?

  6. 在日常开发中难免会遇到 Module 的增删,Module 的增删会影响增量编译、Module 发布两个过程。增加 Module 后,势必需要对其进行发布,所以需要保证发布任务的创建必须灵活可靠,足以应对各种不规范 Module 的创建行为,保证它顺利发布,EnjoyDependence 通过查看是否存在域对象、域对象中是否包含 GroupId、AtifactId 来生成发布任务,兼容不规范 Module 的创建。Module 的删除会影响增量发布,为了避免删除后依然执行发布,我们可以将删除的 Module 加入到忽略中,从而保证其不参与发布。

结语

基于 EnjoyDependence 的全量编译提效方案一期内容分享到此就结束了,但是我们的编译优化项目并未停止,我们会持续攻坚克难,找寻最优解。下一期为大家带来的增量编译工具 Savitar 也是我们在编译提效中的一大利器,希望大家持续关注。


2020-03-15 20:201120

评论

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

一起学习设计模式:责任链模式

宇宙之一粟

设计模式 8月月更

玩转KubeEdge保姆级攻略

乌龟哥哥

8月月更

自然语言处理--神经网络的复习

IT蜗壳-Tango

自然语言处理 nlp 9月月更

移动办公平台如何在企业中发挥数字化优势?

WorkPlus

web前端培训程序员学习什么呢

小谷哥

参加前端培训后再就业难吗?

小谷哥

构建万物可信的基石:解密区块链跨链技术

创意时空

[教你做小游戏] 展示斗地主扑克牌,支持按出牌规则排序!支持按大小排序!

HullQin

CSS JavaScript html 前端 9月月更

基于Vue3常用代码块

青柚1943

typescript Vue3 Element Plus Pinia sortablejs

【JVM】HotspotJVM 分代回收机制

小明Java问道之路

8月月更

iofod导入任意前端资产,以 Element UI 为例

iofod jude

小程序 前端 低代码 网页

【算法实践】一天路走到黑--手把手带你实现坚持不懈的线性查找

迷彩

Python 数据结构 算法实践 8月月更 线性查找

【案例回顾】春节一次较波折的MySQL调优

京东科技开发者

MySQL 数据库 索引 RDS 调优

从Core Dump中提取CUDA的报错信息

OneFlow

深度学习 报错 cuda

这些智能合约漏洞,可能会影响你的账户安全!

创意时空

每日一R「21」Unsafe Rust

Samson

学习笔记 8月月更 ​Rust

金融科技创新者的困境

木风

金融科技 数字化转型 科技创新

微服务网关Gateway实践总结

Java 架构

如何在保护用户隐私的同时实现精准广告投放?

HMS Core

广告sdk

定时任务报警通知解决方案详解

阿里巴巴云原生

阿里云 微服务 云原生 定时任务

艺术收藏NFT系统开发:NFT功能搭建

开源直播系统源码

数字藏品 数字藏品系统软件开发 数字藏品开发

阿里云高性能计算负责人何万青:阿里云大计算加速HPC与AI融合

阿里云弹性计算

AI HPC 高性能计算 无影云电脑 计算巢

web前端培训入门难吗?

小谷哥

【编程实践】认识爬虫并手把手带手实现新闻网站的爬取

迷彩

记录 Python爬虫 8月月更 网络爬虫

C/CPP基础练习题多维数组,矩阵转置,杨辉三角详解

CtrlX

c c++ 基础 8月月更

IDEA配置tomcat

楠羽

#开源

Java进阶(一)内存解析

No Silver Bullet

Java 9月月更 内存解析

私有化部署的企业IM:实现工作消息、文件的全面可控

WorkPlus

大数据培训是否可以延迟工作周期

小谷哥

区块链交易隐私如何保证?华为零知识证明技术实战解析

创意时空

架构师的十八般武艺:合规架构

agnostic

企业架构 合规

有赞 Android 编译进阶之路——全量编译提效方案_文化 & 方法_Silas_InfoQ精选文章