[Android元件化]- SPI載入

Cang_Wang發表於2019-03-04

大家好,我係蒼王。
這個系列已經出到了第30章節了,已經開通了已經有一年半的時間了。
在一年半里,建立了千人的QQ大群,不少編輯也找過我編輯圖書,也有同行找過我合作出公眾號。但是個人的時間是有限的,並不可能全部願望都實現。
那麼上一年就選了一件對這輩子非常有意義的事情,和電子工業出版社出版一本關於元件化技術的書。非常感謝陳曉猛編輯找到了我一同出書,也感謝在技術群中不斷深討元件化技術的群友們。
書中重點介紹了使用元件化思想去搭建一個Android專案,介紹了元件化的思想,元件化的程式設計技術,多人管理元件化,元件化的編譯優化,以及對專案演進的思想感悟。
此書並不是只是介紹技術,也包含了我對一些生活的理解,技術思維的理解。
京東淘寶噹噹均可以購買,有興趣可以點選連結就可以跳轉了。

[Android元件化]- SPI載入
Android元件化架構

以下是我這個系列的相關文章,有興趣可以參考一下,可以給個喜歡或者關注我的文章。

[Android]如何做一個崩潰率少於千分之三噶應用app--章節列表

關於spi,其全名是Service Provider Interfaces。ServiceLoder用於動態載入介面實現類的載入器。
1.其可以動態載入一些繼承某個介面的實體類
2.需要將實體類名宣告到resources/META-INF/services目錄,可以取巧使用@AutoService
3.其載入的並不是單例,而且構造方法不帶任何引數,因為ServiceLoader底層是使用了反射的機制來載入。
4.載入檔案順序應該是按照resources/META-INF/services目錄中順序載入,所以如果使用@AutoService是不可控的。
5.ServiceLoader繼承iterator介面,可以像List一樣遍歷實體類。
6.其實際也是通過反射來實現初始化操作,使用介面的方式使模組,ServiceLoader裝載器、啟動器之間更加解耦。
7.比較適合於元件化中,模組入口初始化的統一載入場景。

[Android元件化]- SPI載入
SPI原理圖

以下借用一個Modular框架中的載入為例
1.宣告介面

public interface IModule {
    /**
     * 模組初始化,只有組建時才呼叫,用於開啟子執行緒輪訓訊息
     */
    void init();

    /**
     * 模組ID
     *
     * @return 模組ID
     */
    int getModuleId();

    /**
     * 模組註冊並連線成功後,可以做以下事情:
     * <p>
     * 1、註冊監聽事件
     * 2、傳送事件
     * 3、註冊服務
     * 4、呼叫服務
     */
    void afterConnected();
}
複製程式碼

2.使用@AutoService,將全路徑名寫到resources/META-INF/services目錄

@AutoService(IModule.class)
public class Module extends BaseModule {
    @Override
    public void afterConnected() {


    }

    @Override
    public int getModuleId() {
        return Constants.MODULE_B;
    }
}
複製程式碼

3.使用ServiceLoder載入模組

@Override//只有當是組建單獨執行時,才當Application執行,才會走onCreate,最終打包時根本沒有這個類
    public void onCreate() {
        super.onCreate();
        ……

        //自動註冊伺服器(如果是獨立模組內宣告只有一個IModule)
        ServiceLoader<IModule> modules = ServiceLoader.load(IModule.class);
        mBaseModule = (BaseModule) modules.iterator().next();

        //模組初始化
        mBaseModule.init();
        ……
    }
複製程式碼
public void onCreate() {
        super.onCreate();
        ……

        //SPI自動註冊服務(主module裝載的時候,已經將全部META_INF檔案合併)
        ServiceLoader<IModule> modules = ServiceLoader.load(IModule.class);
        for (IModule module : modules) module.afterConnected();
    }
複製程式碼

使用看起來非常簡單,我們研究一下ServiceLoader原始碼的特別之處。

    //呼叫靜態load方法來初始化XXXInterface介面資訊。
    public static <S> ServiceLoader<S> load(Class<S> service) {
        //獲取當前執行緒ClassLoader
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

    //構建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");
        //檢測classloader是否為空,為空使用系統classloader載入器
        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();
    }
   
    public void reload() {
        //清理provides配置載入器
        providers.clear();
        //初始化懶載入迭代器
        lookupIterator = new LazyIterator(service, loader);
    }
複製程式碼

可以看到使用的是懶載入的迭代器,只有迭代器被使用的時候,才會真正初始化每一個繼承介面的實體類。

   //判斷是否有下一個物件
   private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    //PREFIX = "META-INF/services/"
                    //載入配置地址
                    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;
                }
                //解析config的節點
                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 {
                //通過類路徑名,載入類資訊
                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
        }
複製程式碼

ServiceLoader實際還是通過路徑名反射來完成,只是其通過配置到META_INF中的目錄檔案來完成解耦。
ServiceLoader使用場景是用於不需要區分module載入順序的情況,如果有載入順序,還需要重新排序後再初始化方法,這裡最後還是使用優先順序機制。

在開編的時候已經介紹了SPI的優點和侷限性,跳出SPI,依然能做一個更靈活更可控的載入機制,例如json指令碼,xml指令碼動態更新。


相關文章