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
插件被执行之后的afterEvaluate
api中,所以我们只要在这个执行之前通过反射去修改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),会改变原数组,从某个位置开始删除多个元素,并可以插入新的元素。…...

微信小程序之bind和catch
这两个呢,都是绑定事件用的,具体使用有些小区别。 官方文档: 事件冒泡处理不同 bind:绑定的事件会向上冒泡,即触发当前组件的事件后,还会继续触发父组件的相同事件。例如,有一个子视图绑定了b…...
Java如何权衡是使用无序的数组还是有序的数组
在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...

Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility
Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility 1. 实验室环境1.1 实验室环境1.2 小测试 2. The Endor System2.1 部署应用2.2 检查现有策略 3. Cilium 策略实体3.1 创建 allow-all 网络策略3.2 在 Hubble CLI 中验证网络策略源3.3 …...

《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》
在注意力分散、内容高度同质化的时代,情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现,消费者对内容的“有感”程度,正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中࿰…...

uniapp微信小程序视频实时流+pc端预览方案
方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度WebSocket图片帧定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐RTMP推流TRTC/即构SDK推流❌ 付费方案 (部分有免费额度&#x…...
[Java恶补day16] 238.除自身以外数组的乘积
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法,且在 O(n) 时间复杂度…...
【Java学习笔记】BigInteger 和 BigDecimal 类
BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点:传参类型必须是类对象 一、BigInteger 1. 作用:适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...
C++课设:简易日历程序(支持传统节假日 + 二十四节气 + 个人纪念日管理)
名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏介绍:《编程项目实战》 目录 一、为什么要开发一个日历程序?1. 深入理解时间算法2. 练习面向对象设计3. 学习数据结构应用二、核心算法深度解析…...
MinIO Docker 部署:仅开放一个端口
MinIO Docker 部署:仅开放一个端口 在实际的服务器部署中,出于安全和管理的考虑,我们可能只能开放一个端口。MinIO 是一个高性能的对象存储服务,支持 Docker 部署,但默认情况下它需要两个端口:一个是 API 端口(用于存储和访问数据),另一个是控制台端口(用于管理界面…...
【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案
目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后,迭代器会失效,因为顺序迭代器在内存中是连续存储的,元素删除后,后续元素会前移。 但一些场景中,我们又需要在执行删除操作…...