JDK ServiceLoader加载优化

本文记录在启动优化项目中解决ServiceLoader加载长耗时问题的方案,介绍了JDK ServiceLoader在安卓平台中会带来哪些问题,方案执行过程中遇到的问题和相应的思考。特别是在一个负责的业务系统中,如何选择技改方案,保证方案的顺利落地。

0x01 什么是Java SPI?

SPI,全称Service Provicer Interface。在JDK1.6内置的一套服务发现机制,主要用来服务架构扩展和替换组件。在面向对象设计中,最基本的原则就是面向接口编程,通过接口暴露模块的功能定义,将具体的接口实现隐藏在模块内部。而服务发现机制,提供了通过接口获取服务实现的能力,实现模块的可插拔、可拓展、可替换。保证了由调用方在调用时选择自己需要的外部实现。

image.png
简单来是在利用JDK提供的ServiceLoader机制可以通过以下几个步骤:

  1. 定义接口类
  2. 定义接口实现类
  3. 将实现类的类名保存在META-INFO/services目录中,文件名为接口类的类名
  4. 使用java.util.ServiceLoader.load方法加载接口类,获取到在META-INFO中定义的接口实现类

在META-INFO中的声明形式大致如下所示:
image.png

0x02 JDK ServiceLoader存在哪些问题?

Autoservice组件解决了什么问题?

我们经常使用的autoservice(https://github.com/google/auto)解决了什么问题呢?其实autoservice只是提供了注解和注解处理器的能力,通过自定义一个annotationprocessor将@AutoService注解标记的实现类信息,收集起来并保存到META-INFO/services目录中。通过注解的方式将上面的步骤3优化掉了,这样既提高了效率,又避免了方案变更时需要手动更新配置文件的问题。

Android中使用JDK ServiceLoader会带来什么问题?

主要是类配置文件的加载耗时问题。在java项目中,java.util.ServiceLoader通过加载jar文件META-INFO/services目录下的配置文件获取实现类信息,当在Android项目中,java.util.ServiceLoader时通过加载apk包目录下META-INFO/services中的配置文件,这里都涉及到整个zip文件的遍历,加载效率是非常低的,一般情况下会导致几百毫秒的耗时。而如果在一些关键流程中触发了JDK ServiceLoader的加载,几百毫秒就会带来很差的用户体验。
image.png

0x03 如何解决JDK ServiceLoader带来的问题?

先梳理现状:

  1. 项目中大量使用了多个改造自autoservice组件的注解处理器,在meta-info目录下生成了多个配置文件
  2. 耗时主要发生在启动流程业务初始化阶段,使用JDK serviceloader加载meta-info下的配置文件导致了长耗时

ServiceLoader加载耗时过长的主要原因是,需要从zip文件中检索出meta-info目录下的配置文件,想要提升配置加载的效率,解决方法也很简单,可以将配置文件保存到更容易加载的地方,这里可以选择将配置信息保存到安卓原生提供的资源路径下,也可以选择将配置信息保存到类文件中。当然想要在相对复杂的业务系统中解决一个问题,既包括解决技术问题本身,也需要考虑到方案落地的成本和影响面。特别是高业务价值低技术价值的遗产系统中做技术优化,首先保证的是兼容现有业务逻辑。这里优先选择将相关配置信息保存在类文件中,借助android编译插件技术,在编译期间做信息收集,在运行时通过类加载的方式将所需service信息加载出来。这里可以主要参考wmrouter的实现思路,但是又稍有差别。
先对比wmrouter的实现方案,可以发现大致可以分为以下几个步骤:

  1. 开发编码流程,定义服务接口,通过注解标记服务实现类
  2. 组件aar发布流程,通过annotationprocessor生成.class的文件,将服务实现的类信息保存在自动生成的.class文件中
  3. 编译打包流程,通过gradle插件收集步骤2中新生成的所有类,因为服务实现可能分布在项目中的多个组件中,所以在apk整包的编译流程,需要将所有服务实现的类信息统一收集起来,并加入到初始化的代码中。

通过以上流程的说明,可以看到整个流程被分为两步:

  1. annotationprocessor通过将注解转换成jvm字节码
  2. 通过AGP transform生成初始化代码,将jvm的初始化调用收集起来在初始化时统一调用

前面也提到,在现有的遗产系统中,如果照搬wmrouter的方案会存在什么问呢?如果照搬上述的方案,在annotationprocessor处理阶段,需要将所有使用autoservice @AutoService的组件使用新的annotationprocessor重新发布一边。这个工作量和变更范围是巨大的。有没有更优化的方案呢?

解决思路也很直接,将注解的收集和配置信息保存到类文件合并成一步。

通过在AGP transform流程中收集注解,同时使用ASM框架动态生成相关字节码,将相关的实现类信息保存到类文件中。就可以避免重新发布大量业务组件的问题,将变更范围控制到最小。

image.png

0x04 总结

ServiceLoader带来的性能问题,一旦被发现,解决思路其实是很直接的,并没有太多拐弯的地方。但是在这个技术方案落地的过程中遇到的问题比技术方案本身多很多。特别是在已有大量业务系统在使用的组件中做技改,影响面的评估和回滚方案是要优先被考虑的,所以在运行时加载配置阶段,需要通过配置开关控制是优先加载类信息还是保持原加载流程不变,来做到技改方案可观测可回滚。同时在技术方案的选择上,应该选择影响面尽可能小,变更尽可能少的方案。尽可能避免业务方的更改。

参考文档

https://zhuanlan.zhihu.com/p/28909673
https://github.com/meituan/WMRouter