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" } != 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源码中就替换了原来的方式,进而对工程内所有的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@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

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue中解决ajax跨域问题(no “access-control-allow-origin”)

文章目录 跨域报错信息产生原因举例解决方法方式一优缺点方式二优缺点 跨域报错信息 产生原因 跨域是是因为浏览器的同源策略限制&#xff0c;是浏览器的一种安全机制&#xff0c;服务端之间是不存在跨域的。 所谓同源指的是两个页面具有相同的协议、主机和端口&#xff0c;三…

【Qt学习】05:自定义封装界面类

OVERVIEW 自定义封装界面类1.QListWidget2.QTreeWidget3.QTableWidget4.StackedWidget5.Others6.自定义封装界面类-显示效果&#xff08;1&#xff09;添加设计师界面类&#xff08;2&#xff09;在ui中设计自定义界面&#xff08;3&#xff09;在需要使用的界面中添加&#xf…

深入分析负载均衡情景

本文出现的内核代码来自Linux5.4.28&#xff0c;为了减少篇幅&#xff0c;我们尽量不引用代码&#xff0c;如果有兴趣&#xff0c;读者可以配合代码阅读本文。 一、有几种负载均衡的方式&#xff1f; 整个Linux的负载均衡器有下面的几个类型&#xff1a; 实际上内核的负载均衡…

激活潜能:探索职场中的自我效能感之道

引言&#xff1a;自我效能感的定义与重要性 自我效能感&#xff0c;简而言之&#xff0c;是个体对自己能够成功完成某项任务的信心。这种信心不仅影响我们的思考方式和情感&#xff0c;还影响我们的行为和动机。在职场中&#xff0c;高自我效能感的人往往更有动力&#xff0c;…

智能工厂移动式作业轻薄加固三防平板数据采集终端

在这个高度自动化和数字化的环境中&#xff0c;数据采集变得尤为重要。为了满足这个需求&#xff0c;工业三防平板数据采集终端应运而生。工业三防平板数据采集终端采用了轻量级高强度镁合金材质&#xff0c;这使得它在保持轻薄的同时具有更强的坚固性。这种材质还具有耐磨防损…

lab9 fs

文章目录 Large filestaskhints思路 Symbolic linkstaskhints思路sys_symlinksys_open Large files 目标&#xff1a;11256256*256个block inode的格式在fs.h的struct dinode中被定义&#xff0c;你需要特别注意以下几点 NDIRECTNINDIRECTMAXFILEaddrs[] 在磁盘上找一个文件…

ChatGPT癌症治疗“困难重重”,真假混讲难辨真假,准确有待提高

近年来&#xff0c;人工智能在医疗领域的应用逐渐增多&#xff0c;其中自然语言处理模型如ChatGPT在提供医疗建议和信息方面引起了广泛关注。然而&#xff0c;最新的研究表明&#xff0c;尽管ChatGPT在许多领域取得了成功&#xff0c;但它在癌症治疗方案上的准确性仍有待提高。…

如何将储存在Mac或PC端的PDF文件传输到移动设备呢?

iMazing是一款iOS设备管理软件&#xff0c;用户借助它可以将iPad或iPhone上的文件备份到PC或Mac上&#xff0c;还能实现不同设备之间的文件传输&#xff0c;能很大程度上方便用户进行文件管理。 在阅读方面&#xff0c;iPad和iPhone是阅读PDF的优秀选择&#xff0c;相较于Mac或…

如何制作党建专题汇报片

通过展示党组织的凝聚力和战斗力&#xff0c;增强党员的组织归属感和团结合作意识。通过宣传片&#xff0c;可以加强党组织的凝聚力&#xff0c;推动党的事业发展。制作党建专题汇报片需要一定的前期准备和后期制作技巧。下面是由深圳党建专题汇报片制作公司老友记小编为您整理…

Java实现根据关键词搜索1688商品新品数据方法,1688API节课申请指南

要通过1688的API获取商品新品数据&#xff0c;您可以使用1688开放平台提供的接口来实现。以下是一种使用Java编程语言实现的示例&#xff0c;展示如何通过1688开放平台API获取商品新品数据&#xff1a; 首先&#xff0c;确保您已注册成为1688开放平台的开发者&#xff0c;并创…

【Go 基础篇】切片:Go语言中的灵活数据结构

在Go语言中&#xff0c;切片&#xff08;Slice&#xff09;是一种强大且灵活的数据结构&#xff0c;用于管理和操作一系列元素。与数组相比&#xff0c;切片的大小可以动态调整&#xff0c;这使得它成为处理动态数据集合的理想选择。本文将围绕Go语言中切片的引入&#xff0c;介…

Vue2向Vue3过度核心技术工程化开发和脚手架

目录 1 工程化开发和脚手架1.1 开发Vue的两种方式1.2.脚手架Vue CLI 2 项目目录介绍和运行流程2.1 项目目录介绍2.2 运行流程 3 组件化开发4 根组件 App.vue4.1 根组件介绍4.2 组件是由三部分构成4.3 总结 5 普通组件的注册使用-局部注册5.1 特点&#xff1a;5.2 步骤&#xff…