Android AutoService原理

2,820 阅读7分钟

一、AutoService 介绍

1、AutoService 简介

AutoService 是谷歌提供的一个组件,使用简单但是功能却是非常强大。AutoService 会自动在 META-INF 文件夹下生成 Processor 配置信息文件,该文件包含实现该服务接口的具体实现类。当外部程序加载这个模块时,可以通过该 jarMETA-INF/services/ 里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

AutoService 其实是基于 SPI 机制,这样我们可以直接跨模块查找到想要的接口实现类,避免不必要的模块间依赖,降低模块之间的耦合性。

优点:

  • 简化 Java SPI 使用:不需要手动创建和维护 META-INF/services 目录下的配置文件,只需要在对应的实现类上添加 @AutoService 注解即可。
  • 提高代码可读性:通过注解明确标记出哪些类是服务的提供者,使得代码更易于理解。
  • 提高开发效率:自动化的过程减少了人为错误,提高了开发效率。

缺点:

  • 依赖注解处理:AutoService 会在构建时创建需要的依赖配置,所以会增加项目编译时间。
  • 局限性:AutoService 只能用于生成 Java SPI 的配置文件,对于其他类型的文件生成没有支持。

2、SPI 简介

SPI 全称是(Service Provider Interface),简单理解就是服务提供接口者。将接口的实现类的全限定名配置在文件中(文件名是接口的全限定名),由服务加载器读取配置文件,并加载实现类,实现了运行时动态的将接口替换对应的实现类。

二、AutoService 使用

大家看完上述对 AutoServiceSPI 描述之后,是不是处在云里雾里的状态,嘴里嘀咕楼主讲的啥玩意。我第一次接触 AutoService 也是比较懵逼的状态,看文档根本没办法理解。所以,实践才是唯一的真理,接下来我会举例说明,并对其原理进行剖析。请大家跟着我的思路往下走,如有不对或者疑惑的地方,欢迎交流!

1、业务需求

现在有这种需求,library 需要读取 app 的配置参数来完成具体业务。我们知道 library 是无法直接调用 app 的类和方法,所以,并不能通过常规方式来实现。这时轮到 AutoService 出场了,我们可以借助它的能力来实现这种业务场景。

2、功能实现

1)app 模块 build.gradle

在项目中 app 模块的 build.gradle 进行集成。

implementation 'com.google.auto.service:auto-service-annotations:1.0.1'
annotationProcessor 'com.google.auto.service:auto-service:1.0.1'

2)创建 library

library 模块创建 IBuildConfig 类,定义 config 方法。

public interface IBuildConfig {
    String config();
}

3)创建 BuildConfigImp

app 模块创建接口实现类 BuildConfigImp,需要注意的是实现类要添加 @AutoService 注解,不然是无法动态生成代码的。

@AutoService(IBuildConfig.class)
public class BuildConfigImpl implements IBuildConfig {
    @Override
    public String config() {
        return "debug mode";
    }
}

4)library 功能调用

library 调用 ServiceLoader.load 方法,拿到 app 的参数,实现了业务功能。

public class InitManager {

    public void init() {
        ServiceLoader<IBuildConfig> load = ServiceLoader.load(IBuildConfig.class);
        for (IBuildConfig config : load) {
            String cf = config.config();
            Log.d("InitManager", "config ===> " + cf);
        }
    }
}

上面的简单示例向大家展示了 AutoService 是如何使用,其实在实际业务场景中 AutoService 有着更广泛的应用。包括插件化开发、模块化开发、跨模块通信,AutoService 都可以胜任。作为一种组件化实现方案,它主要用于实现服务接口的自动发现和加载,降低模块间的耦合性,提高应用的灵活性和可扩展性。

3、项目中遇到的问题

在实际项目使用中,当我调用 ServiceLoader.load 拿到 ServiceLoader 去迭代遍历时,发现返回的数据是空的,也就是 ServiceLoader 里面的数据并没有对应接口实现类,ServiceLoader 迭代器是空的。这下犯了嘀咕,我是按照 demo 的实现方式去应用到实际项目中,为啥项目中却出现了问题。

首先我们看一下 ServiceLoader 提供的两个重载的方法,如下所示:

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

第一个方法需要指定 ClassLoader,第二个方法不需要指定(默认使用ClassLoader.getSystemClassLoader())。

正常使用采用第二个方法就行,可偏偏在实际项目中却遇到了问题。所以为了解决该问题,请大家跟着我来解析一下 AutoService 实现原理。

三、AutoService 实现原理

1、原理分析

当我们调用如下代码,返回一个ServiceLoader对象,让我们看一下load方法做了什么。

    ServiceLoader<IBuildConfig> load = ServiceLoader.load(IBuildConfig.class);

我们看一下 Thread.currentThread().getContextClassLoader() 方法,这个方法是获取当前线程类加载器。拿到当前类加载器,我们能做什么呢?我们都知道 Java 加载类时,通过双亲委派机制。当我们需要加载自定义类时,可以通过这种方式来打破 Java 类加载双亲委派机制。

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

我们接着往下看 load 方法,创建并返回了一个 ServiceLoader 对象。

public static <S> ServiceLoader<S> load(Class<S> service,
                                        ClassLoader loader)
{
    return new ServiceLoader<>(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    // Android-changed: Do not use legacy security code.
    // On Android, System.getSecurityManager() is always null.
    // acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

这里创建 LazyIterator 类,这个类实现了 Iterator,我们继续往下看。

public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

当我们使用迭代器 Iterator 去遍历时,会调用 hasNext 方法。在 Android 源码中,hasNext 的实现其实是通过 hasNextService

public boolean hasNext() {
    // Android-changed: do not use legacy security code
    /* if (acc == null) { */
        return hasNextService();
    /*
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
            public Boolean run() { return hasNextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
    */
}

我们看一下 hasNextService 方法,关键地方做了注释。

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            //固定路径(META-INF/services/)+接口全限定名
            String fullName = PREFIX + service.getName();
            if (loader == null)
                //如果当前类加载为null,则使用父类加载器
                configs = ClassLoader.getSystemResources(fullName);
            else
                //使用当前类加载器,返回路径URL
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        //获取对应接口实现类
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}

我们看一下 parse 方法,主要是在指定 URL 中读取对应接口实现类全限定名称,为后续实例化做准备。

//将给定URL的内容解析为提供程序配置文件
private Iterator<String> parse(Class<?> service, URL u)
    throws ServiceConfigurationError
{
    InputStream in = null;
    BufferedReader r = null;
    ArrayList<String> names = new ArrayList<>();
    try {
        in = u.openStream();
        r = new BufferedReader(new InputStreamReader(in, "utf-8"));
        int lc = 1;
        //一行一行读取实现类全限定名称
        while ((lc = parseLine(service, u, r, lc, names)) >= 0);
    } catch (IOException x) {
        fail(service, "Error reading configuration file", x);
    } finally {
        try {
            if (r != null) r.close();
            if (in != null) in.close();
        } catch (IOException y) {
            fail(service, "Error closing configuration file", y);
        }
    }
    return names.iterator();
}

我们看一下 parseLine 方法,一行一行读取接口实现类的全限定名称。

private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                      List<String> names)
    throws IOException, ServiceConfigurationError
{
    String ln = r.readLine();
    if (ln == null) {
        return -1;
    }
    int ci = ln.indexOf('#');
    if (ci >= 0) ln = ln.substring(0, ci);
    ln = ln.trim();
    int n = ln.length();
    if (n != 0) {
        if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
            fail(service, u, lc, "Illegal configuration-file syntax");
        int cp = ln.codePointAt(0);
        if (!Character.isJavaIdentifierStart(cp))
            fail(service, u, lc, "Illegal provider-class name: " + ln);
        for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
            cp = ln.codePointAt(i);
            if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
        }
        if (!providers.containsKey(ln) && !names.contains(ln))
            names.add(ln);
    }
    return lc + 1;
}

我们接着看 nextService 方法,主要是通过类加载方式,实例化接口对应的实现类。

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        //创建接口实现类class对象,通过类加载方式
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
             // Android-changed: Let the ServiceConfigurationError have a cause.
             "Provider " + cn + " not found", x);
             // "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        // Android-changed: Let the ServiceConfigurationError have a cause.
        ClassCastException cce = new ClassCastException(
                service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
        fail(service,
             "Provider " + cn  + " not a subtype", cce);
        // fail(service,
        //        "Provider " + cn  + " not a subtype");
    }
    try {
        //通过类加载实例化出对象
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

通过上述源码,我们知道 AutoService 实现原理,主要就是去加载 META-INF/services/ 路径下的接口全限定名称的文件。然后通过 IO 读取文件,找到实现类的类路径。然后通过类加载方式将其实例化,供使用者调用。

2、解决问题

其实看完源码之后,问题的原因已经找到了。在 hasNextService() 方法中,如下所示:

String fullName = PREFIX + service.getName();
if (loader == null)
    configs = ClassLoader.getSystemResources(fullName);
else
    configs = loader.getResources(fullName);

当我们不指定 ClassLoader 时,会使用系统默认的 ClassLoader。我们知道最终生成的文件是放置在 META-INF/services/ 目录下,我们可以看一下打包的 apk,如下图所示:

image.png

回过头来分析项目中的问题,当我们使用 AutoService 组件生成的配置文件,是属于应用内部文件。如果不指定对应的 ClassLoader ,就会使用系统默认的,当然是无法找到的。所以,要解决项目中的问题,我们需要指定当前类的 ClassLoader,通过 getClass().getClassLoader() 方式。

参考

www.jb51.net/article/201…