Android 元件化之通訊(多模組,多程式)

wutongke發表於2019-03-01

專案地址:github.com/wutongke/Mo…

1. 引子

寫這篇文章主要是有兩個原因:

  1. 之前寫過一篇Android元件化開發實踐,元件化最直接的表現形式就是工程中包含了多個業務Module,程式碼要解耦,但是業務間要解耦幾乎是不可能的,於是就要涉及到業務間的通訊,表現在程式碼上就是Module間通訊。其實在文章提到的ActivityRouter就是模組間通訊很好的一個library,但是其主要作為Activity Router來使用,傳遞資料的能力有限(當然稍微改造一下程式碼,交換資料還是很容易解決的)。
  2. 最近看到了Spiny同學的Android架構思考(模組化、多程式)
    ,寫的非常好,在掘金上分享後得到了不少同學的點贊。尤其文中提到的多程式方案,自己也非常感興趣,於是就去看了原始碼,很有想象力,但是在使用時個人感覺也有一些小問題,於是就fork了一份程式碼,開始對程式碼進行了一些修改,本文中將主要介紹一下這些修改。

2. 模組間通訊原理

我們先來看一下架構圖,圖片來源於blog.spinytech.com/2016/12/28/…

Android 元件化之通訊(多模組,多程式)
路由架構

從A、B到N的多個Module都引用了Common庫,同時Main Module還引用了A、B、N這幾個Module,經過這樣的處理之後,所有的Module之間的相互呼叫就都消失了,耦合性降低,所有的通訊統一都交給Router來處理分發,而註冊工作則交由Main Module去進行初始化。這個架構思想其實和Binder的思想很類似,採用C/S模式,模組之間隔離,資料通過共享區域進行傳遞。模組與模組之間只暴露對外開放的Action,所以也具備面向介面程式設計思想。

架構圖中每個紅色的Action都是可以提供的服務,而Provider是一個服務的集合,每個Module中可以有一個或者多個Provider,當程式開始執行時,module會把Provider註冊到Router Module。Action服務請求流程如下:

Android 元件化之通訊(多模組,多程式)

  1. 任意程式碼建立一個RouterRequest,包含Provider和Action資訊,向Router進行請求。
  2. Router接到請求,通過RouterRequest的Provider資訊,在內部的HashMap中查詢對應的Provider。
  3. Provider接到請求,在內部的HashMap中查詢到對應的Action資訊。
  4. Action呼叫invoke方法。
  5. 返回invoke方法生成的ActionResult。
  6. 將Result封裝成RouterResponse,返回給呼叫者。

3. 多程式架構模組通訊原理

還是先來看下架構圖:

Android 元件化之通訊(多模組,多程式)

Router是JVM級別的單例模式,並不支援跨程式訪問。也就是說,你的後臺程式的所有Provider、Action,是註冊給後臺Router的。當你在前臺程式呼叫的時候,根本呼叫不到其他程式的Action。

解決方案是單獨提取一個Wide Router模組,所有的Local Route與Wide Router通過程式間通訊的方式建立連線,Action請求如果在Local Router中找不到時,則通過WideRouter與其它程式建立連線,WideRouter充當區域網之間的路由。一次跨程式的Action請求如下圖所示:

Android 元件化之通訊(多模組,多程式)

以上的內容主要來自於Android架構思考(模組化、多程式)
,目前也已經有了相應的demo,大家可以去嘗試一下,體驗多程式App的樂趣。

4. 可以改進的地方

關於ModularizationArchitecture的使用,有相應的文件:ModularizationArchitecture 使用教程。個人在使用後感覺又一些不太方便的地方,主要有三點:

4.1 通訊資料格式問題

目前發起請求時傳遞的資料是RouterRequest,接收的資料是RouterReponse,兩種型別的資料中包含的只有字串資料,在實際的程式間通訊時傳遞的資料也是字串(程式碼中轉為了json資料)。這種方式不能傳遞自定義的資料,在資料使用時也要手動解析字串,比較繁瑣。
其實Android為程式間通訊提供了Parcelable,可以通過這種方式非常方便地傳遞資料。

4.2 執行緒切換問題

demo中採用新建執行緒的方式請求非同步資料:

final RouterResponse response = LocalRouter.getInstance(MaApplication.getMaApplication())
                            .route(MainActivity.this, RouterRequest.obtain(MainActivity.this)
                                    .domain("com.spinytech.maindemo:music")
                                    .provider("music")
                                    .action("play"));
                    response.isAsync();
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                final String temp = response.getData();
                                final long time = System.currentTimeMillis() - startTime;
                                handler.post(new Runnable() {
                                    @Override
                                    public void run() {
                                        try {
                                            Toast.makeText(MainActivity.this, "async:" + response.isAsync() + " cost:" + time + " response:" + response.get(), Toast.LENGTH_SHORT).show();
                                        } catch (Exception e) {
                                            e.printStackTrace();
                                        }
                                    }
                                });
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }).start();複製程式碼

程式碼看起來不夠優雅,採用Rxjava 的方式會使得執行緒切換更加優雅,Router模組可以使用Rxjava的方式返回結果。

4.3 Provider、Action 註冊問題

程式碼中Provider、Action需要手動註冊,如果增加一個Action,需要有多個地方進行變動,這裡可以採用apt的方式來自動進行註冊。

5. 解決方案

針對以上的三個問題,對程式碼進行來部分修改,如下:

5.1 程式間通訊資料格式的修改

請求資料RouterResquest 和返回資料MaActionResult分別實現了Parcelable介面,並且分別提供了兩個可以自定義的成員變數requestObjectresult,使用者在建立請求資料RouterResquest 和返回資料MaActionResult可以把自定義的資料傳遞進去,需要注意的是傳遞的自定義型別也要實現Parcelable介面。

//自定義資料
public class Song implements Parcelable {
    public String name;

    public Song(String name) {
        this.name = name;
    }

    protected Song(Parcel in) {
        name = in.readString();
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    public static final Creator<Song> CREATOR = new Creator<Song>() {
        @Override
        public Song createFromParcel(Parcel in) {
            return new Song(in);
        }

        @Override
        public Song[] newArray(int size) {
            return new Song[size];
        }
    };
}複製程式碼
//RouterResquest中設定了自定義型別Song
RouterRequestUtil.obtain(MainActivity.this)
                                    .domain("com.spinytech.maindemo:music")
                                    .provider("music")
                                    .action("play")
                                    .reqeustObject(new Song("see you"))
                            )
//MaActionResult中設定自定義型別Song
MaActionResult result = new MaActionResult.Builder()
                .code(MaActionResult.CODE_SUCCESS)
                .msg("play success")
                .result(new Song("lili"))
                .build();複製程式碼

5.2 Provider、Action自動生成

// 註解Provider,程式執行後將自動註冊MusicProvider到com.spinytech.maindemo:music到Router
@Provider(processName = "com.spinytech.maindemo:music")
public class MusicProvider extends MaProvider{
    @Override
    protected String getName() {
        return "music";
    }
}
// 註解Action,程式執行後將自動註冊PlayAction到Provider
@Action(processName = "com.spinytech.maindemo:music", providerName = "music")
public class PlayAction implements MaAction<Song>複製程式碼

5.3 Rxjava 的引入

引入Rxjava之後,修改LocalRoute的route方法,使之返回Observable<MaActionResult>,在呼叫時可以非常方便地使用Rxjava切換執行緒:

 LocalRouter.getInstance(MaApplication.getMaApplication())
                .rxRoute(MainActivity.this, RouterRequestUtil.obtain(MainActivity.this)
                        .domain("com.spinytech.maindemo:pic")
                        .provider("pic")
                        .action("pic")
                        .data("is_big", "0"))
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.from(ThreadPool.getThreadPoolSingleton()))
                .subscribe(new Consumer<MaActionResult>() {
                    @Override
                    public void accept(MaActionResult maActionResult) throws Exception {
                        Toast.makeText(MainActivity.this, maActionResult.getMsg(), Toast.LENGTH_SHORT).show();
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(Throwable throwable) throws Exception {
                        Toast.makeText(MainActivity.this, "error", Toast.LENGTH_SHORT).show();
                    }
                });複製程式碼

6. 使用教程

專案地址:github.com/wutongke/Mo…

6.1 在專案中整合

6.1.1 在project的build.gradle中dependencies塊中支援apt:

classpath `com.neenbedankt.gradle.plugins:android-apt:1.8`複製程式碼

6.1.2 所有Module中配置apt外掛:

apply plugin: `com.neenbedankt.android-apt`複製程式碼

dependencies塊中設定:

 apt `com.github.wutongke.modularization:compile:1.1.1`
 compile `com.github.wutongke.modularization:macore:1.1.1`複製程式碼

6.2 建立自定義Application

6.2.1 實際Application
我們知道一個app中只有一個Application,所以在主Module中定義Application,然後在其它模組中根據需要實現邏輯Application即可,然後啟動時註冊邏輯Application,即可管理其生命週期:

public class MyApplication extends MaApplication {

    //多程式中註冊各個程式的Router,可以參考第3小節的原理圖
    @Override
    public void initializeAllProcessRouter() {
        WideRouter.registerLocalRouter("com.spinytech.maindemo",MainRouterConnectService.class);
        WideRouter.registerLocalRouter("com.spinytech.maindemo:music",MusicRouterConnectService.class);
        WideRouter.registerLocalRouter("com.spinytech.maindemo:pic",PicRouterConnectService.class);
    }

//註冊各個模組中的邏輯Application,每個模組中可以註冊多個邏輯
//Applicatoin,設定優先順序,可以調整模組中多個邏輯Application的
//呼叫順序
    @Override
    protected void initializeLogic() {
        registerApplicationLogic("com.spinytech.maindemo",999, MainApplicationLogic.class);
        registerApplicationLogic("com.spinytech.maindemo",998, WebApplicationLogic.class);
        registerApplicationLogic("com.spinytech.maindemo:music",999, MusicApplicationLogic.class);
        registerApplicationLogic("com.spinytech.maindemo:pic",999, PicApplicationLogic.class);
    }

//設定是否支援多程式
    @Override
    public boolean needMultipleProcess() {
        return true;
    }
}複製程式碼

當然這個自定義的Application需要註冊到manifest檔案中。
使用多程式提供服務的模組需要繼承LocalRouterConnectService,並且在manifest中註冊服務:

public class MusicRouterConnectService extends LocalRouterConnectService {
    @Override
    public boolean onUnbind(Intent intent) {
        Log.e("MRCS","onUnbind");
        return super.onUnbind(intent);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.e("MRCS","onDestroy");
    }
}複製程式碼
<service android:name=".MusicRouterConnectService"
            android:process=":music"/>複製程式碼

6.2.2 邏輯Application
邏輯Application通過繼承BaseApplicationLogic,實現相應的方法即可被回撥。

public class BaseApplicationLogic {
    protected MaApplication mApplication;
    public BaseApplicationLogic() {
    }

    public void setApplication(@NonNull MaApplication application) {
        mApplication = application;
    }

    public void onCreate() {
    }

    public void onTerminate() {
    }

    public void onLowMemory() {
    }

    public void onTrimMemory(int level) {
    }

    public void onConfigurationChanged(Configuration newConfig) {
    }
}

//邏輯Application只需要繼承BaseApplicationLogic,註冊後
//生命週期會被回撥
public class MainApplicationLogic extends BaseApplicationLogic {
    @Override
    public void onCreate() {
        super.onCreate();
    }
}複製程式碼

6.3 自定義Provider和Action

定義Provider

@Provider(processName = "com.spinytech.maindemo:music")
public class MusicProvider extends MaProvider{
    @Override
    protected String getName() {
        return "music";
    }
}複製程式碼

定義Action

@Action(processName = "com.spinytech.maindemo:music", providerName = "music")
public class PlayAction implements MaAction<Song> {

    @Override
    public boolean isAsync(Context context, RouterRequest<Song> requestData) {
        return false;
    }

    @Override
    public MaActionResult invoke(final Context context, final RouterRequest<Song> requestData) {
        Intent intent = new Intent(context, MusicService.class);
        intent.putExtra("command", "play");
        context.startService(intent);
        MaActionResult result = new MaActionResult.Builder()
                .code(MaActionResult.CODE_SUCCESS)
                .msg("play success")
                .result(new Song("lili"))
                .build();
        Handler handler = new Handler(context.getMainLooper());
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (requestData != null && requestData.getRequestObject() != null) {
                    Toast.makeText(context, "歌曲名字:" + requestData.getRequestObject().name + "(並不知道)", Toast.LENGTH_SHORT).show();
                }
            }
        });
        Logger.d("com.spinytech", requestData.getRequestObject().name);
        return result;
    }

    @Override
    public String getName() {
        return "play";
    }
}複製程式碼

可以看到定義Provider和Action時分別使用了@Provider@Action 註解,這樣可以在程式編譯時完成自動的註冊,不需要手動註冊到Router了。

其中 @Provider需要設定程式名字,@Action 需要設定程式名字和註冊到的Provider名字:

@Provider(processName = "com.spinytech.maindemo:music")
@Action(processName = "com.spinytech.maindemo:music", providerName = "music")複製程式碼

6. 4 呼叫Action

6.4.1 建立Action呼叫
首先需求建立一個請求RouterRequest,說明要請求的內容:

RouterRequestUtil.obtain(MainActivity.this)
                                    .domain("com.spinytech.maindemo:music")
                                    .provider("music")
                                    .action("play")
                                    .reqeustObject(new Song("see you"))複製程式碼

可以通過RouterRequestUtil的obtain方法快速建立請求,上例中請求的Action位於”com.spinytech.maindemo:music”程式,Provider是”music”,Action是”play”,並且傳遞了相應的引數new Song(“see you”)。

然後使用Rxjava的方式請求Action:

LocalRouter.getInstance(MaApplication.getMaApplication())
                            .rxRoute(MainActivity.this, RouterRequestUtil.obtain(MainActivity.this)
                                    .domain("com.spinytech.maindemo:music")
                                    .provider("music")
                                    .action("play")
                                    .reqeustObject(new Song("see you"))
                            )
                            .subscribeOn(Schedulers.from(ThreadPool.getThreadPoolSingleton()))
                            .observeOn(AndroidSchedulers.mainThread())
                            .subscribe(new Consumer<MaActionResult>() {
                                @Override
                                public void accept(MaActionResult maActionResult) throws Exception {
                                    Toast.makeText(MainActivity.this, maActionResult.getMsg(), Toast.LENGTH_SHORT).show();
                                }
                            }, new Consumer<Throwable>() {
                                @Override
                                public void accept(Throwable throwable) throws Exception {
                                    Toast.makeText(MainActivity.this, "error", Toast.LENGTH_SHORT).show();
                                }
                            });複製程式碼

6.4.2 處理請求
在music模組中處理剛剛發出的請求,6.3中定義的Provider和Action其實就是處理6.4.1中的請求的,並且返回了MaActionResult:

MaActionResult result = new MaActionResult.Builder()
                .code(MaActionResult.CODE_SUCCESS)
                .msg("play success")
                .result(new Song("lili"))
                .build();複製程式碼

7. 總結

6小節介紹了ModularizationArchitecture的基本使用,ModularizationArchitecture提供了完整的demo,大家可以clone程式碼參考,有任何問題可以在該專案下提issue,歡迎交流。

注意:在demo的gradle.properties中可以配置Local屬性,從而根據需要設定使用本地的library,還是遠端的library,更改Local後注意sync。

Android 元件化之通訊(多模組,多程式)

歡迎關注公眾號wutongke,每天推送移動開發前沿技術文章:

Android 元件化之通訊(多模組,多程式)
wutongke

推薦閱讀:

Android 元件化開發原理和配置

Android元件化開發實踐

相關文章