Android SPI Study Notes
- 2021-12-11 09:10:26
- OfStack
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 lossThese 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!