Android Gradle 同步优化

作者:究极逮虾户

很多人听到方法论三个字,就觉得我要开始pua,说我阿里味,但是我觉得这个查问题的方式可能会对大家有点帮助。

很多人都会有这样的困扰,给你的一个工作内容是一个你完全陌生的东西,第一选择是逃避然后开始摆烂。我记得前一阵子和一个网友聊天,他有一次面试的时候也问了这样的问题。这次同步优化其实也相似的问题,是一个对我来说相对比较陌生的东西。

我就是想说下我们是如何来拆解这个问题的。首先需要一些对应相关的基础知识,我去官网查看了些对应的文档资料,仔细的了解了Gradle生命周期相关的,看看能不能对我们后续有所帮助,这个对于后续优化其实是非常重要的。

然后我通过我们的一个monitor插件,我看了大概一个礼拜的同步相关的编译日志,发现了一蛛丝马迹的。monitor就是一个通过BuildOperationNotificationListenerRegistrar把编译信息都记录到一个本地文件夹下的html中,然后把这些信息都发布都远端,方便后续排查问题。

问题大概如下:

  1. 遍历工程文件夹速度过慢,耗时大概1分钟左右
  2. 所有依赖全部切换成源码之后因为工程太多,所以展开速度过慢
  3. 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" } != null
        if (file.exists() && hasSettings) {
            resolves.add(file.getLog().resolve())
        }
    }
}

private fun localCacheKey(): String {
    var key = ""
    resolves.forEach {
        key += it.commitSha + "_"
    }
    val file = rootDir
    return "${GitUtils.currentBranch(file.path).replace("\\/", "_")}_${key.hashCode()}"
}

然后我们在数据结构获取的时候会先判断本地是否存在改缓存key的文件夹,文件夹下面是否有对应的文件,之后基于这个来重新反序列化出对应的数据结构。如果没有则按照原来的文件访问操作进行数据结构获取了。

另外在数据结构中本身是还有父类,子类对应文件的信息的,但是这部分数据并没有办法进行缓存,因为缓存下来之后重新反序列化出来的就是新的一个对象。这部分需要我们重新通过自己的遍历方法,补充这部分数据机构的关系。

另外的一部分边界情况就是我们要判断当前的git status中是否存在新增的对应的数据结构存在,如果有则需要单独添加一份数据结构。因为我们绕开了文件访问,所以需要对这部分进行补充。

从本地测试结果来看,第一次展开情况下耗时60s时间,如果从缓存内读取则时间压缩到9s左右就完成数据结构还原了。所以这个算是我们加快工程同步速度的第二步了。

最有意思但最难的问题

先说结论,我们发现同步阶段的后期耗时是android jetifier,会在aar或者jar资源下载完毕之后会执行jetifier的清洗androidx的操作。

为什么jetifier会选择在这个时机,而不是在打包流程进行对应的替换呢?其实在于他们并不仅仅要完成字节码上的转化操作,另外还要对资源文件也进行同样的清洗,比如layout文件中的。

所以jetifier在后续的AGP源码中就替换了原来的方式,进而对工程内所有的aarjar产物进行替换操作,也就是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的代码的,就需要对所有的aarjar进行一次扫描,之后重新生成一个新的aar或者jar

但是也正是因为TransformAction写法,导致了jetifier操作被放在了同步阶段完成了。而且因为我们的module数量太多以及我们的快编等等,更导致了这个问题被放大了好几倍。

动态修改gradle配置

android.useAndroidX=true
android.enableJetifier=true

因为jetifier的开关设置在gradle.properties中,所以我们打算在插件内判断是否是同步操作,如果是同步则主动关闭jetifier,从而绕开TransformAction的耗时。

我尝试通过添加android.enableJetifier=falseandroid.useAndroidX=false参数到gradle.startParameter.projectProperties或者gradle.startParameter.systemPropertiesArgs中去,这两个配置是gradle的全局配置参数。

但是尝试重新通过setProjectPropertiessetSystemPropertiesArgs函数去重新赋值,但是测试下来发现没有生效。这个值已经在内存中被Gradle持有,重新设置是无效的。然后我们尝试了下通过反射去修改这个值,最后发现个更尴尬的事情,这个值是在AGP内通过ProjectsServices来进行读取的,所以我们只能放弃这个方案了。

hook agp ProjectsServices

当发现这个值是在AGP中去进行读取的。后续就决定从修改AGPProjectsServices进行入手,从而达到关闭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@withType
           val 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/97359.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

迁移学习:实现快速训练和泛化的新方法

文章目录 迁移学习的原理迁移学习的应用快速训练泛化能力提升 迁移学习的代码示例拓展应用与挑战结论 &#x1f389;欢迎来到AIGC人工智能专栏~迁移学习&#xff1a;实现快速训练和泛化的新方法 ☆* o(≧▽≦)o *☆嗨~我是IT陈寒&#x1f379;✨博客主页&#xff1a;IT陈寒的博…

debug调试时,通过[[FunctionLocation]]找到函数在源代码中的位置

[[FunctionLocation]] 是 JavaScript 引擎内部的一个属性&#xff0c;用于记录函数在代码中的位置。它不是 JavaScript 语法的一部分&#xff0c;而是在调试和开发过程中用于追踪函数定义位置的一个辅助属性。 当我们在浏览器的开发者工具或其他调试工具中查看函数的属性时&am…

微信短链跳转到小程序指定页面调试

首先说下背景&#xff1a;后端给了短链地址&#xff0c;但是无法跳转到指定页面。总是在小程序首页。指定的页面我们是h5页面。排查步骤如下&#xff1a; 1、通过快速URL Scheme 编译。上部普通编译 下拉找到此选项。 、 2、按照小程序的要求的URL Scheme输入。另外后端给的…

SQL-DQL

-----分组查询----- 1.语法&#xff1a; SELECT 字段列表 FROM 表名 [WHERE 条件 ] GROUP BY 分组字段名 [HAVING 分组后过滤条件]&#xff1b; 2.where与having区别 》执行时机不同&#xff1a;where是分组之前进行过滤&#xff0c;不满足where条件&#xff0c;不参与分组&…

微信小程序 校园周边美食商城分享系统

管理员、会员、商家可通过Android系统手机打开系统&#xff0c;注册登录后可进行管理员后端&#xff1b;首页、个人中心、会员管理、商家管理、美食类型管理、美食信息管理、美食交流、我的收藏管理、系统管理、订单管理&#xff0c;会员前端&#xff1b;首页、美食信息、美食交…

C++快速回顾(三)

前言 在Android音视频开发中&#xff0c;网上知识点过于零碎&#xff0c;自学起来难度非常大&#xff0c;不过音视频大牛Jhuster提出了《Android 音视频从入门到提高 - 任务列表》&#xff0c;结合我自己的工作学习经历&#xff0c;我准备写一个音视频系列blog。C/C是音视频必…

keras深度学习框架通过简单神经网络实现手写数字识别

背景 keras深度学习框架&#xff0c;并不是一个独立的深度学习框架&#xff0c;它后台依赖tensorflow或者theano。大部分开发者应该使用的是tensorflow。keras可以很方便的像搭积木一样根据模型搭出我们需要的神经网络&#xff0c;然后进行编译&#xff0c;训练&#xff0c;测试…

基于RabbitMQ的模拟消息队列之四——内存管理

文章目录 一、设计数据结构二、管理集合1.交换机2.队列3.绑定4.消息5.队列上的消息6.待确认消息7.恢复数据 一、设计数据结构 针对交换机、队列、绑定、消息、待确认消息设计数据结构。 交换机集合 exchangeMap 数据结构&#xff1a;ConcurrentHashMap key:交换机name value:交…

rpm打包

文章目录 rpm打包 1. rpm打包步骤0&#xff09;安装打包工具rpm-build和rpmdevtools1&#xff09;创建初始化目录2&#xff09;准备打包内容3&#xff09;编写打包脚本 spec文件 rpm打包 1. rpm打包步骤 0&#xff09;安装打包工具rpm-build和rpmdevtools yum install rpm-bu…

C++信息学奥赛1176:谁考了第k名

#include <iostream> #include <string> using namespace std; int main() {int n, a;cin >> n >> a; // 输入整数 n 和 aint arr[n]; // 创建大小为 n 的整型数组 arrdouble btt[n]; // 创建大小为 n 的双精度浮点型数组 bttfor (int i 0; i < n;…

03-基础例程3

基础例程3 01、外部中断 ESP32的外部中断有上升沿、下降沿、低电平、高电平触发模式。 实验目的 使用外部中断功能实现按键控制LED的亮灭 按键按下为0。【即下降沿】 * 接线说明&#xff1a;按键模块-->ESP32 IO* (K1-K4)-->(14,27,26,25)* * …

2023年天府杯——C 题:码头停靠问题

问题背景&#xff1a; 某个港口有多个不同类型的码头&#xff0c;可以停靠不同种类的船只。每 艘船只需要一定的时间来完成装卸货物等任务&#xff0c;并且每个码头有容量 限制和停靠时间限制。港口需要在保证收益的情况下&#xff0c;尽可能地提高 运营效率和降低成本。同…

【中危】Spring Kafka 反序列化漏洞 (CVE-2023-34040)

zhi.oscs1024.com​​​​​ 漏洞类型反序列化发现时间2023-08-24漏洞等级中危MPS编号MPS-fed8-ocuvCVE编号CVE-2023-34040漏洞影响广度小 漏洞危害 OSCS 描述Spring Kafka 是 Spring Framework 生态系统中的一个模块&#xff0c;用于简化在 Spring 应用程序中集成 Apache Kaf…

新型安卓恶意软件使用Protobuf协议窃取用户数据

近日有研究人员发现&#xff0c;MMRat新型安卓银行恶意软件利用protobuf 数据序列化这种罕见的通信方法入侵设备窃取数据。 趋势科技最早是在2023年6月底首次发现了MMRat&#xff0c;它主要针对东南亚用户&#xff0c;在VirusTotal等反病毒扫描服务中一直未被发现。 虽然研究…

零知识证明(zk-SNARK)(一)

全称为 Zero-Knowledge Succinct Non-Interactive Argument of Knowledge&#xff0c;简洁非交互式零知识证明&#xff0c;简洁性使得运行该协议时&#xff0c;即便statement非常大&#xff0c;它的proof大小也仅有几百个bytes&#xff0c;并且验证一个proof的时间可以达到毫秒…

ExpressLRS开源之RC链路性能测试

ExpressLRS开源之RC链路性能测试 1. 源由2. 分析3. 测试方案4. 测试设计4.1 校准测试4.2 实验室测试4.3 拉距测试4.4 遮挡测试 5. 总结6. 参考资料 1. 源由 基于ExpressLRS开源基本调试验证方法&#xff0c;对RC链路性能进行简单的性能测试。 修改设计总能够满足合理的需求&a…

达梦数据库管理用户和创建用户介绍

概述 本文主要对达梦数据库管理用户和创建用户进行介绍和总结。 1.管理用户介绍 1.1 达梦安全机制 任何数据库设计和使用都需要考虑安全机制&#xff0c;达梦数据库采用“三权分立”或“四权分立”的安全机制&#xff0c;将系统中所有的权限按照类型进行划分&#xff0c;为每…

浅谈 Pytest+HttpRunner 如何展开接口测试!

软件测试有多种多样的方法和技术&#xff0c;可以从不同角度对它们进行分类。其中&#xff0c;根据软件生命周期&#xff0c;针对不同的测试对象与目标&#xff0c;可将测试过程分为 4 个阶段&#xff1a;单元测试、集成测试、系统测试和验收测试。本文着重介绍了如何借用 pyte…

基于机器学习的fNIRS信号质量控制方法

摘要 尽管功能性近红外光谱(fNIRS)在神经系统研究中的应用越来越广泛&#xff0c;但fNIRS信号处理仍未标准化&#xff0c;并且受到经验和手动操作的高度影响。在任何信号处理过程的开始阶段&#xff0c;信号质量控制(SQC)对于防止错误和不可靠结果至关重要。在fNIRS分析中&…

Endnote中查看一个文献的分组的具体方法——以Endnote X8为例

Endnote中查看一个文献的分组的具体方法——以Endnote X8为例 一、问题 当Endnote中使用分类方法对文献进行分组管理后&#xff0c;有时需要重新调整该文献的分组&#xff0c;则需要找到这个文献在哪个分组中。本文阐述怎样寻找一个文献的分组的位置信息。 二、解决方法 1.选…