Android Gradle 同步优化

news2024/11/25 13:09:23

作者:究极逮虾户

很多人听到方法论三个字,就觉得我要开始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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

补贴纷争,台积电欧洲计划引发格芯抗议 | 百能云芯

台积电近期海外布局计划引发了一系列反响。 继美国工会悍拒台积电加派台湾人力到亚利桑那厂进行支持&#xff0c;在德国设厂多年的芯片代工大厂格芯因不满德国对台积电在此设厂提供50亿欧元的巨额补贴&#xff0c;扬言将向欧盟提出申诉。 此外&#xff0c;台积电还有计划在日本…

Harbor 私有仓库迁移

文章目录 一.私有仓库迁移的介绍1.为何要对Harbor 私有仓库的迁移2.Harbor 私有仓库的迁移特点3. Harbor 私有仓库的迁移注意要点 二.私有仓库迁移配置1.源Harbor配置&#xff08;192.168.198.11&#xff09;&#xff08;1&#xff09;接着以下操作查看容器状况及是否可以登录 …

如何选择合适的自动化测试工具?

自动化测试是高质量软件交付领域中最重要的实践之一。在今天的敏捷开发方法中&#xff0c;几乎任一软件开发过程都需要在开发阶段的某个时候进行自动化测试&#xff0c;以加速回归测试的工作。自动化测试工具可以帮助测试人员以及整个团队专注于自动化工具无法处理的各自任务&a…

【C++ 学习 ⑱】- 多态(上)

目录 一、多态的概念和虚函数 1.1 - 用基类指针指向派生类对象 1.2 - 虚函数和虚函数的重写 1.3 - 多态构成的条件 1.4 - 多态的应用场景 二、协变和如何析构派生类对象 2.1 - 协变 2.2 - 如何析构派生类对象 三、C11 的 override 和 final 关键字 一、多态的概念和虚…

微信扫码跳转微信小程序

一:首先声明为什么需要这样做 项目中需要在后台管理页面进行扫码支付,其他人弄了微信小程序支付,所以就需要挑战小程序进行支付,在跳转的时候需要参数例如订单编号等 二:跳转小程序的方法有多种 接口调用凭证 | 微信开放文档 具体可以参考微信开放文档 1.获取scheme码 按照文…

Spring security报栈溢出几种可能的情况

今天在运行spring security的时候&#xff0c;发现出现了栈溢出的情况&#xff0c;总结可能性如下&#xff1a; 1.UserDetailsService的实现类没有加上Service注入到容器中&#xff0c;导致容器循环寻找UserDetailsService的实现类&#xff0c;最终发生栈溢出的现象。 解决方法…

工业总线与工业以太网通信协议性能评估与比较

在现代工业自动化领域&#xff0c;通信协议是实现设备间高效通信的关键。工业总线和工业以太网是两种常见的工业通信协议&#xff0c;它们在性能和适用场景方面各有优势。本文将对工业总线和工业以太网的性能进行评估与比较&#xff0c;探讨其传输速率、实时性、可靠性等指标&a…

短视频矩阵源码saas开发搭建

一、 短视频矩阵系统源码开发部署步骤分享 确定开发环境&#xff1a;务必准备好项目的开发环境&#xff0c;包括操作系统、IDE、数据库和服务器等。 下载源码&#xff1a;从官方网站或者Github等平台下载短视频矩阵系统源码&#xff0c;并进行解压。 配置数据库&#xff1a;根…

数据结构之树型结构

相关概念树的表示二叉树二叉树性质二叉树储存 实现一颗二叉树创建遍历&#xff08;前中后序&#xff09;获取树中节点个数获取叶子节点个数获取第k层节点个数获取二叉树高度检测值为value元素是否存在层序遍历&#xff08;需要队列来实现&#xff09;判断是否为完全二叉树&…

Day48|leetcode 198.打家劫舍、213.打家劫舍II、打家劫舍|||

leetcode 198.打家劫舍 题目链接&#xff1a;198. 打家劫舍 - 力扣&#xff08;LeetCode&#xff09; 视频链接&#xff1a;动态规划&#xff0c;偷不偷这个房间呢&#xff1f;| LeetCode&#xff1a;198.打家劫舍_哔哩哔哩_bilibili 题目概述 你是一个专业的小偷&#xff0c;…

Java实现根据按图搜索商品数据,按图搜索获取1688商品详情数据,1688拍立淘接口,1688API接口封装方法

要通过按图搜索1688的API获取商品详情跨境属性数据&#xff0c;您可以使用1688开放平台提供的接口来实现。以下是一种使用Java编程语言实现的示例&#xff0c;展示如何通过1688开放平台API获取商品详情属性数据接口&#xff1a; 首先&#xff0c;确保您已注册成为1688开放平台…

Android工具条

在底层&#xff0c;所有通过主题得到应用条的活动都使用ActionBar类实现它的应用条。不过最新的应用条特性已经增加到AppCompat支持库中的Toolbar类。这意味着&#xff0c;如果你想在应用中使用最新的应用条特性&#xff0c;就需要使用支持库中的ToolBar类。 如何增加工具条 1…

C++中数组作为参数进行传递方法

文章目录 基础&#xff1a;数组作为函数形参示例&#xff1a;1、一维数组的传递&#xff08;1&#xff09;直接传递&#xff08;2&#xff09;指针传递&#xff08;3&#xff09;引用传递 2、二维数组的传递&#xff08;1&#xff09;直接传递&#xff08;2&#xff09;指针传递…

【产品规划】优先级规划

文章目录 1、功能优先级保障了产品在最短时间接受验证2、隐藏在优先级背后的是产品的目标和价值3、敏捷方法论中的功能优先级制定方法4、优先级制定时常见问题和应对方法5、敏捷方法论中的开发计划制定 1、功能优先级保障了产品在最短时间接受验证 2、隐藏在优先级背后的是产品…

C语言数值表示——进制、数值存储方式

进制 进制也就是进位制&#xff0c;是人们规定的一种进位方法对于任何一种进制—X进制&#xff0c;就表示某一位置上的数运算时是逢X进一位 十进制是逢十进一&#xff0c;十六进制是逢十六进一&#xff0c;二进制就是逢二进一&#xff0c;以此类推&#xff0c;x进制就是逢x进位…

Git基础教程-常用命令整理:学会Git使用方法和错误解决

目录 一、了解Git的基本概念 二、Git的安装和配置 Git的安装 Git的配置 用户信息 文本编辑器 差异分析工具 查看配置信息 三、Git的基本操作 基本原理 基本操作命令 基本操作示例 场景一&#xff1a;创建新仓库 场景二&#xff1a;拉取并编辑远程仓库 四、常见问…

Java“牵手”1688商品跨境属性数据,1688API接口申请指南

1688平台商品详情跨境属性数据接口是开放平台提供的一种API接口&#xff0c;通过调用API接口&#xff0c;开发者可以获取1688商品的标题、价格、库存、月销量、总销量、库存、详情描述、图片&#xff0c;重量&#xff0c;详情描述等详细信息 。 获取商品详情接口API是一种用于…

《Linux内核源码分析》(3)调度器及CFS调度器

《Linux内核源码分析》(3)调度器及CFS调度器 文章目录 《Linux内核源码分析》(3)调度器及CFS调度器一、调度器1、调度器2、调度类sched_class结构体3、优先级4、内核调度策略 二、CFS调度器1、CFS调度器基本原理2、调度子系统各个组件模块3、CFS调度器就绪队列内核源码 一、调度…

影响屏蔽箱使用效果的因素有哪些?

屏蔽箱到底是用来屏蔽什么的&#xff1f;屏蔽箱主要是为无线制造行业&#xff0c;如手机、平板、遥控器、无线网卡、蓝牙设备、路由器、GPS行业、智能家居等提供隔离测试环境&#xff0c;屏蔽外界对被测产品的干扰&#xff0c;让信号经过特殊处理和被测产品通讯。 影响屏蔽效果…