Android Gradle 同步优化
作者:究极逮虾户
很多人听到方法论三个字,就觉得我要开始pua,说我阿里味,但是我觉得这个查问题的方式可能会对大家有点帮助。
很多人都会有这样的困扰,给你的一个工作内容是一个你完全陌生的东西,第一选择是逃避然后开始摆烂。我记得前一阵子和一个网友聊天,他有一次面试的时候也问了这样的问题。这次同步优化其实也相似的问题,是一个对我来说相对比较陌生的东西。
我就是想说下我们是如何来拆解这个问题的。首先需要一些对应相关的基础知识,我去官网查看了些对应的文档资料,仔细的了解了Gradle生命周期相关的,看看能不能对我们后续有所帮助,这个对于后续优化其实是非常重要的。

然后我通过我们的一个monitor插件,我看了大概一个礼拜的同步相关的编译日志,发现了一蛛丝马迹的。monitor就是一个通过BuildOperationNotificationListenerRegistrar把编译信息都记录到一个本地文件夹下的html中,然后把这些信息都发布都远端,方便后续排查问题。
问题大概如下:
- 遍历工程文件夹速度过慢,耗时大概1分钟左右
- 所有依赖全部切换成源码之后因为工程太多,所以展开速度过慢
Configuration之后竟然有个很慢的东西,占据了大量的耗时
这个就是我的方法论,通常碰到一个比较大的问题,我会把一个问题先尝试拆解成几个不同的小问题,然后列出一个优先级和难易度,之后从易到难的逐步解决问题。一般情况下当你的leader发现问题有缓解之后才会逐步的更多的投入人力资源。而想要一步登天改完所有问题还是有点异想天开的。
简单的说我们将一个的大的工程结构拆分成若干小的而且独立的部分,然后业务同学在各自小的独立的编译单元中进行自己的工作流,之后大家不会改动到的模块就会自动的切换成aar产物,避免了无效工程结构的展开。最后的编译阶段由我们的大的工程结构来进行接管,这样就能同时保证代码的更快速展开和代码的稳定性了。
数据结构缓存
因为工程目录结构太复杂了,导致获取工程模块数据结构的速度偏慢,大概耗时需要1分钟左右的时间。但是我们认为工程结构本身是处于比较稳定的状态,并没有必要每次都使用文件展开的方式进行数据结构的生成。
所以打算结合当前的工程分支信息以及各个子git工程的信息等,将这部分数据缓存复用,从而绕开这个文件展开过程,已达到对这部分提速的能力。
因为知道当前工程含有几个git工程,但是并不是所有人都有工程的权限的,然后会判断该git工程是否存在,以及文件夹下是否存在有一个settings.gradle或者build.gradle,如果都符合则认为该子仓是一个符合标准的工程仓库,需加入作为缓存唯一key值的计算中,不符合的工程就会跳过。
val rootDir = FileTools.rootProjectDir
val resolves = mutableListOf<XXX>()
val cacheKey by lazy {localCacheKey()
}init {resolves.add(rootDir.getLog().resolve())allBabels.forEach {val file = File(rootDir, it)val hasSettings = file.walkTopDown().firstOrNull { walkFile -> walkFile.name == "settings.gradle" || walkFile.name == "build.gradle" } != nullif (file.exists() && hasSettings) {resolves.add(file.getLog().resolve())}}
}private fun localCacheKey(): String {var key = ""resolves.forEach {key += it.commitSha + "_"}val file = rootDirreturn "${GitUtils.currentBranch(file.path).replace("\\/", "_")}_${key.hashCode()}"
}
然后我们在数据结构获取的时候会先判断本地是否存在改缓存key的文件夹,文件夹下面是否有对应的文件,之后基于这个来重新反序列化出对应的数据结构。如果没有则按照原来的文件访问操作进行数据结构获取了。
另外在数据结构中本身是还有父类,子类对应文件的信息的,但是这部分数据并没有办法进行缓存,因为缓存下来之后重新反序列化出来的就是新的一个对象。这部分需要我们重新通过自己的遍历方法,补充这部分数据机构的关系。
另外的一部分边界情况就是我们要判断当前的git status中是否存在新增的对应的数据结构存在,如果有则需要单独添加一份数据结构。因为我们绕开了文件访问,所以需要对这部分进行补充。
从本地测试结果来看,第一次展开情况下耗时60s时间,如果从缓存内读取则时间压缩到9s左右就完成数据结构还原了。所以这个算是我们加快工程同步速度的第二步了。
最有意思但最难的问题
先说结论,我们发现同步阶段的后期耗时是android jetifier,会在aar或者jar资源下载完毕之后会执行jetifier的清洗androidx的操作。
为什么jetifier会选择在这个时机,而不是在打包流程进行对应的替换呢?其实在于他们并不仅仅要完成字节码上的转化操作,另外还要对资源文件也进行同样的清洗,比如layout文件中的。
所以jetifier在后续的AGP源码中就替换了原来的方式,进而对工程内所有的aar和jar产物进行替换操作,也就是Gradle官方提供的TransformAction相关的api。
As described in different kinds of configurations, there may be different variants for the same dependency. For example, an external Maven dependency has a variant which should be used when compiling against the dependency (java-api), and a variant for running an application which uses the dependency (java-runtime). A project dependency has even more variants, for example the classes of the project which are used for compilation are available as classes directories (org.gradle.usage=java-api, org.gradle.libraryelements=classes) or as JARs (org.gradle.usage=java-api, org.gradle.libraryelements=jar).
@CacheableTransform
abstract class JetifyTransform : TransformAction<JetifyTransform.Parameters> {
}
这个是从agp源码中抠出来的,我看了下4.0.0和7.0+版本的agp,都已经是TransformAction写法了。另外没有扫描前是不确定当前输入aar或者jar是否含有非androidx的代码的,就需要对所有的aar和jar进行一次扫描,之后重新生成一个新的aar或者jar。
但是也正是因为TransformAction写法,导致了jetifier操作被放在了同步阶段完成了。而且因为我们的module数量太多以及我们的快编等等,更导致了这个问题被放大了好几倍。
动态修改gradle配置
android.useAndroidX=true
android.enableJetifier=true
因为jetifier的开关设置在gradle.properties中,所以我们打算在插件内判断是否是同步操作,如果是同步则主动关闭jetifier,从而绕开TransformAction的耗时。
我尝试通过添加android.enableJetifier=false和android.useAndroidX=false参数到gradle.startParameter.projectProperties或者gradle.startParameter.systemPropertiesArgs中去,这两个配置是gradle的全局配置参数。
但是尝试重新通过setProjectProperties和setSystemPropertiesArgs函数去重新赋值,但是测试下来发现没有生效。这个值已经在内存中被Gradle持有,重新设置是无效的。然后我们尝试了下通过反射去修改这个值,最后发现个更尴尬的事情,这个值是在AGP内通过ProjectsServices来进行读取的,所以我们只能放弃这个方案了。
hook agp ProjectsServices
当发现这个值是在AGP中去进行读取的。后续就决定从修改AGP的ProjectsServices进行入手,从而达到关闭jetifier。有了上一次的反射经验,然后我们也顺利的沿用到了这次。
因为AGP相关的时机其实并不是特别靠前,而是在Android插件被执行之后的afterEvaluateapi中,所以我们只要在这个执行之前通过反射去修改projectServices就行了。
这里因为我们的插件需要判断当前的Project内是否存在agp插件,并在他的 afterEvaluate执行之前调用,所以我们选择了 project.plugins.withType这个api来执行。
override fun apply(project: Project) {project.plugins.withType(BasePlugin::class.java) {val service = it.getProjectService() ?: return@withTypeval service = it.getProjectService() ?: return@withType
val projectOptions = service.projectOptions
val projectOptionsReflect = Reflect.on(projectOptions)
val optionValueReflect = Reflect.onClass("com.android.build.gradle.options.ProjectOptions\$OptionValue",projectOptions.javaClass.classLoader
)
val defaultProvider = DefaultProvider() { false }
val optionValueObj = optionValueReflect.create(projectOptions, BooleanOption.ENABLE_JETIFIER).get<Any>()
Reflect.on(optionValueObj).set("valueForUseAtConfiguration", defaultProvider).set("valueForUseAtExecution", defaultProvider)
val map = getNewMap(projectOptionsReflect, optionValueObj)
projectOptionsReflect.set("booleanOptionValues", map)}
}
private fun BasePlugin<*, *, *>?.getProjectService() =Reflect.on(this).field("projectServices").get<ProjectServices?>()
在这个阶段上,我们能获取到getProjectService,然后就可以为所欲为了。虽然听起来挺离谱的,但是貌似也雀食是可以。
这次我们雀食成功了,这种方式确实能在同步阶段自动的去把jetifier给关闭掉,然后我们就打算尝试性的在工程内进行实验了。
allProject{apply plguins:"jetifier_closs.class"
}
最后我们还是失败了,以前介绍过项目内含有很多个复合构建的项目,然后我们是通过所有子工程apply from根的build.gradle的方式完成这部分配置同步的。但是前面说到jetifier读取的时机实在afterEvaluate。但是好巧不巧,这次所有复合构建的工程因为apply from的缘故,导致了时机触发都在afterEvaluate,导致了反射修改的值没有生效。所以我们又失败了。
方法签名检查是否存在support包
最后我们仔细想了想,这种修改还是太过于黑魔法了,万一后面AGP有修改我们也要跟随一起改动。最后决定移除项目内所有的support库,主动关闭同步和编译阶段的jetifier,这样既能同时加快打包速度也可以让同步速度变得更快,一举两得。
这次移除操作就大部分是人力堆叠了,通过dependcies把所有依赖了support都进行移除,另外比如微博这种jar包内的,则采取在一个开启了jetifier的工程中,先完成转化之后再拿到jar包之后二次上传我们的私有maven,从而完成项目内所有库的support移除。
另外作为一个工程师,我们不能只看到眼前的苟且。移除所有support一时间我们可能可以解决这个问题,但是作为一个巨大无比的工程,你不开启jetifier的时候,后续的新增接入的代码都需要确保剔除了support库,否则最后上线就是会出各种问题。另外有个小注意的点就是在support整改之后,需要在Configuration的时候去把support的依赖全部进行移除。这样就能保证以后所有的support包就算新增了也不会被带到apk中。
allprojects {configurations.all { Configuration c ->if (c.state == Configuration.State.UNRESOLVED) {exclude group: 'androidx.lifecycle', module: "lifecycle-extensions"}}
}
项目需要一个长期有效的手段去确定新增的依赖库已经没有用到support。最后采取了之前说的方法签名验证,因为已经移除了所有support库,所以最后apk产物内必然是缺失对应的依赖的,这样在方法签名校验的过程中就会出现异常。我们的A8检查会加载android.jar以及所有的dex文件,如果调用的方法找不到的情况下则会报错。这样就能确保后续引入的新的aar或者jar中如果调用了support则无法完成代码合入。
总结
之后可能文章更新的频率估计也就类似现在这样了呢,大部分时间都是在一个修修补补的状态,其实挺难做一些0-1的优化的,更多的时候是做一些1-100的努力。
看起来本文的内容不多,但是其实我们从年初就开始定位问题以及做一些尝试性的修复了。发现问题的时间以及基于工程去解决当下的困扰都是挺费时费力的。
Android 学习笔录
Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap
相关文章:
Android Gradle 同步优化
作者:究极逮虾户 很多人听到方法论三个字,就觉得我要开始pua,说我阿里味,但是我觉得这个查问题的方式可能会对大家有点帮助。 很多人都会有这样的困扰,给你的一个工作内容是一个你完全陌生的东西,第一选择…...
BeautifulSoup:学习使用BeautifulSoup库进行HTML解析和数据提取。
BeautifulSoup是一个用于解析HTML和XML文档的Python库。它可以帮助我们从网页中提取数据,并以易于操作的方式进行分析。 以下是使用BeautifulSoup进行HTML解析和数据提取的基本语法: 安装BeautifulSoup库:首先,你需要在你的Python…...
基于沙猫群算法优化的BP神经网络(预测应用) - 附代码
基于沙猫群算法优化的BP神经网络(预测应用) - 附代码 文章目录 基于沙猫群算法优化的BP神经网络(预测应用) - 附代码1.数据介绍2.沙猫群优化BP神经网络2.1 BP神经网络参数设置2.2 沙猫群算法应用 4.测试结果:5.Matlab代…...
PCL 判断三点共线(三维空间)
文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 继续之前的思路PCL 判断两条线段的平行性(三维空间),我们可以把判断三点共线看做是判断两条线段是否具有平行性,且这两条线段共用其中一个端点,基于此当这两条线段平行时,则证明这三点共线。 二、实现代码 /…...
【数据库】事务(概念和特点)
一、 什么是事务: 事务是在数据库中执行的一系列操作单元,这些操作要么全部成功提交,要么全部失败回滚。 二、事务的特点: 原子性(Atomicity):事务是一个不可分割的操作单元,要么…...
LA@齐次线性方程组解的结构
文章目录 齐次线性方程组解的结构🎈解的性质齐次线性方程组的解的线性组合还是方程组的解基础解系通解 定理:齐次线性方程组基础解系存在定理齐次线性方程组的基础解系包含的向量个数(秩)👺应用和示例推论1推论2推论3:转置矩阵对的乘积秩的性质非自由未知…...
Docker修改容器ulimit的全部方案及各方案的详细步骤
要修改Docker容器的ulimit(用户资源限制),有以下三种方案,每个方案的详细步骤如下: 方案一:在Dockerfile中设置ulimit 打开您的Dockerfile。在文件中添加以下命令来修改ulimit:RUN ulimit -n …...
进程间通信-Binder
Binder Binder框架概述服务端Binder驱动客户端 设计服务端和客户端设计服务端客户端设计 Binder与ServiceServiceAIDL 保证包裹内参数顺序IMusicPlayerServiceProxyStub 系统服务中的Binder对象ServiceManger管理的服务理解Manger功能快捷键合理的创建标题,有助于目…...
一个简单的vim例子
一.欢迎来到我的酒馆 在本章节介绍vim工具。 目录 一.欢迎来到我的酒馆二.什么是vim三.开始使用vim 二.什么是vim 2.1什么是vim vim是一种Linux命令行类型的文本编辑器。vim指的是"vi improved",意思是vi工具的升级版。vim是基于vi实现的&#x…...
sql server 备份到网络共享
场景:sql server服务器A将数据库备份文件备份到服务器B 1)服务器B创建共享目录 这里我将 D:\ProDbBak 共享,并且Everyone完全控制 2)sql server服务器A能够访问服务器B共享目录,并且能完全控制 3)修改服务…...
程序与进程
一、程序是怎么被执行的 1.在程序中,由引导代码去调用程序中得main函数,而这个过程由链接器完成,链接器将引导代码链接到我们的应用程序构成可执行文件。 2.程序运行需要通过操作系统的加载器来实现,加载器是操作系统中的程序&a…...
大模型从入门到应用——LangChain:链(Chains)-[链与索引:图问答(Graph QA)和带来源的问答(QA with Sources)]
分类目录:《大模型从入门到应用》总目录 图问答(Graph QA) 创建图 在本节中,我们构建一个示例图。目前,这对于较小的文本片段效果最好,下面的示例中我们只使用一个小片段,因为提取知识三元组对…...
spark sql 数据倾斜--join 同时开窗去重的问题优化
spark sql 数据倾斜–join 同时开窗去重的问题优化 文章目录 spark sql 数据倾斜--join 同时开窗去重的问题优化结论1. 原方案:join步骤时,同时开窗去重数据倾斜 2. 优化2.1 参数调优2.2 SQL优化 背景: 需求:在一张查询日志表中&a…...
lv3 嵌入式开发-linux介绍及环境配置
目录 1 UNIX、Linux和GNU简介 2 环境介绍 3 VMwareTools配置 4 vim配置: 1 UNIX、Linux和GNU简介 什么是UNIX? unix是一个强大的多用户、多任务操作系统,支持多种处理器架构 中文名 尤尼斯 外文名 UNIX 本质 操作系统 类型 分时操作系统 开…...
RabbitMQ工作模式-路由模式
官方文档参考:https://www.rabbitmq.com/tutorials/tutorial-four-python.html 使用direct类型的Exchange,发N条消息并使用不同的routingKey,消费者定义队列并将队列routingKey、Exchange绑定。此时使用direct模式Exchange必须要routingKey完成匹配的情况下消息才…...
StringIO BytesIO
上一篇中我们介绍了文件的基本读写操作,但是很多时候数据的读写并不一定都是在文件中,我们也可以在内存中读写数据,因此引出我们今天的主要内容,即 StringIO 和 BytesIO,让你学会在内存中进行数据的基本读写操作。 1 …...
通讯录管理系统(个人学习笔记黑马学习)
1、系统需求 通讯录是一个可以记录亲人、好友信息的工具。 本教程主要利用C来实现一个通讯录管理系统系统中需要实现的功能如下: 添加联系人:向通讯录中添加新人,信息包括(姓名、性别、年龄、联系电话、家庭住址)最多记录1000人显示联系人:显示通讯录中所有联系人信…...
[SpringBoot3]远程访问@HttpExchange
六、远程访问HttpExchange[SpringBoot3] 远程访问是开发的常用技术,一个应用能够访问其他应用的功能。SpringBoot提供了多种远程访问的技术。基于HTTP协议的远程访问是最广泛的。SpringBoot中定义接口提供HTTP服务。生成的代理对象实现此接口,代理对象实…...
Linux安装ntp并使用阿里云配置ntp服务器
安装 NTP 客户端: 打开终端,以 root 权限执行以下命令来安装 NTP 客户端: sudo zypper install ntp 编辑 NTP 配置文件: 使用文本编辑器打开 NTP 的配置文件 /etc/ntp.conf,例如使用 nano 编辑器: sudo v…...
js常用方法总结
1、slice 和 splice slice表示截取,slice(start,end),不改变原数组,返回新数组。 splice表示删除,splice(start,length,item),会改变原数组,从某个位置开始删除多个元素,并可以插入新的元素。…...
基于大模型的 UI 自动化系统
基于大模型的 UI 自动化系统 下面是一个完整的 Python 系统,利用大模型实现智能 UI 自动化,结合计算机视觉和自然语言处理技术,实现"看屏操作"的能力。 系统架构设计 #mermaid-svg-2gn2GRvh5WCP2ktF {font-family:"trebuchet ms",verdana,arial,sans-…...
AI Agent与Agentic AI:原理、应用、挑战与未来展望
文章目录 一、引言二、AI Agent与Agentic AI的兴起2.1 技术契机与生态成熟2.2 Agent的定义与特征2.3 Agent的发展历程 三、AI Agent的核心技术栈解密3.1 感知模块代码示例:使用Python和OpenCV进行图像识别 3.2 认知与决策模块代码示例:使用OpenAI GPT-3进…...
【第二十一章 SDIO接口(SDIO)】
第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...
Keil 中设置 STM32 Flash 和 RAM 地址详解
文章目录 Keil 中设置 STM32 Flash 和 RAM 地址详解一、Flash 和 RAM 配置界面(Target 选项卡)1. IROM1(用于配置 Flash)2. IRAM1(用于配置 RAM)二、链接器设置界面(Linker 选项卡)1. 勾选“Use Memory Layout from Target Dialog”2. 查看链接器参数(如果没有勾选上面…...
学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1
每日一言 生活的美好,总是藏在那些你咬牙坚持的日子里。 硬件:OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写,"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...
python如何将word的doc另存为docx
将 DOCX 文件另存为 DOCX 格式(Python 实现) 在 Python 中,你可以使用 python-docx 库来操作 Word 文档。不过需要注意的是,.doc 是旧的 Word 格式,而 .docx 是新的基于 XML 的格式。python-docx 只能处理 .docx 格式…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
AI病理诊断七剑下天山,医疗未来触手可及
一、病理诊断困局:刀尖上的医学艺术 1.1 金标准背后的隐痛 病理诊断被誉为"诊断的诊断",医生需通过显微镜观察组织切片,在细胞迷宫中捕捉癌变信号。某省病理质控报告显示,基层医院误诊率达12%-15%,专家会诊…...
智能AI电话机器人系统的识别能力现状与发展水平
一、引言 随着人工智能技术的飞速发展,AI电话机器人系统已经从简单的自动应答工具演变为具备复杂交互能力的智能助手。这类系统结合了语音识别、自然语言处理、情感计算和机器学习等多项前沿技术,在客户服务、营销推广、信息查询等领域发挥着越来越重要…...
C#中的CLR属性、依赖属性与附加属性
CLR属性的主要特征 封装性: 隐藏字段的实现细节 提供对字段的受控访问 访问控制: 可单独设置get/set访问器的可见性 可创建只读或只写属性 计算属性: 可以在getter中执行计算逻辑 不需要直接对应一个字段 验证逻辑: 可以…...
