一、什么是SPI
SPI即Service Provider Interfaces。Java的接口可以有多種實(shí)現(xiàn)方式,為便于代碼靈活,有時(shí)需要?jiǎng)討B(tài)加載實(shí)現(xiàn)類。這就是SPI機(jī)制. SPI機(jī)制非常簡單, 步驟如下:
- 定義接口和接口的實(shí)現(xiàn)類
- 創(chuàng)建resources/META-INF/services目錄
- 在上述Service目錄下,創(chuàng)建一個(gè)以接口名(類的全名) 命名的文件, 其內(nèi)容是實(shí)現(xiàn)類的類名 (類的全名)。
在services目錄下創(chuàng)建的文件是androidtest.project.com.aninterface.AppInit 文件中的內(nèi)容為AAppInit接口的實(shí)現(xiàn)類, 可能是androidtest.project.com.sharemodule.ShareApplicationInit
- 在java代碼中使用ServcieLoader來動(dòng)態(tài)加載并調(diào)用內(nèi)部方法.
二、在Android中能干嘛?
隨著自己開發(fā)的應(yīng)用的版本迭代,新功能不斷增多,隨之引入的第三方庫也不可避免地多了起來,你可能就會(huì)發(fā)現(xiàn)自己應(yīng)用Application中各種框架的初始化代碼也在逐漸臃腫起來:什么推送啦,分享啦,統(tǒng)計(jì)啦,定位啦...另外還有你自己封裝的一些工具和框架。這些七七八八加起來,可能最終你的Application會(huì)變成這樣:
/**
* @author liuboyu E-mail:545777678@qq.com
* @Date 2019-08-24
* @Description 自定義 Application
*/
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
//初始化推送
PushAgent mPushAgent = PushAgent.getInstance(this);
mPushAgent.register(new IUmengRegisterCallback() {
@Override
public void onSuccess(String deviceToken) {
//注冊(cè)成功會(huì)返回device token
}
@Override
public void onFailure(String s, String s1) {
}
});
//初始化統(tǒng)計(jì)
UMConfigure.init(this, "xxxxxxxxxxxxxx", "umeng", UMConfigure.DEVICE_TYPE_PHONE, "");
//初始化分享
PlatformConfig.setWeixin("xxxxxxxxxxxxxx", "xxxxxxxxxxxxxx");
PlatformConfig.setSinaWeibo("xxxxxxxxxxxxxx", "xxxxxxxxxxxxxx", "http://sns.whalecloud.com");
PlatformConfig.setYixin("xxxxxxxxxxxxxx");
PlatformConfig.setQQZone("xxxxxxxxxxxxxx", "xxxxxxxxxxxxxx");
PlatformConfig.setTwitter("xxxxxxxxxxxxxx", "xxxxxxxxxxxxxx");
//初始化定位
LocationClient mLocationClient = new LocationClient(context);
mLocationClient.setLocOption(getLocationOption());
mLocationClient.registerLocationListener(new MyLocationListener());
mLocationClient.start();
mLocationClient.requestLocation();
//初始化glide
DisplayOption options = DisplayOption.builder().setDefaultResId(R.drawable.ic_default)
.setErrorResId(-1).setLoadingResId(-1);
imageDisplayLoader.setDefaultDisplayOption(options);
//初始化自己的一些工具
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
});
}
}
從個(gè)人開發(fā)經(jīng)驗(yàn)上說,這些應(yīng)用程序級(jí)別的框架,作用的時(shí)間貫穿APP的整個(gè)生命周期,所以都會(huì)要求你在一開始的時(shí)候就進(jìn)行初始化
二、優(yōu)化它,盤它!
SPI在平時(shí)我們用到的會(huì)比較少,但是在Android模塊開發(fā)中就會(huì)比較有用,不同的模塊可以基于接口編程,每個(gè)模塊有不同的實(shí)現(xiàn)service provider,然后通過SPI機(jī)制自動(dòng)注冊(cè)到一個(gè)配置文件中,就可以實(shí)現(xiàn)在程序運(yùn)行時(shí)掃描加載同一接口的不同service provider。這樣模塊之間不會(huì)基于實(shí)現(xiàn)類硬編碼,可插拔。
我們?cè)囍ㄟ^ SPI 的方式解決這個(gè)問題
創(chuàng)建項(xiàng)目
- 創(chuàng)建Android Module命名為app 依賴 spi_library,shareModule,pushModule
- 創(chuàng)建Android library Module 命名為 interface
- 創(chuàng)建Android library Module 命名為 shareModule 依賴 interface
- 創(chuàng)建Android library Module 命名為 pushModule 依賴 interface
- 創(chuàng)建Android library Module 命名為 spi_library 依賴 interface
Module職責(zé)
- interface:接口模塊
- spi_library:spi 工具類,一鍵初始化
- shareModule:share模塊
- pushModule:push模塊
實(shí)現(xiàn)
1、創(chuàng)建接口
/**
* @author liuboyu E-mail:545777678@qq.com
* @Date 2019-08-24
* @Description Application 初始化接口
*/
public interface AppInit {
void applicationInit(@NonNull Application application);
}
2、子模塊實(shí)現(xiàn)自己的功能,并在自己模塊聲明自己的配置文件
share 模塊
/**
* @author liuboyu E-mail:545777678@qq.com
* @Date 2019-08-24
* @Description share模塊初始化
*/
public class ShareApplicationInit implements AppInit {
@Override
public void applicationInit(@NonNull Application application) {
Log.e("spi_study", "share模塊初始化");
}
}
配置文件內(nèi)容
androidtest.project.com.sharemodule.ShareApplicationInit
push 模塊
/**
* @author liuboyu E-mail:545777678@qq.com
* @Date 2019-08-24
* @Description push模塊初始化操作
*/
public class PushApplicationInit implements AppInit {
@Override
public void applicationInit(@NonNull Application application) {
Log.e("spi_study", "push模塊初始化");
}
}
配置文件內(nèi)容
androidtest.project.com.pushmodule.PushApplicationInit
3、通過ServiceLoader來加載接口
關(guān)鍵的代碼其實(shí)就是下面幾行,通過ServiceLoader來加載接口的不同實(shí)現(xiàn)類,然后會(huì)得到迭代器,在迭代器中可以拿到不同實(shí)現(xiàn)類全限定名,然后通過反射動(dòng)態(tài)加載實(shí)例就可以調(diào)用applicationInit方法了。
/**
* 初始化各個(gè)moduel的Application
* @param application
*/
public void initAllModuelApplication(@NonNull Application application) {
ServiceLoader<AppInit> loader = ServiceLoader.load(AppInit.class);
Iterator<AppInit> mIterator = loader.iterator();
while (mIterator.hasNext()) {
mIterator.next().applicationInit(application);
}
}
4、項(xiàng)目初始化
我們僅僅通過一行代碼便可實(shí)現(xiàn)各個(gè)模塊的初始化操作,如果后續(xù)引入其他的模塊,比如bugly啊,統(tǒng)計(jì)啊,定位啊,直接新建子模塊初始化自己的application,并聲明配置文件即可,對(duì)于主工程完全無感址,
也就達(dá)到了 模塊之間不會(huì)基于實(shí)現(xiàn)類硬編碼,可插拔 的目的。
/**
* @author liuboyu E-mail:545777678@qq.com
* @Date 2019-08-24
* @Description 自定義 Application
*/
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
AppInitTools.getInstance().initAllModuelApplication(this);
}
}
看一下運(yùn)行結(jié)果
08-24 16:30:51.481 1387-1387/androidtest.project.com.spi_study E/spi_study: share模塊初始化
08-24 16:30:51.482 1387-1387/androidtest.project.com.spi_study E/spi_study: push模塊初始化
三、源碼分析
主要工作都是在ServiceLoader中,這個(gè)類在java.util包中。先看下幾個(gè)重要的成員變量:
- PREFIX就是配置文件所在的包目錄路徑;
- service就是接口名稱,在我們這個(gè)例子中就是AppInit;
- loader就是類加載器,其實(shí)最終都是通過反射加載實(shí)例;
- providers就是不同實(shí)現(xiàn)類的緩存,key就是實(shí)現(xiàn)類的全限定名,value就是實(shí)現(xiàn)類的實(shí)例;
- lookupIterator就是內(nèi)部類LazyIterator的實(shí)例。
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的幾個(gè)關(guān)鍵步驟:
ServiceLoader<AppInit> loader = ServiceLoader.load(AppInit.class);
Iterator<AppInit> mIterator = loader.iterator();
while (mIterator.hasNext()) {
mIterator.next().applicationInit(application);
}
先看下第一個(gè)步驟load:ServiceLoader提供了兩個(gè)靜態(tài)的load方法,如果我們沒有傳入類加載器,ServiceLoader會(huì)自動(dòng)為我們獲得一個(gè)當(dāng)前線程的類加載器,最終都是調(diào)用構(gòu)造函數(shù)。
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);
}
在構(gòu)造函數(shù)中工作很簡單就是清除實(shí)現(xiàn)類的緩存,實(shí)例化迭代器。
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = svc;
loader = cl;
reload();
}
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
注意了,我們?cè)谕饷嫱ㄟ^ServiceLoader.load(Display.class)并不會(huì)去加載service provider,也就是懶加載的設(shè)計(jì)模式,這也是Java SPI的設(shè)計(jì)亮點(diǎn)。
那么service provider在什么地方進(jìn)行加載?我們接著看第二個(gè)步驟loader.iterator(),其實(shí)就是返回一個(gè)迭代器。我們看下官方文檔的解釋,這個(gè)就是懶加載實(shí)現(xiàn)的地方,首先會(huì)到providers中去查找有沒有存在的實(shí)例,有就直接返回,沒有再到LazyIterator中查找。
* Lazily loads the available providers of this loader's service.
*
* <p> The iterator returned by this method first yields all of the
* elements of the provider cache, in instantiation order. It then lazily
* loads and instantiates any remaining providers, adding each one to the
* cache in turn.
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();
}
};
}
我們一開始providers中肯定是沒有緩存的實(shí)例的,接著會(huì)到LazyIterator中查找,去看看LazyIterator,先看下hasNext()方法。
- 首先拿到配置文件名fullName,我們這個(gè)例子中是com.example.Display
- 通過類加載器獲得所有模塊的配置文件Enumeration<URL> configs configs
- 依次掃描每個(gè)配置文件的內(nèi)容,返回配置文件內(nèi)容Iterator<String> pending,每個(gè)配置文件中可能有多個(gè)實(shí)現(xiàn)類的全限定名,所以pending也是個(gè)迭代器。
public boolean hasNext() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
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;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
在上面hasNext()方法中拿到的nextName就是實(shí)現(xiàn)類的全限定名,接下來我們?nèi)タ纯淳唧w實(shí)例化工作的地方next():
- 首先根據(jù)nextName,Class.forName加載拿到具體實(shí)現(xiàn)類的class對(duì)象
- Class.newInstance()實(shí)例化拿到具體實(shí)現(xiàn)類的實(shí)例對(duì)象
- 將實(shí)例對(duì)象轉(zhuǎn)換service.cast為接口
- 將實(shí)例對(duì)象放到緩存中,providers.put(cn, p),key就是實(shí)現(xiàn)類的全限定名,value是實(shí)例對(duì)象。
- 返回實(shí)例對(duì)象
public S next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
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 {
S p = service.cast(c.newInstance());
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
}
四、總結(jié)
通過Java的SPI機(jī)制也有一點(diǎn)缺點(diǎn)就是在運(yùn)行時(shí)通過反射加載類實(shí)例,這個(gè)對(duì)性能會(huì)有點(diǎn)影響。但是瑕不掩瑜,SPI機(jī)制可以實(shí)現(xiàn)不同模塊之間方便的面向接口編程,拒絕了硬編碼的方式,解耦效果很好。用起來也簡單,只需要在目錄META-INF/services中配置實(shí)現(xiàn)類就行。
文中的栗子已上傳github,有需要的可以參考:github demo鏈接
參考博客