Weixin Official Accounts Platform

前外交部副部长傅莹:一旦中美闹翻,有没有国家会站在中国一边

终于找到了高清版《人间中毒》,各种姿势的图,都能看

去泰国看了一场“成人秀”,画面尴尬到让人窒息.....

2017年受难周每日默想经文(值得收藏!)

生成图片,分享到微信朋友圈

自由微信安卓APP发布,立即下载! | 提交文章网址
查看原文

SPI 机制及在Android中的使用

FredYe 徐公 2022-09-24

作者:FredYe
链接:https://juejin.cn/post/698516883725982108

SPI即 Service Provider Interface,该方案是为某个接口动态寻找服务的机制,类似IOC的思想。

SPI的使用

先通过一个简单的例子来对SPI机制有一个初步的认识




image.png

定义接口

在Android Studio中新建一个module,新增一个接口Machine, 接口定义如下:

1public interface Machine {
2    void powerOn();
3}

实现类

新增两个实现类,分别是TVComputer, 如下:

 1public class TV implements Machine {
2    @Override
3    public void powerOn() {
4        System.out.println("TV power on");
5    }
6}
7
8
9public class Computer implements Machine {
10    @Override
11    public void powerOn() {
12        System.out.println("Computer power on");
13    }
14}

定义类关系

main目录下定义一个resources.META-INF.services目录, 在该目录下添加一个名为com.fred.spi.Machine文件,需要注意该文件名必须是和上面的Machine对应上。

在 com.fred.spi.Machine

文件中添加两行

1com.fred.spi.impl.Computer
2com.fred.spi.impl.TV

测试

  • 我们定义一个MachineFactory类,用一个工厂来管理。

 1public class MachineFactory {
2    private static MachineFactory mInstance;
3    private Iterator<Machine> mIterator;
4
5    private MachineFactory() {
6        ServiceLoader<Machine> loader = ServiceLoader.load(Machine.class);
7        mIterator = loader.iterator();
8    }
9
10    static MachineFactory getInstance() {
11        if (null == mInstance) {
12            synchronized (MachineFactory.class) {
13                if (null == mInstance) {
14                    mInstance = new MachineFactory();
15                }
16            }
17        }
18        return mInstance;
19    }
20    Machine getMachine() {
21        return mIterator.next();
22    }
23    boolean hasNextMachine() {
24        return mIterator.hasNext();
25    }
26}
  • 测试入口文件

1public static void main(String[] args) {
2    MachineFactory factory = MachineFactory.getInstance();
3    while (factory.hasNextMachine()) {
4       factory.getMachine().powerOn();
5    }
6}

执行上面的代码会输出

1Computer power on
2TV power on

从java代码的层面上看,我们并没有任何地方new Computer, TV; 更没有执行其powerOn()方法,只是在一个配置文件里面加了ComputerTV对应的类名。

SPI机制原理

MachineFactory中我们可以看到,加载Machine接口的实现类只依赖于一行代码:

1ServiceLoader<Machine> loader = ServiceLoader.load(Machine.class);

接着只需要对ServiceLoader进行遍历,就可以找到所有有实现类。在上面的例子中,因为我们在com.fred.spi.Machine文件中配了两个,所以能找到两个实现类。

`ServiceLoader`的源码

我们来从ServiceLoader的源码角度看看是如何完成类加载的。由于ServiceLoader是在rt.jar包中, 我们在安装jdk的时候是可以下载一个src.zip文件,可以将该文件导入到IDE中去关联源码,但rt.jar并不在src.zip中,从这里可以拿到相关的代码。其核心代码如下:

  1public final class ServiceLoader<S>
2    implements Iterable<S>
3
{
4    private static final String PREFIX = "META-INF/services/";
5    public static <S> ServiceLoader<S> load(Class<S> service) {
6        ClassLoader cl = Thread.currentThread().getContextClassLoader();
7        return ServiceLoader.load(service, cl);
8    }
9
10    public static <S> ServiceLoader<S> load(Class<S> service,
11                                                ClassLoader loader) 
{
12        return new ServiceLoader<>(service, loader);
13    }
14
15    private ServiceLoader(Class<S> svc, ClassLoader cl) {
16        service = Objects.requireNonNull(svc, "Service interface cannot be null");
17        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
18        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
19        reload();
20    }
21
22    public void reload() {
23        providers.clear();
24        lookupIterator = new LazyIterator(service, loader);
25    }
26
27    private Iterator<String> parse(Class<?> service, URL u)
28        throws ServiceConfigurationError
29    
{
30        InputStream in = null;
31        BufferedReader r = null;
32        ArrayList<String> names = new ArrayList<>();
33        try {
34            in = u.openStream();
35            r = new BufferedReader(new InputStreamReader(in, "utf-8"));
36            int lc = 1;
37            while ((lc = parseLine(service, u, r, lc, names)) >= 0);
38        } catch (IOException x) {
39            fail(service, "Error reading configuration file", x);
40        } finally {
41            try {
42                if (r != null) r.close();
43                if (in != null) in.close();
44            } catch (IOException y) {
45                fail(service, "Error closing configuration file", y);
46            }
47        }
48        return names.iterator();
49    }
50
51    private class LazyIterator
52            implements Iterator<S>
53        
{
54
55            Class<S> service;
56            ClassLoader loader;
57            Enumeration<URL> configs = null;
58            Iterator<String> pending = null;
59            String nextName = null;
60
61            private LazyIterator(Class<S> service, ClassLoader loader) {
62                this.service = service;
63                this.loader = loader;
64            }
65
66            private boolean hasNextService() {
67                if (nextName != null) {
68                    return true;
69                }
70                if (configs == null) {
71                    try {
72                        String fullName = PREFIX + service.getName();
73                        if (loader == null)
74                            configs = ClassLoader.getSystemResources(fullName);
75                        else
76                            configs = loader.getResources(fullName);
77                    } catch (IOException x) {
78                        fail(service, "Error locating configuration files", x);
79                    }
80                }
81                while ((pending == null) || !pending.hasNext()) {
82                    if (!configs.hasMoreElements()) {
83                        return false;
84                    }
85                    pending = parse(service, configs.nextElement());
86                }
87                nextName = pending.next();
88                return true;
89            }
90
91            private S nextService() {
92                if (!hasNextService())
93                    throw new NoSuchElementException();
94                String cn = nextName;
95                nextName = null;
96                Class<?> c = null;
97                try {
98                    c = Class.forName(cn, false, loader);
99                } catch (ClassNotFoundException x) {
100                    fail(service,
101                         "Provider " + cn + " not found");
102                }
103                if (!service.isAssignableFrom(c)) {
104                    fail(service,
105                         "Provider " + cn  + " not a subtype");
106                }
107                try {
108                    S p = service.cast(c.newInstance());
109                    providers.put(cn, p);
110                    return p;
111                } catch (Throwable x) {
112                    fail(service,
113                         "Provider " + cn + " could not be instantiated",
114                         x);
115                }
116                throw new Error();          // This cannot happen
117            }
118
119            public boolean hasNext() {
120                if (acc == null) {
121                    return hasNextService();
122                } else {
123                    PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
124                        public Boolean run() return hasNextService(); }
125                    };
126                    return AccessController.doPrivileged(action, acc);
127                }
128            }
129
130            public S next() {
131                if (acc == null) {
132                    return nextService();
133                } else {
134                    PrivilegedAction<S> action = new PrivilegedAction<S>() {
135                        public S run() return nextService(); }
136                    };
137                    return AccessController.doPrivileged(action, acc);
138                }
139            }
140        }
141    } 
142}

可以看到,其思路就是:

  1. 先获得一个classloader

  2. 然后去加载META-INF/services/下面的文件,获取相关的配置,如代码:

1String fullName = PREFIX + service.getName();
2if (loader == null)
3    configs = ClassLoader.getSystemResources(fullName);
4else
5    configs = loader.getResources(fullName);
  1. 获取对应的实现类名, 即parse方法

  2. 利用反射,根据类名去创建对应的实例, 即nextService方法

Android中的应用

SPI机制能够较好的解藕,便于代码的扩展,比如有这么个场景,我们需要从多个数据源获取数据,每一个数据源相关的操作都作为一个子module集成到app中,这个时候,我们可以定义一个META-INF/services/xxx文件,来配置数据源。

如果开发者自己写过类似于ARouter这种路由框架,肯定会了解com.google.auto.service:auto-service, 该组件便是简化了SPI的使用,让开发者不需要去手动维护META-INF/services/xxx

在我们自己写一个路由框架时,会需要自己实现一个AbstractProcessor, 用来生成路由相关的配置。于是会定义一个类似的文件:

 1@AutoService(Processor.class)
2// 允许/支持的注解类型,让注解处理器处理
3@SupportedAnnotationTypes({Constants.ROUTER_ANNOTATION_TYPES})
4// 指定JDK编译版本
5@SupportedSourceVersion(SourceVersion.RELEASE_7)
6// 注解处理器接收的参数
7@SupportedOptions({Constants.MODULE_NAME, Constants.APT_PACKAGE})
8public class RouterProcessor extends AbstractProcessor {
9    private Elements elementsUtils;
10
11    ....
12
13}

手写注入

如果我们不采用自动注入的方式,我们需要自己去维护一个META-INF/services/xxx文件(上面代码中的第一行就没有必要加了),如下;

image.png

我们需要将对应Processor配置到这个文件里面,而Google的com.google.auto.service:auto-service组件便是简化了SPI的使用,让开发者不需要去手动维护META-INF/services/xxx。只需要加一个注解,由框架在编译时自动生成这个配置文件

自动注入

再回到上面的第一行代码@AutoService(Processor.class), 这个注解是auto-service这个库提供的。在编译阶段,会执行AutoServiceProcessorprocess方法,在该方法中会先调用generateConfigFiles生成配置文件,如下:

 1 private void generateConfigFiles() {
2    Filer filer = processingEnv.getFiler();
3
4    for (String providerInterface : providers.keySet()) {
5      String resourceFile = "META-INF/services/" + providerInterface;
6      log("Working on resource file: " + resourceFile);
7      try {
8        SortedSet<String> allServices = Sets.newTreeSet();
9        try {
10
11          FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",
12              resourceFile);
13          log("Looking for existing resource file at " + existingFile.toUri());
14          Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
15          log("Existing service entries: " + oldServices);
16          allServices.addAll(oldServices);
17        } catch (IOException e) {
18          log("Resource file did not already exist.");
19        }
20
21        Set<String> newServices = new HashSet<String>(providers.get(providerInterface));
22        if (allServices.containsAll(newServices)) {
23          log("No new service entries being added.");
24          return;
25        }
26
27        allServices.addAll(newServices);
28        log("New service file contents: " + allServices);
29        FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
30            resourceFile);
31        OutputStream out = fileObject.openOutputStream();
32        ServicesFiles.writeServiceFile(allServices, out);
33        out.close();
34        log("Wrote to: " + fileObject.toUri());
35      } catch (IOException e) {
36        fatalError("Unable to create " + resourceFile + ", " + e);
37        return;
38      }
39    }
40  }

最终生成了配置文件中的类便指向了我们自定义的Processor。

image.png

当这个模块在使用的时候,便可以通过该配置找到具体的实现类,并完成实例化。

推荐阅读:

什么?Android 编译线程爆了, gradle 内存 OOM 解决之路

巨丝滑 —— 自己动手撸一个Android图片编辑器(支持长图)

Android IO监控 | 性能监控系列

程序员该如何写好自己的简历,一位 5 年中大厂老哥跟你聊聊





文章有问题?点此查看未经处理的缓存