| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 移动开发 -> Android编译优化系列-kapt篇 -> 正文阅读 |
|
[移动开发]Android编译优化系列-kapt篇 |
一、背景
相信 android 开发对于 kapt 并不陌生,之前也有很多文章在编译优化过程中谈及过 Kapt,主要是针对增量编译场景。 抖音火山版同学在接入 hilt 过程中,遇到了更严重的问题: 在 16G 内存的电脑上触发 OOM。例如火山项目在执行 kapt 的过程中,不论采用 aar 依赖,还是全源码编译,均无法编译通过,可以认为 Kapt 会对内存产生比较大的影响。 在分析这个问题之前,先介绍下 kapt 的原理。 二、 Kapt 原理kapt 可以理解为就是在 kotlin 开发场景下进行注解处理的工具。至于作用可以完全等效于 java 的 apt。因为 java 的 apt 处理不了 kotlin 源码文件,所以才出现了kapt,来实现混合工程或者纯 kotlin 工程的 apt 任务。 使用起来非常简单: 你只需要引入 kapt 插件,将原来的
当你在某个 module 下引入了 ‘kotlin-kapt’,相应的模块构建过程中就会自动生成 上文说到引入了 kapt 的模块会相应的增加两个 Task,这两个 Task 会完成处理注解生成类的功能。接下来我们简单的看一下这两个 Task 的工作原理。 这里可以看到,整个 kapt 的处理过程分为了两个步骤:“生成 Stub 文件"及"调用 apt 处理注解”。可以非常清晰的看到,其实kapt并没有新的东西,底层依然是调用的 java apt 来完成的整个任务。这里多说一句, kotlin 团队为什么这么设计呢? Java 的 apt 是通过实现 JSR269 来实现的。JSR269 为 apt 插件定义了 api,Java apt 实现了这套 api。 那么作为后起之秀,想要实现类似的功能可以很容易想到如下两种方式:
显然,第二种路径更简单且更成熟,再加上在 kotlin 考虑这件事之前,业界已有先例,比如 groovy 对于 apt 的支持也是这么干的。这就不难理解 kotlin 的设计思路了,只要想办法把 kotlin 的源码转成 java 源码即可。 到这里就不难理解 kapt 的处理为什么分为了两个步骤:"生成 Stub 文件"及"调用 apt 处理注解"了。 下面说一下这两个步骤的大致流程。 生成Stub文件这个过程由 左边是一个 .kt 文件,右边是 kaptGenetateStub 生成的 .java 文件,聪明的你应该知道 kotlin 想干嘛了吧? 可以看到,这里并不是将 kotlin 源码生成与之等效的 java 源码,只是生成了类似 abi 形式的 java 源码,只要保证能找到对应的方法和字段的描述符即可,无需处理方法体的实现内容。 调用apt处理注解这个过程的大致流程:
整个 kapt 的原理就介绍到这里。接下来我们来分析一下 kapt 可能带来的问题。这里会花一部分篇幅来讲述下背景中提到的问题的解决过程。 三、kapt引发的内存问题这里再简单的描述下本文背景中提到的问题。 火山项目在接入 Hilt 的过程中,在 16G 的 mac 上打包无法通过,频繁报 OOM,对应的堆栈如下: 初看堆栈是由编译器内部报出来的问题,看起来是内存爆掉了,但是从堆栈上看不出明显的突破点。 既然是内存问题,我们先想办法复现下,推荐用 VisuaxlVM 进行分析,不了解该工具的同学可以点击链接学习下,算是比较好用的JVM问题排查工具了。 我们用 VisualVM 对 Gradle daemon 进程进行了内存分析。发现在 kapt 过程中,内存确实一直在往上涨。 为了能知道这些内存突然上涨的地方在代码里究竟发生了什么,我们得想办法进行代码调试。 kotlin 的 debug 比 gradle 稍微麻烦一些,kotlin compiler 在运行的时候,有三种模式。
默认情况下,kotlin compiler 的代码是运行在 kotlin 的 daemon 进程中的,这里我们为了方便,可以直接指定为 in-process 模式。这样一来,相当于在 gradle 的 daemon 进程中进行调试,岂不是方便很多,进行如下设置即可。
能够断点调试后,通过 debug kotlin,很容易就梳理出 kapt 的完整执行流程,如下图所示: 跟着 jdk 的代码走了一遍之后,我们大概知道了在 jdk 中是这样处理 apt 的。 但火山项目实在太庞大了,一个 heap dump 就达 10 几 G,如果直接选择某个 Scope$Entry[] 对象进行GC Root 分析的话,等一天也完不成。 所以采用一个接入了 hilt 的 demo 进行测试。 从第一轮开始,选择一个 Scope$Entry[] 对象,此时它的 GC Root 如下: 第二轮,此时 GC Root 如下: 注意到 JavacProcessingEnvironment 中有这样一段代码:
而 cleanTrees() 的操作如下:
treeCleaner 的定义如下:
显然,jdk 的设计者想通过遍历 JCTree,将语法树上包括符号表在内的各对象置为空,从而让这些对象有被释放的机会。 但是,这样的操作并没有释放掉符号表的引用,比如这里就保存在 log 的 diagFormatter 对象中。 不过如果仅仅是这样,问题也还不严重,因为从 GC Root 图可发现 log.diagFormatter 每次都只保存前一次的符号表。 第三轮,这个时候总该释放了吧,毕竟此时 log.diagFormatter 也没保存它了,但结果是它竟然还有 GC Root,如下: 显然,是有某个 JNI Global Reference 持有了它,导致它无法被释放。 到这里可以确定,由于 jdk8 的设计,导致每一轮处理注解而创建的符号表,都会一直保留在内存中,一直到全部处理完才释放,从而导致对于代码量大或者 processors 数量多(比如 hilt 引入了13个 processor )的项目,就很容易因为占用内存过大而导致 OOM。这个锅 jdk 得背着。 想判断 kapt 过程是否有内存泄漏,可配置打开 log 开关查看。 如果在 annotation processing 过程就发生了 OOM,那么它只能抛出异常,根本都不会走到内存泄漏探测这一步。可见这个内存泄漏检测,对于本文的排查工作起不到什么大的作用。 解决方案虽然定位到了问题在 jdk 里面,但官方一时间也不可能给解决,更何况这还是一个比较老的 jdk 版本。那只能想想别的办法了。 由于 jdk 中进行 annotation processing,会先将输入的 java 文件进行语法分析,构建符号表,从而新建非常多类似 Scope$Entry[] 这样的对象。 在 debug 中发现,一个源文件对应一个JCCompilationUnit,而一个 JCCompilationUnit 中就包含一棵语法树。 从这里可以推断出,annotation processing 的内存占用与输入的源文件成正比关系。 那么是否可以通过过滤输入的源文件减少内存占用呢? 我们分析了一遍输入的文件,发现在 app module 中有大量的 R.java 参与了 kapt 编译,对于中大型项目而言,至少会存在几千到上万个,关于 R.java 在 app 编译中的作用,在这里就不赘述了。 其实对于 app module 来说,R.java 只是辅助编译的作用。一般来说,app module 都比较轻量,很少会放很多代码,但是由于 R.java 要参与辅助编译,所以 R.java 被 agp 塞到了 在火山的项目中,有 95% 的输入文件都是 R.java,并且每个 R.java 都有大几千行的代码。因为 R.java 里面都是一些没有注解的 field。 可以说,R.java 文件是与 kapt 无关的,完全没必要参与语法分析,增加额外的执行时间和内存。 所以,将 KaptTask 中 javaSourceRoots 的代码改为如下,过滤掉生成的 R.java。
收益目前该 feature 一魔改版的 kotlin 已经接入火山,今日头条等项目。 对于火山来说,app:kapt task 从 18min 发生 OOM,变为 15s 编译通过,不仅减少了很多编译时间,而且节约了 13G+ 的内存空间。 而对于其他之前未发生 OOM 的 kapt task, 其实也一样有收益,如下图是在头条进行测试前后的对比图: 另外多说一句:在 debug jdk 的过程中,发现 jdk 8 无论从模块解耦,还是内存管理都做得并不好,不过也能理解,毕竟这主要是 2013 年完成的代码。所以,从编译优化的角度看,尽快升级项目中使用的 jdk 版本也是一件收益较大的事情(事实上使用 jdk9 就能编过,虽然还是慢)。 需要注意的是,以上优化适应于AGP 3.6.0之前,在AGP 3.6.0之后,由于参与编译的是R.jar而不是R.java, 不存在此问题,本文重点阐述的是kapt的原理,遇到相关问题的排查过程以及进行优化的思路。最后,针对 Kapt 相关优化给出几点建议。 四、Kapt的建议与优化要想 kapt 的使用不引入大的编译相关负向收益,我们有以下几点建议: 之前遇到很多项目组,为了方便会创建一个 library.gradle/base.gradle 这样的文件,这个文件中定义了很多通用的 kapt 依赖,随着项目模块化组件化的改造,项目中模块数量越来越多,一些只包含 model 类和接口、完全不需要 kapt 的 api 模块也被统一的使用到了这些 kapt 依赖,使得项目中有大量模块进行了无意义的 kapt 耗时, 因此我们建议:
本文只阐述了kapt关于内存问题的一个相关优化,其实 kapt 及 kotlin 编译还有很多的问题值得去优化。目前在字节内部,我们团队开发了一系列优化工具来无感知地解决此类问题来加快增量编译速度。受限于篇幅原因,这里不进行展开说明,后续会有单独的文章来阐述相关内容。 在项目中使用 kapt 无非是需要一个通用的代码生成逻辑,减少重复代码的编写,能实现类似效果的方案不仅仅只有 kapt :
kapt 需要先经过 kaptGenerateStub 将 kotlin 代码转换为 java 代码,然后再交给 jdk 处理,这样显然太麻烦了。那么,是否可以直接在 kotlin compiler 中就进行 annotation processing 呢?答案是肯定的,实际上 kotlin 官方在更高的版本上已经有了这样的方案,叫 Kotlin Symbol Processing(KSP),不过目前还处于 alpha 阶段,还需要等待各大 processor 进行适配。等稳定之后我们会推出关于 KSP 的最佳实践,帮助大家更好地进行 annotation processing 的开发。 五、加入我们Build Infra 团队致力于解决 android 研发体验问题,提升 android 编译体验,负责保障和提升公司内各业务线的研发构建效率。如果你对技术充满热情,追求极致,欢迎加入我们,我们期待你与我们共同成长 。目前我们在北京、上海、杭州均有招聘需求,简历投递邮箱: lanjunjian@bytedance.com , 邮件标题是:姓名-Devops-Build Infra. 🔥 火山引擎 APMPlus 应用性能监控是火山引擎应用开发套件 MARS 下的性能监控产品。我们通过先进的数据采集与监控技术,为企业提供全链路的应用性能监控服务,助力企业提升异常问题排查与解决的效率。目前我们面向中小企业特别推出**「APMPlus 应用性能监控企业助力行动」,为中小企业提供应用性能监控免费资源包。现在申请,有机会获得60天免费性能监控服务,最高可享6000万**条事件量。 👉 点击这里,立即申请 |
|
移动开发 最新文章 |
Vue3装载axios和element-ui |
android adb cmd |
【xcode】Xcode常用快捷键与技巧 |
Android开发中的线程池使用 |
Java 和 Android 的 Base64 |
Android 测试文字编码格式 |
微信小程序支付 |
安卓权限记录 |
知乎之自动养号 |
【Android Jetpack】DataStore |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 16:55:14- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |