QQ音乐Android端120万行代码,编译耗时是怎样优化的,互联网寒冬公司倒闭后

本文介绍了阿里巴巴开发的增量编译组件,该组件显著提升了本地开发速度,特别是对于Kotlin支持和资源管理的改进。组件基于Gradle,具有低侵入性和多平台兼容性,通过代码依赖分析和资源增量编译大幅减少编译时间。
摘要由CSDN通过智能技术生成

另一个就是阿里推出的Freeline方案了,它可以充分利用缓存文件,在几秒钟内迅速地对代码的改动进行编译并部署到设备上,提速效果十分明显。不过它同样存在着一些不可忽视的问题。首先是不支持Kotlin,这在Kotlin已经被谷歌官宣为Android开发首选语言的今天,是比较致命的。另外,不支持删除带id的资源,否则可能导致资源编译流程出错。

另外一个潜在的问题是,为了确保编译速度,Freeline是牺牲了一部分正确性的。例如,在改动公有静态常量的时候,只会编译对应的类文件,而引用到该常量的其他类,并不会参与编译的。由于常量内联优化的存在,就可能导致这些类在运行时,使用的仍然是旧的值,进而出现改动不生效的问题。

综合上述,目前业界已有的解决方案,并不能满足我们的需求。所以在2019年初,我们开启了增量编译组件的自研之路。

4. 增量编译的诞生

在2019年6月份,增量编译组件完成了首版开发,开始正式接入QQ音乐工程。

接入后,对于本地开发的提速效果是比较明显的。据团队实际数据统计,进行一次全量编译的耗时约为418秒,而增量编译单次耗时仅需13秒。以天为单位计算,每个人花在工程编译上的总时长,由3.95小时,降低至了1.02小时,效率提升达到74%

增量编译组件完全基于Gradle标准,实现为一个Gradle插件,具备良好的多平台兼容性,而且对于目标工程的侵入性极低。使用者只需要接入我们的Gradle插件,即可通过执行特定的Gradle任务,进入增量编译模式。

在功能的支持上,组件支持Java、Kotlin等代码文件以及所有类型资源文件的快速编译。在今年年初,加入了DataBinding的增量支持。而且,为了进一步减少使用成本,我们还在最新版本中提供了配套的Android Studio插件,开发者可以通过可视化的方式,更方便的使用组件功能。

下图描述了组件的整体原理,我们将开发周期分为编译期和运行期。

首次编译(亦可称全量编译),需要完整编译工程,得到原始安装包,耗时与原生的打包任务持平。后续再触发编译,将会进入耗时极短的增量编译模式,组件会负责收集改动过的代码进行编译,得到增量产物,并推送到手机上。

运行期则负责将手机上的增量产物进行动态加载运行。

在本文的后续内容中,将介绍几个重点模块的实现。

5. 核心原理

代码编译

(1)获取改动文件并进行编译

首先需要考虑的问题是,如何识别出用户改动了哪些文件?

我们的做法是,在每次编译成功后,收集所有工程文件的最后修改时间,保存为一份文件快照。在下次编译开始时,组件会生成最新的文件快照,与上一次的文件快照进行比对,就可以收集到用户改动过的文件了。

为了能够单独编译这些文件,还需要解决类引用的问题。

在首次完整编译工程时,组件会收集所有生成的class文件,放到缓存目录中。在编译被改动的文件时,会调用原生的javac或者是kotlinc程序,将刚才的缓存目录作为classpath传递进去,就可以解决编译时代码引用的问题了。

(2)进行代码依赖分析

上文中,提供classpath可以使编译阶段成功执行,却无法确保运行期的代码逻辑是正确的。举个例子,某个类修改了某个方法的参数列表,那么除了这个类需要被编译外,依赖这个类的其他类,也是需要重新编译的。否则,就会在运行期,出现NoSuchMethodException。

因此,由于代码之间相互依赖关系的存在,仅仅收集被用户改动的代码来编译,是不够的。还可能需要找出它的子依赖集,纳入编译范围。

沿着这个思路,还需要考虑两个问题:

  • 如何得到改动类的变化类型? 修改方法内部实现等类型的改动,是不会影响到其子依赖集的。在确保编译正确的前提下,为了尽可能地减少参与编译的代码数量,我们需要得到被改动类的变化类型,才能够决定是否需要将其子依赖集重新进行编译。
  • 如何得到改动类的子依赖集? 这个很好理解,只有计算出某个类的子依赖集,组件才能知道要编译什么。

想获取这两项信息,都需要对类的内部结构进行分析,提取出类名、类的修饰符、成员变量、方法等数据。我们的做法是,引入ASM工具对class文件进行解析,然后将解析出来的信息,保存到自定义的ResolvedClass数据结构中。

接下来的解决方案是这样的:

  1. 在全量编译期间,组件会同步启动一个独立的进程,对所有的class文件进行遍历分析,得到对应的ResolvedClass信息,并保存在本地文件中。其中,如果发现某个类引用了另一个类,那么就会把当前类的类名,添加到被引用类的子依赖集列表中(resolvedBy字段)。

  2. 触发增量编译后,组件首先编译改动类,得到新的class文件。然后启动代码依赖分析流程,解析出新的ResolvedClass,将其与全量编译期解析出来的旧ResolvedClass进行比对,就可以得到这个类的改动类型了。

当发现当前类的改动类型在下表中,组件才会获取其子依赖集,启动第二轮编译,得到子依赖集对应的class文件。

通过上面的方式,我们在确保编译正确的前提下,尽可能地减少了需要编译的代码数量。

随后,增量编译期间生成的所有class文件,会被dx工具进一步地编译为Dex文件,然后通过ADB推送到手机上,等待被动态加载。

资源编译

(1)资源增量

这一块的基本思路,与代码增量是类似的。即先收集被改动的资源,然后进行编译。

原生的资源编译流程主要采用的是aapt,或者是aapt2 。

一开始,我们工程使用的仍然是aapt,基于它去资源增量的难度相对较大。因为aapt工具是不支持单个资源编译的。Freeline通过修改aapt的源码,实现了单个资源的增量功能。不过他们的这部分方案没有开源,并且改动后仍然不支持带ID资源的删除,所以没有考虑在组件中引入。

再来看看aapt2。与aapt最大的不同在于,它是天然支持单个资源编译的。其内部把资源的打包分成了 编译(compile)与链接(link) 两步,在编译阶段,负责将单个或者多个资源编译为二进制文件;链接阶段,则负责合并所有二进制文件再打包。

于是,我们首先升级工程的工具链,引入了aapt2,然后组件也基于此重新设计了资源增量方案。

在工程首次编译结束之后,组件会将所有编译好的资源二进制文件都收集到一个缓存目录中。后续改动资源时,会先调用aapt2的编译功能,将改动的资源编译成为二进制文件。然后将新的二进制文件拷贝到资源缓存目录中,覆盖掉同名文件。

接着,会针对这个目录,采用aapt2的链接功能,打包生成最后的增量资源包,并推送到手机上,等待被动态加载。

通过这样改造后,QQ音乐工程中资源增量编译阶段的耗时,由原来的32秒降低到了12秒,效率得到进一步提升。

(2)资源ID固定

资源编译过程中,有一个文件是需要特别关注的:R.java文件。

为了让开发者能够在代码中引用资源,资源编译器会在编译的过程中,为每一个资源分配索引ID,并以公有静态常量的方式保存在R.java文件中。开发者只需要在代码中通过R.color.text等形式,即可引用到对应的资源。

而编译器编译源代码时,如果发现某处代码引用了常量(同时使用static和final两个关键字来修饰),且该常量为字面值形式的原始数据类型或字符串时,编译器就会将此处的常量引用替换为常量值

也就是说,代码中类似R.color.text的引用,在class文件中都会被替换成为对应的数字。

资源编译的过程中,资源是按照名称排序后,按序递增分配索引的。如果新增或者删除资源,会导致其后续资源的索引出现错位。

在这种场景下,如果某个类引用到索引变化了的资源,就需要重新参与编译。否则,就会在运行时遇到资源引用错乱的问题。

但是这就会导致大量的类需要在增量过程中参与编译,和我们的初衷是相违背的。

所以,需要将R.java中的ID进行固定。简单来说,就是使得两次编译之间,对于同一个资源,分配到的ID是不变的。其实在热修复场景下,也具有相同的诉求。对于补丁包,是有严格的大小要求的。如果我们要对资源进行热修复,不可能把所有用到该资源的代码都重新编译纳入补丁包中下发,所以也需要进行资源ID固定。

相对应的解决方案也是业界比较通用的。若尝试输出aapt2命令行工具的帮助文档,可以发现有两个参数:

  • –stable-ids: File containing a list of name to ID mapping.
  • –emit-ids : Emit a file at the given path with a list of name to ID mappings, suitable for use with --stable-ids.

因此,我们可以在编译资源的时候,给aapt2注入emit-ids参数,在指定文件中输出资源名称到资源ID之间的映射关系。并在下次启动aapt2时,通过stable-ids传入刚才的映射关系,达到资源ID固定的效果。

动态加载

(1)代码注入

编译完成后,可以得到若干个增量Dex包,并推送到手机的特定目录下。

那么在运行期,我们需要做的,是干涉原生的类加载流程,使被改动的代码优先被加载,达到改动生效的目的。

先来看看Android原生的类加载流程。

在应用程序启动后,会采用名为PathClassLoader的类加载器,去加载安装包中的Dex文件。需要加载某个类的时候,系统会从前往后依次遍历Dex数组,直到找到对应的类。

基于此,增量组件会在应用启动的时候,将增量Dex文件,通过反射手段插入Dex数组的最前面。后续需要加载某个类的时候,由于系统机制会从前往后遍历,因此会优先从增量的Dex中查找并命中改动后的类。需要说明的是,所有增量的Dex,会按照生成的时间,倒序插入到Dex数组中,如inc_3.dex、inc_2.dex、inc_1.dex,这样就可以确保一个类被多次增量修改后,被加载到的总是其最新实现。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

img
img

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V:vip204888 备注Android获取(资料价值较高,非无偿)
img

尾声

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题* (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-3mu09qHq-1711585568116)]

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值