spi在android中的使用

匿名 (未验证) 提交于 2019-12-03 00:30:01

概述

  • 什么是spi

SPI (Service Provider Interface)属于动态加载接口实现类的的一项技术,是JDK内置的一种服务提供发现机制,使用ServiceLoader去加载接口对应的实现,这样我们就不用关注实现类,ServiceLoader会告诉我们。官方文档描述为:为某个接口寻找服务的机制,类似IOC思想,将装配的控制权交给ServiceLoader。

  • 解决问题

只提供服务接口,具体服务由其他组件实现,接口和具体实现分离(类似桥接),同时能够通过系统的ServiceLoader拿到这些实现类的集合,统一处理,这样在组件化中往往会带来很多便利,SPI机制可以实现不同模块之间方便的面向接口编程,拒绝了硬编码的方式,解耦效果很好

即相当于制定标准,然后不同实现方用不同的方式实现标准供使用方使用,并且可以动态加载

在Android中如何使用

上面说的可能比较抽象,下面将结合例子说明下在Android中的运用。

这种机制在使用起来也比较简单,使用步骤如下:

  • 定义接口和接口的实现类

  • 创建resources/META-INF/services目录

  • 在上述Service目录下,创建一个以接口名(类的全名) 命名的文件, 其内容是实现类的类名 (类的全名)。

在services目录下创建的文件是com.binglumeng.spidemo.IService 文件中的内容为Animal接口的实现类, 可能是com.binglumeng.spidemo.AService

  • 在java代码中使用ServcieLoader来动态加载并调用内部方法.

主工程和组件之间一些“服务”的配置

定义接口

 package com.example;  public interface IDisplay {     String display(); } 

在主工程和bdisplay 模块中的实现该接口

创建spi描述文件

在工程的main目录下新建目录resources/META-INF/services,以服务接口名为文件名新建spi描述文件,内容为具体的服务实现类权限定名,可以有多个

文件结构如下

加载不同服务

通过ServiceLoader来加载接口的不同实现类,然后会得到迭代器,在迭代器中可以拿到不同实现类全限定名,然后通过反射动态加载实例就可以调用display方法了。

ServiceLoader<Display> loader = ServiceLoader.load(IDisplay.class); mIterator =loader.iterator();  while(mIterator.hasNext()){     mIterator.next().display(); } 

源码分析

感觉有点很神奇

就可以拿到Display.class接口的所有实现类了, amazing!(感觉这里跟Retrift使用有点类似)下面来分析一下这个背后到底隐藏了什么

ServiceLoader.java

先看下几个重要的成员变量

  • PREFIX就是配置文件所在的包目录路径;
  • service就是接口名称,在我们这个例子中就是Display;
  • loader就是类加载器,其实最终都是通过反射加载实例;
  • providers就是不同实现类的缓存,key就是实现类的全限定名,value就是实现类的实例
  • lookupIterator就是内部类LazyIterator的实例。
private static final String PREFIX = "META-INF/services/";  // The class or interface representing the service being loaded  private Class<S> service;  // The class loader used to locate, load, and instantiate providers  private ClassLoader loader;  // Cached providers, in instantiation order  private LinkedHashMap<String,S> providers = new LinkedHashMap<>();  // The current lazy-lookup iterator  private LazyIterator lookupIterator; 

之前spi加载的三个关键步骤

  • mIterator =loader.iterator();
  • while(mIterator.hasNext()){
    mIterator.next().display();
    }

获取实现接口集合

ServiceLoader提供了两个静态的load方法,如果我们没有传入类加载器,ServiceLoader会自动为我们获得一个当前线程的类加载器,最终都是调用构造函数。

 public static <S> ServiceLoader<S> load(Class<S> service) {     ClassLoader cl = Thread.currentThread().getContextClassLoader();     return ServiceLoader.load(service, cl); }  public static <S> ServiceLoader<S> load(Class<S> service,                                             ClassLoader loader){     return new ServiceLoader<>(service, loader); } 

构造函数中有一个重要的函数reload

 public void reload() {     providers.clear();     lookupIterator = new LazyIterator(service, loader); }  private LazyIterator(Class<S> service, ClassLoader loader) {     this.service = service;     this.loader = loader; } 

所以看到当我们load class之后并没有得到什么实现类,那么在何时加载的呢?

懒加载

那么service provider在什么地方进行加载?我们接着看第二个步骤loader.iterator(),

  • 首先会到providers中去查找有没有存在的实例,有就直接返回,没有再到LazyIterator中查找
 public Iterator<S> iterator() {     return new Iterator<S>() {          Iterator<Map.Entry<String,S>> knownProviders             = providers.entrySet().iterator();          public boolean hasNext() {             if (knownProviders.hasNext())                 return true;             return lookupIterator.hasNext();         }          public S next() {             if (knownProviders.hasNext())                 return knownProviders.next().getValue();             return lookupIterator.next();         }          public void remove() {             throw new UnsupportedOperationException();         }      }; } 

其实就是返回一个迭代器。我们看下官方文档的解释,这个就是懒加载实现的地方,
焦点聚焦在LazyIterator

  • hasNext()
  1. 首先拿到配置文件名fullName,我们这个例子中是com.example.Display
 public boolean hasNext() {     if (nextName != null) {         return true;     }     if (configs == null) {         try {             //首先拿到配置文件名fullName             String fullName = PREFIX + service.getName();             if (loader == null)             configs = ClassLoader.getSystemResources(fullName);             else                 configs = loader.getResources(fullName);         } catch (IOException x) {         fail(service, "Error locating configuration files", x);         }     }     while ((pending == null) || !pending.hasNext()) {         if (!configs.hasMoreElements()) {             return false;         }         //依次扫描每个配置文件的内容,返回配置文件内容Iterator<String> pending         pending = parse(service, configs.nextElement());     }     nextName = pending.next();     return true; } 

Tips

关于 ClassLoader.getSystemResources(fullName)可以查阅

  • next()

在上面hasNext()方法中拿到的nextName就是实现类的全限定名,接下来我们去看看具体实例化工作的地方next():

  • 1.首先根据nextName,Class.forName加载拿到具体实现类的class对象
  • 2.Class.newInstance()实例化拿到具体实现类的实例对象
  • 3.将实例对象转换service.cast为接口
  • 4.将实例对象放到缓存中,providers.put(cn, p),key就是实现类的全限定名,value是实例对象。
  • 5.返回实例对象
 public S next() {     if (!hasNext()) {         throw new NoSuchElementException();     }     String cn = nextName;     nextName = null;     Class<?> c = null;     try {         //首先根据nextName,Class.forName加载拿到具体实现类的class对象         c = Class.forName(cn, false, loader);     } catch (ClassNotFoundException x) {         fail(service,              "Provider " + cn + " not found", x);     }     if (!service.isAssignableFrom(c)) {         ClassCastException cce = new ClassCastException(                 service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());         fail(service,              "Provider " + cn  + " not a subtype", cce);     }     try {         //将实例对象转换service.cast为接口         S p = service.cast(c.newInstance());         //将实例对象放到缓存中,providers.put(cn, p),key就是实现类的全限定名,value是实例对象         providers.put(cn, p);         return p;     } catch (Throwable x) {         fail(service,         "Provider " + cn + " could not be instantiated: " + x, x);     }     throw new Error();          // This cannot happen } 

总结

Spi的优缺点

  • 优点

只提供服务接口,具体服务由其他组件实现,接口和具体实现分离,同时能够通过系统的ServiceLoader拿到这些实现类的集合,统一处理。

  • 缺点
  1. Java中SPI是随jar发布的,每个不同的jar都可以包含一系列的SPI配置,而Android平台上,应用在构建的时候最终会将所有的jar合并,这样很容易造成相同的SPI冲突,常见的问题是DuplicatedZipEntryException异常
  2. 读取SPI配置信息是在运行时从jar包中读取,由于apk是签过名的,在从jar中读取的时候,签名校验的耗时问题会造成性能损失

后续可以改进的点

Java中使用ServiceLoader去读取SPI配置信息是在程序运行时,我们可以将这个读取配置信息提前,在编译时候就搞定,通过gradle插件,去扫描class文件,找到具体的服务类(可以通过标注来确定),然后生成新的java文件,这个文件中包含了具体的实现类。这样程序在运行时,就已经知道了所有的具体服务类,缺点就是编译时间会加长,自己需要重新写一套读取SPI信息、生成java文件等逻辑。

经过优化后,SPI已经偏离了原本的初衷,但是可以做更多的事,可以将业务服务分离,通过SPI找到业务服务入口,业务组件化,抽成单独的aar,独立成工程。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!