Android 通用的元件自動註冊、自動初始化解決方案

EastWoodYang發表於2018-07-16

背景問題

我們在元件化的過程,業務被拆分至獨立的Module中,一些公用元件會在各個Module中通過APT生成一些需要被註冊至元件中的資訊類,比如EventBus生成的Index類。我們這邊RN定製的Plugin是跟隨各自module,需要被註冊。還有,各Module對外提供的api介面的話,也需要被註冊。

另外,有些元件為某些Module特有,需要在App啟動的時候就要初始化,有些需要在主執行緒中初始化,有些為不阻塞主執行緒可以在非主執行緒中初始化。

在元件化之前,我們是在Main Module通過硬編碼來進行註冊,在Application中堆積各個元件的初始化邏輯。

有沒有更好的解決方式?

解決思路

首先,將問題分解抽象:

  • 把註冊行為進行抽象化,可以把一個類(需要被註冊的資訊)看作方法函式的入參,那方法函式就可以看作是對註冊相關邏輯的實現。那註冊問題可以進一步轉化為各模組如何把相關類(需要被註冊的資訊)轉化為方法函式的入參,元件定義方法函式,獲取入參來實現註冊邏輯。比如:

      A a = new A()
      B b = new B()
      b.shoot(a.get())
      
      A為Module定義的一個類,通過get方法可以獲得被註冊的資訊,B為元件定義的一個類,在shoot方法中實現註冊邏輯。
    複製程式碼
  • 接下來的問題是,AB分處不同模組,如何把AB按上述程式碼邏輯組合起來?

    AB組合意味著需要程式碼注入,程式碼注入使用的技術方案是Gradle Transform + ASM。AB分別實現約定的介面,再用註解標記。在編譯時,通過Gradle Transform + ASM,通過註解找到AB,生成上述格式的程式碼並注入到合適的位置。

  • 剩下的問題就是,在什麼位置注入?

    採用的方式是預先定義一個空方法,通過註解標記,並在適當時機呼叫這個空方法。在編譯時通過註解找到AB和空方法,生成上述格式的程式碼並注入到這個空方法中。

以上是有關元件註冊方面的解決思路,而模組中的元件初始化有點不同,因為其不需要入參。 但可以直接在方法函式中實現初始化邏輯,比如:

    A a = new A()
    a.shoot()
    
    模組定義一個類A,實現約定的介面,在shoot方法中做實現初始化邏輯。
複製程式碼

其餘和元件註冊方式相類似,主要在注入的程式碼邏輯上有所不同。

設計模型

Android 通用的元件自動註冊、自動初始化解決方案

具體的實現方式其實是借鑑了弓箭耙的模式。

弓箭耙模式:

  • 箭 Arrow:對應一種型號

    提供模組相關類(需要被註冊的資訊)的載體。

  • 弓 Bow:適配一種型號的箭,射向唯一的耙。

    方法函式,即實現註冊邏輯的載體。

  • 耙 Target:位置

    將被注入的空方法。

介紹AutoInject

使用方式

在根專案的build.gradle中新增外掛依賴:

buildscript {
    ... 
    dependencies {
        ...
        classpath 'com.eastwood.tools.plugins:auto-inject:1.0.0'
    }
}
複製程式碼

在模組的build.gradle中新增註解庫依賴:

dependencies {
    ...
    implementation 'com.eastwood.common:auto-inject:1.0.0'
}
複製程式碼

在主模組的build.gradle中引用外掛:

apply plugin: 'auto-inject'
 
autoInject {
    showLog = true
    ignorePackages = ['android', 'com/google']
}
複製程式碼

@AutoTarget

預先定義一個空方法並呼叫,在方法上標記@AutoTarget,例如:

public class App extends Application {
 
    public EventBusBuilder eventBusBuilder;
 
    @Override
    public void onCreate() {
        super.onCreate();
 
        eventBusBuilder = EventBus.builder();
        // add config to eventBusBuilder
        addIndex2EventBus();
        eventBusBuilder.build();

    }
 
    @AutoTarget
    void addIndex2EventBus() {}

}
複製程式碼

addIndex2EventBus 方法將被注入程式碼。

@AutoArrow

新建一個類,並實現IAutoArrow介面,在get方法中返回需要被註冊的資訊類。例如:

@AutoArrow(model = "eventBusIndex")
public class ModuleBAutoArrow implements IAutoArrow<SubscriberInfoIndex> {
 
    @Override
    public SubscriberInfoIndex get() {
        return new ModuleBEventBusIndex();
    }
 
}
複製程式碼

@AutoBow

新建一個類,並實現IAutoBow介面,在shoot方法中獲取入參並執行具體的註冊邏輯。例如:

@AutoBow(target = "addIndex2EventBus", model = "eventBusIndex", context = true)
public class EventBusAutoBow implements IAutoBow<SubscriberInfoIndex> {
 
    private App app;
 
    EventBusAutoBow(Application application) {
        app = (App) application;
    }
 
    @Override
    public void shoot(SubscriberInfoIndex index) {
        app.eventBusBuilder.addIndex(index);
    }
 
}
複製程式碼

其中 context 用於宣告EventBusAutoBow被例項化時是否需要上下文,這個上下文是被@AutoTarget標記的方法在執行時的上下文。為 true時,該類需定義一個以上下文做為唯一入參的建構函式。

@AutoBowArrow

新建一個類,並實現IAutoBowArrow介面,在shoot方法中執行相關邏輯。

@AutoBowArrow(target = "init")
public class InitAutoBowArrow implements IAutoBowArrow {

    @Override
    public void shoot() {
        // ...
    }

}
複製程式碼

兩種組合方式

  • @AutoArrow + @AutoBow + @AutoTarget,三者關係為 n:1:1

  • @AutoBowArrow + @AutoTarget ,兩者比例關係為 1:1

編譯後,被注入的程式碼樣式

打包成apk後,@AutoTarget標記的方法將會被注入具有固定結構的程式碼,例如:

// @AutoArrow + @AutoBow + @AutoTarget 組合
 
@AutoTarget
void addIndex2EventBus() {
    ModuleBAutoArrow moduleBAutoArrow = new ModuleBAutoArrow();
    EventBusAutoBow eventBusAutoBow = new EventBusAutoBow(this);
    eventBusAutoBow.shoot(moduleBAutoArrow.get());
    ...
    eventBusAutoBow.shoot(***.get());
}
 
// @AutoBowArrow + @AutoTarget 組合
 
@AutoTarget
void init() {
    InitAutoBowArrow initAutoBowArrow = new InitAutoBowArrow();
    initAutoBowArrow.shoot();
    ...
}
複製程式碼

結語

本文並沒有詳細深入解釋Gradle Transform + ASM如何查詢到類和方法並注入,原始碼已上傳至github,方便自行研究。 github地址:github.com/EastWoodYan…

相關文章