Android SPI Study Notes

  • 2021-12-11 09:10:26
  • OfStack

Catalog Overview Basic use 1. Declare service 2 in lower module_common. Implement service 3 in upper module. Use service ServiceLoader. load in other upper module An ServiceLoader instance creates an LazyIterator Summarize Advantages of SPI Disadvantages of SPI

Overview

SPI (Service Provider Interface, service provider interface), service usually refers to an interface or an abstract class, the service provider is the concrete implementation of this interface or abstract class, and the third party implements the interface to provide concrete services. By decoupling the service and its specific implementation class, the scalability of the program is greatly enhanced, even pluggable. Based on service registration and discovery mechanism, service providers register services with the system, and service consumers can achieve the separation of service provision and use by searching and discovering services.

SPI can be applied to Android componentization, and SPI is rarely used directly, but its functions can be extended and its use steps can be simplified based on it.

Basic use

1. Declare the service in the lower module_common


public interface IPrinter {
  void print();
}

2. Implement services in the upper module


// module_a -- implementation project(':module_common')
// com.hearing.modulea.APrinter
public class APrinter implements IPrinter {
  @Override
  public void print() {
    Log.d("LLL", "APrinter");
  }
}
// src/main/resources/META-INF/services/com.hearing.common.IPrinter
//  You can configure multiple implementation classes 
com.hearing.modulea.APrinter

// ----------------------------------------------------------------//

// module_b -- implementation project(':module_common')
// com.hearing.moduleb.BPrinter
public class BPrinter implements IPrinter {
  @Override
  public void print() {
    Log.d("LLL", "BPrinter");
  }
}
// src/main/resources/META-INF/services/com.hearing.common.IPrinter
com.hearing.moduleb.BPrinter

3. Using services in other upper layer module


// implementation project(':module_common')
ServiceLoader<IPrinter> printers = ServiceLoader.load(IPrinter.class);
for (IPrinter printer : printers) {
  printer.print();
}

ServiceLoader.load

The principle analysis of ServiceLoader begins with load method:


public static <S> ServiceLoader<S> load(Class<S> service) {
  //  Gets the class loader for the current thread 
  ClassLoader cl = Thread.currentThread().getContextClassLoader();
  return ServiceLoader.load(service, cl);
}

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

ServiceLoader instance creation


private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

private ServiceLoader(Class<S> svc, ClassLoader cl) {
  service = Objects.requireNonNull(svc, "Service interface cannot be null");
  loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
  reload();
}

// Clear this loader's provider cache so that all providers will be reloaded.
public void reload() {
  providers.clear();
  //  Created the 1 Lazy iterator 
  lookupIterator = new LazyIterator(service, loader);
}

LazyIterator

ServiceLoader implements the Iterable interface, and you can iterate through elements using the iterator/forEach method, whose iterator method is implemented as follows:


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  If it already exists in the cache, it will return directly, otherwise it will load 
      if (knownProviders.hasNext()) return knownProviders.next().getValue();
      return lookupIterator.next();
    }

    public void remove() {
      throw new UnsupportedOperationException();
    }
  };
}

The lazy loading method is used above, and all service implementations are loaded at the beginning of 1, otherwise reflection will affect performance. The LazyIterator classes are as follows:


private static final String PREFIX = "META-INF/services/";

private class LazyIterator implements Iterator<S> {
  Class<S> service;
  ClassLoader loader;
  Enumeration<URL> configs = null;
  Iterator<String> pending = null;
  String nextName = null;

  private LazyIterator(Class<S> service, ClassLoader loader) {
    this.service = service;
    this.loader = loader;
  }

  private boolean hasNextService() {
    if (nextName != null) {
      return true;
    }
    if (configs == null) {
      try {
        //  Get the service profile 
        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;
      }
      //  Resolving Service Configuration 
      pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
  }

  private S nextService() {
    if (!hasNextService()) throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
      //  Reflection loads the specified service through the class loader 
      c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
      // throw Exception
    }
    if (!service.isAssignableFrom(c)) {
      // throw Exception
    }
    try {
      S p = service.cast(c.newInstance());
      providers.put(cn, p);
      return p;
    } catch (Throwable x) {
      // throw Exception
    }
    throw new Error();   // This cannot happen
  }

  public boolean hasNext() {
    return hasNextService();
  }

  public S next() {
    return nextService();
  }

  public void remove() {
    throw new UnsupportedOperationException();
  }
}

Summarize

The principle of ServiceLoader is relatively simple. In fact, it uses a lazy iterator, which can reduce performance loss by loading new services. When loading new services, the configured services are obtained by parsing the service configuration file, then the configured service implementation classes are loaded by the class loader, and finally their examples are returned.

Advantages of SPI

Only the service interface is provided, and the specific service is implemented by other components, and the interface is separated from the specific implementation.

Disadvantages of SPI

The configuration is too cumbersome Instantiation of specific services is completed by ServiceLoader reflection, and the life cycle is uncontrollable When there are multiple implementation class objects, ServiceLoader only provides one Iterator, so it is impossible to get the specific implementation class objects accurately Need to read and parse configuration file, performance loss

These are the details of the Android-SPI study notes. For more information about Android-SPI, please pay attention to other related articles on this site!


Related articles: