組件化方案

近期公司有組件化的打算,因此對市面上的方案進行了調研,目前已經整理出一套作爲項目組件化的方案,這裏分享一波,當然組件化是沒法一步到位的,中間肯定少不了踩坑優化,所以本篇也會持續更新。

那麼我們先說說組件化是幹嘛的吧,組件化就是將單模塊的項目拆成多個,並且每個模塊可以單獨運行。

WTF!!!這麼簡單?

對概念就是這麼簡單,但當我們去做的時候就會發現幾個問題

  1. 模塊如何單獨運行
  2. 拆成獨立模塊後初始化問題(組合運行和獨立運行的時候怎麼初始化)
  3. 跨模塊方法調用(如何啓動Activity、跨模塊獲取數據)
  4. 模塊獨立運行時跨模塊方法調用

那麼要想組件化就需要解決上面這幾個問題,所以接下來就是圍繞這幾個問題展開討論,不過在這之前我們先看看整體架構有個大概的認識。

組件化架構圖

從組件的劃分上分爲四層,從上往下依次爲

  • App殼工程:負責管理各個業務組件和打包APK,沒有具體的業務功能。

  • 業務組件層:根據不同的業務構成獨立的業務組件。

  • 功能組件層:對上層提供基礎功能服務不包含業務,如地圖、拍照、日誌等。

  • 組件基礎設施:Base類、第三方Sdk、View等一些通用代碼。

這裏單獨說下業務組件和功能組件,一個典型的業務組件工程結構是這個樣子:

以上圖爲例,它包含三個模塊(兩個Library和一個Application):

  • jd :組件代碼,它包含了這個組件所有業務代碼並實現了jd-api的接口。
  • jd_api:組件的接口模塊,專門用於與其他組件通信,只包含 Model、Interface 和 Event,不存在任何業務和邏輯代碼。
  • jd_app 模塊:用於獨立運行 app,它直接依賴組件模塊,只要添加一些簡單的配置,即可實現組件獨立運行。

你可能會問爲什麼要有個jd_api模塊,其實和接口隔離是一個意思,jd_api模塊存放着jd模塊需要對外暴露的接口,jd模塊去實現這些接口,當別的模塊想要調用jd模塊方法的時候拿到的是jd_api模塊的接口對象,從而隔離jd模塊,只不過這些接口是裝在一個獨立的library中,之所以這樣也是因爲業務模塊粒度太大,包含的代碼量較多,如果將接口放在業務模塊內,既不利於隔離不同實現,還會因爲獲取接口實現類增加很多冗餘的判斷代碼,所以將接口單獨作爲一個library模塊,具體實現類的話根據具體業務場景依賴對應的業務模塊。

以jd模塊爲例,他需要依賴jd_api並實現它的接口

dependencies {
    ...
    implementation project(':component-jd:jd_api')
  	...
}

而獨立運行的jd_app模塊則需要依賴接口模塊jd_api和業務具體實現模塊jd

dependencies {
    ...
    runtimeOnly project(':component-jd:jd')//runtimeOnly可以防止我們在寫代碼的時候直接引用到jd模塊的類
    implementation project(':component-jd:jd_api')
  	...
}

如果哪天對於jd的業務有新的實現,我們只需要修改runtimeOnly project(':component-jd:jd')依賴即可,至於怎麼拿到接口實現類是通過Arouter這個框架去獲取的,後面會說。

對於功能模塊來說,同樣也需要用接口隔離,但與業務模塊不同的是功能模塊本身相對獨立沒有業務邏輯,所以不需要單獨爲接口創建一個library,直接把對外暴露的接口定義在功能模塊內即可,外部只需通過工廠拿到具體實現類進行操作。

以支付功能模塊爲例:

在支付模塊內有一個接口IPay進行隔離,RandomPay爲接口具體實現類,業務模塊要想調用支付模塊的方法只需通過PayFactory拿到IPay實現類操作即可。

模塊如何單獨運行

模塊要想單獨運行只需要新建一個Application殼工程用來作爲獨立運行的入口,模塊本身永遠是library,然後殼工程依賴模塊即可,那麼一個模塊的目錄將變成如下這樣:

projectRoot
+--app
+--component_module1(文件夾)
	|  +--module1(業務模塊library)
	|  +--module1_api(業務組件的接口模塊,專門用於與其他組件通信library)
	|  +--module1_app (獨立運行的殼工程Application)

app模塊是全量編譯的application模塊入口,module1是業務library模塊,module1_api是業務組件的接口library模塊,module1_app是用來獨立啓動 module1的application模塊。

對於獨立運行的module1_app模塊只需依賴業務接口模塊和業務模塊

dependencies {
  	...
  	runtimeOnly project(':module1')
    implementation project(':module1_api')
  	...
}

對於全量編譯的app模塊則根據所需業務依賴對應的業務接口模塊和業務模塊

dependencies {
  	...
  	runtimeOnly project(':module1')
    implementation project(':module1_api')
    runtimeOnly project(':module2')
    implementation project(':module2_api')
  	...
}

由於有專門用於單獨啓動的module1_app模塊的存在,業務的 library模塊只需要按自己是library模塊這一種情況開發即可,而爲了讓業務模塊單獨啓動所需要的配置、初始化工作都可以放到module1_app模塊裏,並且不用擔心這些代碼被打包到最終Release的App中。

拆成獨立模塊後初始化問題

初始化的邏輯我們可以細分爲兩類

  1. 通用的初始化邏輯
  2. 每個模塊個性化的初始化邏輯

對於通用的初始化邏輯可以寫在Base模塊的Application中

public class BaseApplication extends Application {

    private static Application sApplication;

    @Override
    public void onCreate() {
        super.onCreate();
        sApplication = this;
        initARouter(this);
    }

    public void initARouter(Application application) {
        if (BuildConfig.DEBUG) {           // 這兩行必須寫在init之前,否則這些配置在init過程中將無效
            ARouter.openLog();     // 打印日誌
            ARouter.openDebug();   // 開啓調試模式(如果在InstantRun模式下運行,必須開啓調試模式!線上版本需要關閉,否則有安全風險)
            ARouter.printStackTrace(); // 打印日誌的時候打印線程堆棧
        }
        ARouter.init(application); // 儘可能早,推薦在Application中初始化
    }

    public static Application getApplication() {
        return sApplication;
    }


}

無論是組合運行還是獨立運行的殼app的Application都繼承這個BaseApplication完成通用邏輯的初始化。

對於個性化的初始化邏輯則放在模塊內部,在獨立運行的時候沒有問題可以讓module1_app的Application繼承我們業務模塊提供的Application完成初始化,但組合運行的時候由於系統只會創建一個Application就是app的,又因爲我們不允許app模塊直接調用業務模塊的方法,需要通過module_api去調用,而業務模塊又沒法在app的Application創建前將初始化服務註冊,導致app的Application#onCreate()方法中獲取不到業務模塊的初始化服務實現類無法初始化,其實可以通過APT在編譯期獲取到需要初始化的類然後在BaseApplication裏面加入初始化這些類的邏輯,但我們這裏選用了一個騷方法解決這個問題,使用contentProvider來初始化

每個業務模塊自己聲明一個ContentProvider用來初始化當前模塊自己個性化的東西,如果對ContentProvider初始化順序還有要求可以通過initOrder屬性來控制(值越大,越先初始化),詳情請見Android 多個 ContentProvider 初始化順序

public class JDInitProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        Log.i("zhuliyuan","JD初始化"+getContext());
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        return null;
    }


    @Override
    public String getType(Uri uri) {
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        return 0;
    }
}

有一點需要注意的是ContentProvider的authorities屬性不能重複,爲了模塊組合運行和獨立運行都ok,所以我們用包名作爲前綴避免重複。

        <provider
            android:name=".JDInitProvider"
            android:authorities="${applicationId}.JDInitProvider"
            android:exported="false"
            android:multiprocess="true"
            android:initOrder="200"/>

跨模塊方法調用

跨模塊方法調用可以分爲兩類

  1. startActivity啓動頁面
  2. 模塊間方法調用

先說startActivity,由於我們項目中已經集成了Arouter所以我就直接把它作爲了啓動頁面的路由,並且Arouter本身也支持組件化,對於Arouter Api可以查看官方文檔這裏不贅述,唯一需要規範下的是對於頁面的跳轉我們需要進行一道封裝,原因是因爲通過url方式的路由在ide中沒法提示,那麼當我們要啓動其他人維護的頁面的時候並不能在ide上提示出對應的參數類型和數量導致溝通成本增大,並且容易產生bug。

以jd_app模塊啓動jd模塊Activity爲例:

首先我們在jd_api模塊中定義出對外暴露的路由方法

public class JDRouter {
    public interface Path {
        String JD_ACTIVITY = "/jd/activity";
    }

    public interface Params {
    }

    public static void toJDActivity() {
        ARouter.getInstance().build(Path.JD_ACTIVITY).navigation();
    }
}

jd模塊的JDActivity添加路由標記

@Route(path = JDRouter.Path.JD_ACTIVITY)
public class JDActivity extends BaseActivity {

    @Autowired(name = ResidentRouter.Path.SERVICE_PAY_RESULT)
    PayResultService service;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.jd_activity_main);
    }
}

jd_app模塊則通過jd_api的方法啓動JDActivity

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void jump(View view) {
        JDRouter.toJDActivity();
    }
}

經過JDRouter封裝一層後ide則可以給我們提示出對應的方法和參數,能有效的避免因爲溝通問題產生的bug。

接下來在說說模塊間方法調用,具體點可細分爲上層模塊調用下層模塊和同層模塊間調用,但不管是哪種調用都是需要用接口隔離的,調用者需要拿到接口的實現類去執行對應邏輯,而獲取接口實現類這個過程也是通過Arouter實現的。

以Resident業務模塊爲例:

resident_api模塊需要聲明對外暴露的接口和接口的路徑

public interface PayResultService extends IProvider {//對外暴露接口
    int getPayResult();
}

public class ResidentRouter {
    public interface Path {
        String SERVICE_PAY_RESULT = "/pay/result";//接口路由路徑
    }

    public interface Params {

    }

}

resident模塊實現該接口

@Route(path = ResidentRouter.Path.SERVICE_PAY_RESULT)
public class PayResultServiceImpl implements PayResultService {
    @Override
    public int getPayResult() {
        return 100;
    }

    @Override
    public void init(Context context) {

    }
}

resident_app模塊依賴對外暴露的resident_api和具體實現類resident

dependencies {
  	...
    runtimeOnly project(':component_resident:resident')
    implementation project(':component_resident:resident_api')
  	...
}

通過ARouter提供的注入的方式拿到接口實現類,完成跨模塊方法調用

public class MainActivity extends AppCompatActivity {

    @Autowired(name = ResidentRouter.Path.SERVICE_PAY_RESULT)
    PayResultService service;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ARouter.getInstance().inject(this);
        tv.setText("需要支付金額:" + String.valueOf(service.getPayResult()));
    }
}

模塊獨立運行時跨模塊方法調用

同級模塊有依賴的情況下,組合運行沒問題,但是單獨運行的時候由於沒有對應模塊提供接口實現,那麼我們通過arouter沒法拿到具體的實現,這個時候就需要mock數據了,而mock相關的操作是爲了我們獨立運行,所以寫在獨立運行的殼工程中。以jd模塊爲例,假設jd模塊的運行需要依賴resident模塊,那麼jd_app就需要實現resident_api中jd需要的方法,以便jd模塊獨立運行的時候能夠獲取到resident的數據。

Jd_app依賴關係如下

dependencies {  
  	...
		runtimeOnly project(':component-jd:jd')
    implementation project(':component-jd:jd_api')
    implementation project(':component_resident:resident_api')
  	...
}

mock jd模塊獨立運行所需的resident_api數據

@Route(path = ResidentRouter.Path.SERVICE_PAY_RESULT)
public class MockPayResultService implements PayResultService {
    @Override
    public int getPayResult() {
        return 100;
    }

    @Override
    public void init(Context context) {

    }
}

Tips

  • 組件化後有資源衝突的可能性所以命名還得規範,比如加前綴
 // Login 組件的 build.gradle
 android {
     resourcePrefix "login_"
     // 其他配置 ...
 }

如果組件配置了 resourcePrefix ,其 xml 中定義的資源沒有以 resourcePrefix 的值作爲前綴的話,在對應的 xml 中定義的資源會報紅。resourcePrefix 的值就是指定的組件中 xml 資源的前綴,不過沒法約束圖片命名需要自己注意。

  • 代碼隔離Gradle 3.0 提供了新的依賴方式 runtimeOnly ,通過 runtimeOnly 方式依賴時,依賴項僅在運行時對模塊及其消費者可用,編譯期間依賴項的代碼對其消費者時完全隔離的,避免開發中直接引用到組件中類的問題
 // 主項目的 build.gradle
 dependencies {
     // 其他依賴 ...
     runtimeOnly project(':component-jd:jd')
     implementation project(':component-jd:jd_api')
 }

最後附上組件化Demo地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章