【AICon】探索RAG 技术在实际应用中遇到的挑战及应对策略!AICon精华内容已上线73%>>> 了解详情
写点什么

有赞 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:201093

评论

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

独一无二的「MySQL调优金字塔」相信也许你拥有了它,你就很可能拥有了全世界。

洛神灬殇

性能优化 后端 MySQL 数据库 引航计划 10月月更

springboot vue失物招领网站源码

清风

源码 Vue springboot java 计算机毕业设计

软件架构之原则、风格和实践

俞凡

架构

细说包管理器yarn和npm

devpoint

npm YARN Node 10月月更

强烈推荐!88页《Redis学习文档》完整版,PDF开放下载

Java 架构 面试 程序人生 编程语言

北鲲云超算平台如何将云计算与高性能计算结合

北鲲云

自动驾驶混战,剑气二宗谁能笑傲江湖?

白洞计划

第 9 章 -《Linux 一学就会》-文件的归档和压缩 tar---zip

学神来啦

Linux 运维 linux学习

【LeetCode】 旅行终点站Java题解

Albert

算法 LeetCode 10月月更

如何应对员工犯错?

石云升

项目管理 管理 引航计划 内容合集 10月月更

javaweb springboot汽车租赁系统源码

清风

源码 springboot 计算机毕业设计

SpringMVC源码分析-HandlerAdapter(4)-ModelAndViewContain组件分析

Brave

源码 springmvc 10月月更

[27]智慧金融--AI目前最被看好的落地领域

数据与智能

人工智能

《写给互联网工程师的5G书》全文pdf开放下载

俞凡

架构 5G 网络 通信 10月月更

聊一聊差分放大器

不脱发的程序猿

嵌入式 电路设计 硬件开发 运算放大器

在线GIF图片帧修改工具

入门小站

工具

【Flutter 专题】138 图解自定义国旗渐变头像

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 10月月更

谈 C++17 里的 State 模式之二

hedzr

c++ 算法 设计模式 Design Patterns 有限状态机

以匠心正道,以决心致远:毫末智行的自动驾驶之路

脑极体

linux手误rm可能不需要跑路

入门小站

Linux

【实战】基于TensorRT 加速YOLO系列以及其他加速算法实战与对比

cv君

AI 引航计划

容器 & 服务:Helm Charts(一)

程序员架构进阶

架构 Kubernetes 容器 Helm Charts 10月月更

linux线上CPU100%排查

入门小站

Linux

Docker OOM Killer

BeyondLife

Docker JVM trouble shooting

手把手教学基于深度学习的遥感影像倾斜框算法训练与分析

cv君

AI 引航计划

在线心语日历批量生成工具

入门小站

工具

数据结构与算法 - 复杂度

小马哥

数据结构与算法 日更

一文了解「模块化」 区块链的当前形势:执行、安全性及数据可用性

CECBC

翻译积累 - Java正则表达式Pattern类

小马哥

翻译 日更

003云原生之架构原则

穿过生命散发芬芳

云原生 10月月更

微博系统中”微博评论“的高性能高可用计算架构

michael

#架构实战营

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