Spa框架 -- Android架構優化利器

1 背景

在組件化的模式設計裏,模塊之間基於接口編程,模塊內不對實現類進行硬編碼。由於一旦代碼裏涉及具體的實現類,就違反了可拔插的原則,當須要替換一種實現,就須要修改代碼。爲了實如今模塊裝配的時候能不在程序裏動態指明,這就須要一種服務發現機制。 SPI就是這樣的一個機制:爲某個接口尋找服務實現的機制。有點相似IOC的思想,就是將裝配的控制權移到程序以外,在模塊化設計中這個機制尤爲重要。html

2 業界技術方案

2.1 常規模塊依賴方式

組件開發過程當中,若是想要在模塊B中實例化模塊A中的類或使用模塊A中的方法,常規的方式是讓模塊B依賴模塊A,以下圖所示:java

                             

可是組件化開發過程當中,每每並不但願某些模塊之間有直接的依賴關係,由於若是依賴關係創建了,模塊之間的耦合性就高了。git

2.2 Java的SPI機制

有沒有什麼辦法能夠解決這個問題呢?可使用Java提供的SPI機制!github

SPI(Service Provider Interface)是JDK內置的一種服務發現機制。它的應用仍是很是普遍的,尤爲在服務端開發技術棧中,編程

  • JDBC 中經過 SPI 的方式加載不一樣的驅動實現windows

  • SLF4J中經過 SPI 的方式加載不一樣提供商的日誌實現類緩存

  • Gradle源碼中有大量的服務是基於 SPI機制來作服務實現擴展的安全

  • SpringFactoriesLoader 是 Spring 中十分重要的一個擴展機制之一,算是SPI的一個變種,原理基本一致服務器

2.3 SPI是如何解決上述問題的呢?

Java的SPI機制使用流程如上圖所示,markdown

  • 首先須要一個Base模塊提供IA接口,模塊A和模塊B都依賴Base模塊
  • 模塊A中的類A實現IA接口
  • SourceSet下建立resources/META-INF/services父目錄,
  • 在父目錄下建立以IA接口的全限定名爲文件名的文本文件,文件內容是IA實現類的全限定名列表,以回車換行分割
  • 最後模塊B就能夠經過ServiceLoader.load(IA.class)方法來建立模塊A中類A對象了

 2.4 Java原生SPI機制的不足

若是你熟悉SPI機制那麼你會發現,不管是在JDBC, SLF4J,Gradle仍是Spring中,一個模塊每每只是提供個別關鍵接口做爲一個服務的切入點讓外部模塊發現,這一般是足夠的。

但若是有大量的服務須要被發現,那麼就要在resources/META-INF/services目錄寫不少的接口文件,而後使用ServiceLoader去加載。

  • 問題一:寫法太繁瑣,接口多了不易維護, 能不能簡化?
  • 問題二:resources/META-INF/services下配置的接口文件是個配置文件, ServiceLoader經過文件流讀出接口實現類的全限定名,再經過反射實例化出具體的實現類對象, 性能較低,並且服務越多性能越低。
  • 問題三:  它只提供了服務發現的能力。ServiceLoader只負責把服務實例化出來。沒有對實例化的對象作任何管理。

3. 簡單易用的SPI機制 -- spa       

3.1 spa服務發現機制                   

github連接: github.com/luqinx/sp

spa(Service Pool for Android)將待實例化的類當作一個一個的服務, 是基於Java SPI思想基礎之上建立出來的全新的SPI機制,可是他不只僅只有服務發現能力,還有服務生命週期管理,服務優先級管理,服務攔截管理等等Java SPI以外的能力,它生於Android,但不只僅只試用於Android端,理論上Jvm環境下都適用。

                          

spa的基本思想如上圖所示:

  1. spa使用註解替代了繁瑣的services文件配置, 使用方式獲得了極大的簡化
  2. spa在編譯階段經過字節碼生成爲被@Service註解標記的接口實現類建立工廠方法,實現類對象經過工廠方法建立,不存在文件流操做也不須要反射實例化對象,提升了性能。
  3. 無需讀配置,無需緩存映射表,所以spa甚至作到了無需手動初始化。

其餘相似的框架廣泛須要在運行時讀配置文件(io),緩存配置映射表和反射建立對象,這些都會對性能形成必定影響。而spa並不須要,spa在編譯階段生成工廠類來替代配置文件和和緩存映射,這很容易理解。不使用反射,spa是如何建立服務對象的呢?

其實很簡單,模塊隔離只是一個模式設計, 是爲了在開發過程當中不讓不相關的模塊相互之間存在引用關係,從而下降模塊之間的耦合性。模塊編譯後代碼最終會變成字節碼/jar/aar/dex, 而字節碼/jar/aar/dex裏並無模塊的概念。通俗的說A類中建立B類對象時,類A並不關心也並不知道類B是寫在哪一個模塊的。

這也是爲何spa是使用字節碼生成而不是apt代碼生成的緣由。

到這裏, spa已解決了跨模塊無直接依賴狀況下實例化對象的問題。

                                     就這?就這?就這?就這?就這?表情包圖片- 求表情網,鬥圖今後不求人!

看到這你可能發現,主流的路由組件ARouter也有相似的能力,爲何不直接使用ARouter呢?只是由於spa性能更好一點? 

跨模塊實例化對象是spa的核心能力,但遠遠不是所有,好比spa在何時建立服務對象,一個服務被建立後何時會被gc回收?

3.2 spa服務的生命週期

能夠對比一下ARouter, 使用過ARouter的同窗應該知道,ARouter經過實現IProvider接口定義一個服務,這個服務建立後是全局生命週期的,且全局惟一至關於一個單例。

這在一些場景下是很是有用的,好比我須要一個全局存儲服務StorageService,提供save, get, delete等存儲相關能力,我能夠任什麼時候候、任意模塊下經過StorageService接口獲取到的這個單例服務並使用它,很是方便。

但ARouter只能建立全局生命週期的服務,這是不夠的,好比多個業務模塊都須要一個品類Fragment並命名爲CategoryFragment, 爲了模塊解耦,我須要將CategoryFragment當作一個服務 ,且須要根據不一樣的品類id建立多個CategoryFragment。ARouter的服務是沒辦法建立多個的,且由於是單例Fragment沒法被回收會形成內存泄漏。

spa能夠經過@Service註解的scope字段定義一個服務的生命週期,經常使用的生命週期就是上述的兩種,

StorageService僞代碼以下:

// 全局生命週期,全局惟一
@Service(scope = Spa.Global)
public class StorageServiceImpl implements StorageService {
    ...
}
複製代碼

CategoryFragment僞代碼以下:

// 默認生命週期,每次都會建立一個新的CategoryFragment,
@Service
public class CategoryFragment implements CategoryService {
    ...
}
複製代碼

獲取服務的僞代碼以下

....

StorageService storageService = Spa.getService(StorageService.class); 

CategoryService categoryFragment = Spa.getService(CategoryService.class);

....
複製代碼

除了以上兩種經常使用生命週期,還有被弱引用、軟引用持有的生命週期和自定義生命週期管理。

3.3 spa服務優先級管理

當一個服務有多個實現類,那Spa.getService(xx.class)會獲取哪個呢?所以Spa引入服務優先級管理。

3.3.1  爲何須要優先級管理?

當你的想寫一個基礎服務,而這個服務須要在不一樣的環境條件下執行不一樣的行爲,有什麼好的辦法呢?(不一樣環境能夠指不一樣的編譯環境buildType, 也能夠是不一樣的運行環境(Java or Android, windows or Mac), 也能夠是不一樣的業務場景, 甚至能夠是不一樣的項目等等)

舉個栗子卡通圖片(第1頁) - 要無憂健康圖庫

個人項目中內部環境和生產環境的代碼是嚴格分離的,內部環境相關的代碼好比日誌,調試工具,分析工具,數據Mock等是絕對不會打到生成環境的安裝包中的。這保證了生成環境的安全性和性能。

以日誌輸出爲例, 

public interface LogService implements IService {
    void e(String tag, String message);
    ....
}
複製代碼

內部環境中, 須要將日誌輸出到控制檯,方便發現問題, 由於內部環境代碼和生產環境是隔離的,能夠將優先級設置高一點。則當它存在時會優先被實例化

@Service(scope = Spa.global, priority = 100)
public class AlphaLogService implements LogService {
    
    void e(String tag, String message) {
        ...
        Log.e(tag, message);
    }

    ....
}
複製代碼

線上生產環境,則不能輸出到控制檯, 而是根據必定策略上傳到日誌平臺,由於代碼環境隔離生產環境並不存在AlphaLogService,因此雖然ProductLogService是低優先級,但它依然會被實例化

@Service(scope = Spa.global, priority = 10)
public class ProductLogService implements LogService {
    void e(String tag, String message) {        ...
        RemoteLog.e(tag, message);
    }

    ....
}
複製代碼

有人可能會以爲: 簡單的if-else就能解決的問題你爲何要搞得這麼複雜???

是的, if-else能解決不一樣環境使用不一樣的日誌輸出,可是if-else很難在環境隔離的前提下拿到不一樣環境的LogService的實現類

3.3.2 spa也能夠同時獲取多個服務實現

ServiceLoader的load方法返回的ServiceLoader對象是一個Iterator迭代器,迭代器內容就是服務接口的實現列表。Spa有實現這一個功能嗎?直接上代碼

假設攔截器服務接口Interceptor,有A, B, C三個攔截器實現

// 服務接口
public interface Interceptor extends IService{
    void intercept();
    String interceptorName();
}

// 攔截器A
@Service(priority = 10)
public class AInterceptor implements Interceptor {

    void intercept() {
        System.out.println("interceptor A is running...");
    }    

    public String interceptorName() {
        return "A";
    }
}

// 攔截器B
@Service(priority = 30)
public class BInterceptor implements Interceptor {

    void intercept() {
        System.out.println("interceptor B is running...")
    }

    public String interceptorName() {
        return "B";
    }
}

// 攔截器C
@Service(priority = 20)
public class CInterceptor implements Interceptor {

    void intercept() {
        System.out.println("interceptor C is running...")
    }

    public String interceptorName() {
        return "C";
    }
}
複製代碼

Spa使用CombineService來組合多個服務接口實現,CombineService一樣也是一個Iterator迭代器,迭代器的返回順序將按服務器優先級值大小依次返回

CombineService<Interceptor> as = Spa.getCombineService(Interceptor.class);
for (Interceptor interceptor: interceptors) {    
    System.out.print(interceptor.interceptorName());
}

// 輸出 BCA
複製代碼

Spa.getCombineService(Interceptor.class)返回的對象也同時是一個Interceptor代理, 當代理對象的intercept()方法執行時,將按優先級順序依次執行每一個服務實現的intercept()方法

Interceptor interceptor = Spa.getCombineService(Interceptor.class);
interceptor.intercept();

// 輸出
// interceptor B is running...
// interceptor C is running...
// interceptor A is running...
複製代碼

CombineService默認策略是按優先級大小來決定多服務的執行順序,上面示例中,多個Interceptor服務對象的intercept()方法的調用大致流程以下圖:

ComineService也能夠經過實現CombineStrategy接口來支持自定義執行策略。

1. 定義自定義多服務執行策略

public class InterceptorStrategy implements CombineStrategy {
    @Override    
    public boolean filter(Class serviceClass, Method method, Object[] args) {    
        return Interceptor.class.isAssignableFrom(serviceClass); // 選擇策略對應的接口
    }

    @Override
    public Object invoke(final List<ServiceProxy> proxies, Class serviceClass, final Method method, final Object[] args) {
        // 自定義調用過程
    }

}
複製代碼

2. 使用自定義對服務執行策略

Interceptor interceptor = Spa.getCombineService(Interceptor.class, InterceptorStrategy);
interceptor.intercept()
複製代碼

自定義多服務執行策略是很是有用的,在spa的內部就有多處應用

  • 場景1: SpRouter中路由攔截策略,業務能夠經過實現RouteInterceptor接口,調用onContinue或onInterrapt()來決定是繼續路由仍是攔截路由,實現類是RouteCombineStrategy。每一個路由框架通常都有路由攔截能力,方式大同小異,這裏就再也不贅述,感興趣能夠自行查看實現方式。
  • 場景2: 自定義生命週期的類型檢查策略,實現類是CustomCombineStrategy,感興趣能夠自行查看。
  • 場景3: spa服務攔截的攔截策略,業務能夠經過實現IServiceInterceptor接口,調用onContinue或onInterrapt()來決定是繼續執行方法仍是攔截掉方法不執行,又或者是換一個方法執行等等。服務攔截是spa不可或缺的一部分。

3.4 spa服務攔截

spa定義的服務默認支持服務攔截能力,經過攔截能力能夠實現服務的AOP操做,想要攔截spa的服務也很簡單隻須要實現IServiceInterceptor接口, 且服務攔截器也被當作服務,因此須要使用@Service註解標記

@Service
public class MinPriorityServiceInterceptor implements IServiceInterceptor {    
    @Override    
    public void intercept(Class<? extends IService> originClass, IService source, Method method, Object[] args, IServiceInterceptorCallback callback) {
        logger.log(source.toString() + ": " + method.getName());
        if (method.getReturnType() == int.class) {
            callback.onInterrupt(100); // 攔截方法,並返回100
        } else {
            callback.onContinue(method, args); // 不攔截,繼續執行
        }
    }
}
複製代碼

能夠將@Service的註解參數disableIntercept設置爲true來禁用服務攔截。

3.5 服務別名

上面的介紹都是經過類來查找服務,Spa同時還支持給類設置別名,而後經過別名來查找服務。別名用@Service註解的path(爲何不是alias? 歷史緣由)參數標識。

@Service(path = "firstAlias", scope = Spa.Scope.Global)
public class MyAliasService implements IService {
	....	
}

// 使用
MyAliasService byPath = Spa.getService("firstAlias");
// spa經過接口/抽象類查找它的實現類/子類對應的服務用getService(IXxx.class)
// spa經過指定具體的服務實現類建立服務對象使用getFixedService(Xxx.class)
MyAliasService byClass = Spa.getFixedService(MyAliasService.class); 
assert byPath == byClass; 
複製代碼

Spa的核心是經過類來查找服務,別名查找也是在類查找的基礎上作了一層映射: spa在編譯階段生成了PathServicesInstance類,它維護着一張別名(path)到服務類的映射表。





4. 總結

Spa是目前最完備的SPI開源方案,雖然大廠們開源的如ARouter, DRouter, WMRouter等路由方案都有相似的SPI能力,但它們的立足點都是解決Android框架下的路由問題,不是純粹的SPI方案。而Spa的目標是跨模塊建立服務和管理服務,它不關心上層的服務具體是什麼,因此Spa的服務管理能力更強大,且更容易擴展。

若是想要ARouter同樣的路由能力可使用spa下的SpRouter, SpRouter就是基於Spa實現的一套路由方案。



5. 思考

想象一下,若是把項目中頁面、彈窗、功能都抽象成一個個的服務,而後將這些服務以資源的形式(好比URL)暴露出來給內部和外部訪問(好比給混合端(H5, Flutter)訪問,經過接口訪問,經過推送訪問,經過adb訪問等等), 當這些服務達到必定規模,整個App是否是變得更加靈活、更加動態化,這就是我當前應用的服務化的框架。

參考文檔:

rgb-24bit.github.io/blog/2019/j…

www.cnblogs.com/jalja365/p/…

相關文章
相關標籤/搜索