Android 動(dòng)態(tài)服務(wù)SPI--模塊節(jié)藕

一、什么是SPI

SPI即Service Provider Interfaces。Java的接口可以有多種實(shí)現(xiàn)方式,為便于代碼靈活,有時(shí)需要?jiǎng)討B(tài)加載實(shí)現(xiàn)類。這就是SPI機(jī)制. SPI機(jī)制非常簡單, 步驟如下:

  1. 定義接口和接口的實(shí)現(xiàn)類
  2. 創(chuàng)建resources/META-INF/services目錄
  3. 在上述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

  1. 在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()方法。

  1. 首先拿到配置文件名fullName,我們這個(gè)例子中是com.example.Display
  2. 通過類加載器獲得所有模塊的配置文件Enumeration<URL> configs configs
  3. 依次掃描每個(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():

  1. 首先根據(jù)nextName,Class.forName加載拿到具體實(shí)現(xiàn)類的class對(duì)象
  2. Class.newInstance()實(shí)例化拿到具體實(shí)現(xiàn)類的實(shí)例對(duì)象
  3. 將實(shí)例對(duì)象轉(zhuǎn)換service.cast為接口
  4. 將實(shí)例對(duì)象放到緩存中,providers.put(cn, p),key就是實(shí)現(xiàn)類的全限定名,value是實(shí)例對(duì)象。
  5. 返回實(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鏈接

參考博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容