會控應用子產品化改造及優化
- 1. 子產品設計圖
-
- 1.1 子產品劃分
- 1.2 子產品引用原則
- 2.應用架構圖
- 3.資料倉庫接口設計
- 4.子產品間通信
- 5.産品相容支援
- 6. 代碼實作優化
-
- 6.1 企業通訊錄實作優化
- 6.2 會控應用實作優化
- 7. MVP/MVVM架構類圖實作
1. 子產品設計圖
子產品主要考慮橫向與縱向劃分,縱向考慮代碼複用,共用部分下沉,但是當業務增多後,下沉的代碼量急劇增加,導緻形成一個很大的Common lib,
那麼要避免這種情況就要考慮橫向切分,也就是業務解耦,也就是子產品化劃分。
1.1 子產品劃分
自上而下依次為應用層、業務實作層、業務架構層、基礎架構層
- 應用層
App層包括各個産品Ui适配,其中contacts_ui_common、conf_ui_common中定義一些可以供各産品使用的UI抽象、ViewModel、Presenter等,
比如清單、詳情、公共menu功能、各個業務子產品的Presenter、ViewModel、Adapter、Fragment等;通過泛型或者模闆設計,幫助App中快速實作UI功能。
- 業務實作層
主要是各業務子產品的Model層的代碼實作,在應用架構圖也就是Repo的實作。
- 業務架構層
公共model、路由、産品适配、公共工具、Log工具、網絡架構, 目前這些子產品代碼量較少,是以都放在同一個Module中,當業務量增加後
需要拆分成多個Module進行管理。
- 基礎架構層
該層代碼和業務無關,特定平台UI定制代碼、thirdLibs、MVP/MVVM
1.2 子產品引用原則
Module之間編譯引用的原則如下:
- 上層可以引用下層,反之不可以。
-
同一層次的cmcc_contacts_lib、cmcc_conf_lib不可以互相引用,如果需要發生接口調用,通過
cmcc_common_lib作為路由中心,通過路由設計可以達到解耦目的。
-
App層的UI module可以直接引用與之相關聯的業務實作lib,比如gvc_conf_ui可以直接引用cmcc_conf_lib
如果需要使用contacts的接口,需要通過cmcc_common_lib中路由接口擷取。
-
cmcc_common_lib是業務接口路由中心,如果上層兩個子產品之間是同一程序中調用,可以使用Aroute架構進行接口調用,如果
是不同程序間調用,需要使用aidl或者Provider接口調用。
2.應用架構圖
這個是官方提供的應用架構圖,MVVM、MVP,需要注意的是,在代碼實作的時候需要明确好所屬的概念層級,做好代碼的複用及解耦,盡量不要出現兩大段邏輯相同的代碼。
MVVM及MVP基礎架構中,提供了生命周期管理,可以通過子類繼承方式使用。
App内最好根據業務來分包,當業務量增多時,這種分包方式更加清晰直覺。
3.資料倉庫接口設計
這裡使用簡單的外觀模式将整個子產品業務抽象成一個Repo,Repo中包含本地資料源與遠端資料源,本地資料源可以了解為需要持久化或者
與應用生命周期一緻的資料,例如Db、sharePref、App靜态變量等,遠端資料是指從伺服器擷取的或者需要送出的伺服器的資料,例如Http Api接口。
Repo采用單例模式注入
public class Injection {
private static IContactsRepo mRepo;
public static IContactsRepo providerContactsRepo(@Nullable Context context){
if(mRepo == null){
synchronized (Injection.class){
if(mRepo == null){
ILocalDataSource localDataSource = ContactsLocalDataSource.getInstance();
IRemoteDataSource remoteDataSource = ContactsRemoteDataSource.getInstance();
IContactsRepo contactsRepo = ContactsRepository.getInstance(localDataSource,remoteDataSource);
contactsRepo.init(context);
mRepo = contactsRepo;
}
}
}
return mRepo;
}
public static BaseSchedulerProvider provideSchedulerProvider() {
return SchedulerProvider.getInstance();
}
}
本地資料源及遠端資料源擷取
IContactsRepo contactsRepo = Injection.providerContactsRepo(this);
ILocalDataSource localDataSource = contactsRepo.getLocalDataSource();
IremoteDataSource remoteDataSource = mRepository.getRemoteDataSource();
4.子產品間通信
子產品間通訊包括App元件通信和業務子產品接口通信
元件通信可以采用隐式Intent或者使用Arouter架構,程序間采用隐式Intent,程序内可以采用Arouter
架構。
子產品業務接口通信可以采用路由架構,我們可以自己實作,需要考慮是程序内接口通信還是程序間接口通訊,
如果是程序間通訊,可以參考GsApi的實作,如果是程序内通信可以使用Arouter架構提供的IProvider
接口實作。
Arouter架構使用參考
通訊錄
IContactsService
及其實作類
ContactsApiService
會控
IConfService
及其實作類
ConfApiService
目前的實作是通訊錄與會控應用運作在獨立的程序中,通訊錄對外提供接口主要使用ContentProvider,會控
對外提供接口主要通過aidl提供的binder接口。
通訊錄對外提供接口通過
ContactsProviderHelper
類
會控制應用對外提供接口通過
ConferenceRemoteProxy
類
5.産品相容支援
為了在同一個倉庫相容新舊産品,需要通過接口對産品差異做隔離,例如現有的H60與C12及P21的相容,
需要相容的接口包括Call Api、Dbus Api及Nvram Api。
具體實作過程:
- 定義統一的接口方法
public interface ICallCtlApi extends IApi {
boolean endCall(Context context);
boolean isCallBusy(Context context);
}
-
對統一接口在不同産品上實作
H60實作
public class CallCtlApiImpl implements ICallCtlApi {
@Override
public boolean endCall(Context context) {
return BaseCallApi.endConf();
}
}
C12
public class CallCtlApiImpl implements ICallCtlApi {
@Override
public boolean endCall(Context context) {
CallManager callManager = (CallManager)context.
getSystemService(GSManagerServiceName.CALL_MANAGER_SERVICE);
return callManager != null && callManager.endConference(true);
}
}
- 運作時注入正确的執行個體
//callCtl Api
IApi callCtlIApi = getApiImpl("com.grandstream.cmcc.voiceassistant.api.impl.CallCtlApiImpl");
if(DeviceHelper.getDeviceType() == Constants.DEVICE_TYPE_C12_OLDAPI){
callCtlIApi = getApiImpl("com.grandstream.cmcc.voiceassistant.api.impl.c12.CallCtlApiImpl");
}else if(DeviceHelper.getDeviceType() == Constants.DEVICE_TYPE_P21_OLDAPI){
callCtlIApi = getApiImpl("com.grandstream.cmcc.voiceassistant.api.impl.p21.CallCtlApiImpl");
}
if (callCtlIApi != null) {
sApiMap.put(getApiKey("com.grandstream.cmcc.voiceassistant.api.client.CallCtlApi"), callCtlIApi);
}
- 通過統一的Client調用
public class CallCtlApi {
private static ICallCtlApi getCallCtlApi(Context context){
VoiceCtlApiClient client = VoiceCtlApiClient.getInstance(context);
if(client == null){
throw new ApiUninitializedException();
}
ICallCtlApi api = (ICallCtlApi) client.getApi(NAME);
return api;
}
public static boolean endCall(Context context){
return getCallCtlApi(context).endCall(context);
}
public static boolean isCallBusy(Context context){
return getCallCtlApi(context).isCallBusy(context);
}
-
編譯時做動态依賴
編譯時候,相關産品framework最好做動态依賴,不要打包到App中。
LOCAL_JAVA_LIBRARIES := gsframework gsframework-c12 gsframework-p21
6. 代碼實作優化
- 1.統一網絡架構封裝
使用一套封裝好的網絡Api架構有利于代碼複用與維護
使用
ServiceGenerator
類提供網絡請求架構,适配了RxJava及LiveData資料類型,靜态方法
提供了Contacts及Conference的調用接口。
使用
NetServiceProvider
及
ContactsModule
類提供對外網絡Api接口。
- 2.網絡重試
一些Api常出現SocketTimeoutException異常,針對該異常使用RxJava retrywhen操作符
提供背景無感覺重試操作。
參考
RetryWithDelay
類及
ConfCtlRemoteDataSource
類中
createConf
函數。
- 3.逾時時間
一些Api資料量比較大,預設逾時時間可能不能滿足需求,是以需要動态修改逾時時間,使用OkHttp攔截器
配合反射動态修改OkHttp的預設逾時時間。
參考
DynamicTimeOutInterceptor
及
NetServiceProvider
類中
provideCmccConfApi2Service
函數。
-
4.線程模型
提供一種簡單的線程封裝,單例設計,線程池分組
參考
類。AppExecutors
定義如下:AppExecutors
public class AppExecutors {
private static final int THREAD_COUNT = 4;
private static AppExecutors appExecutors;
private final Executor diskIO;
private final Executor networkIO;
private final Executor compution;
private final MainThreadExecutor mainThread;
private final Executor serialThread;
@VisibleForTesting
AppExecutors(Executor diskIO, Executor networkIO, MainThreadExecutor mainThread, Executor serialThread) {
this.diskIO = diskIO;
this.networkIO = networkIO;
this.mainThread = mainThread;
this.serialThread = serialThread;
this.compution = Executors.newFixedThreadPool(THREAD_COUNT);
}
private AppExecutors() {
this(new DiskIOThreadExecutor(), Executors.newFixedThreadPool(THREAD_COUNT),
new MainThreadExecutor(), Executors.newSingleThreadExecutor());
}
public static AppExecutors getAppExecutors() {
if(appExecutors == null){
synchronized (AppExecutors.class){
if(appExecutors == null){
appExecutors = new AppExecutors();
}
}
}
return appExecutors;
}
public Executor diskIO() {
return diskIO;
}
public Executor networkIO() {
return networkIO;
}
public Executor compution(){
return compution;
}
public MainThreadExecutor mainThread() {
return mainThread;
}
public Executor serialThread(){
return serialThread;
}
public static class MainThreadExecutor implements Executor {
private Handler mainThreadHandler = new Handler(Looper.getMainLooper());
public void execute(@NonNull Runnable command, long delay){
mainThreadHandler.postDelayed(command, delay);
}
@Override
public void execute(@NonNull Runnable command) {
mainThreadHandler.post(command);
}
}
}
調用示例代碼如下:
public void loadOwnerAsyn(AsynCallback<Owner> callback) {
AppExecutors.getAppExecutors().diskIO().execute(new Runnable() {
@Override
public void run() {
final Owner owner = loadOwner();
AppExecutors.getAppExecutors().mainThread().execute(new Runnable() {
@Override
public void run() {
callback.onCallback(owner);
}
});
}
});
}
6.1 企業通訊錄實作優化
- 1.通訊錄層級管理
層級進入及回退采用棧資料結構管理,參考
ContactsActivity
中
onBackPressed()
的實作
private String lastParentId = "0";
private final Stack<StackNodeInfo> stack = new Stack<StackNodeInfo>();
//1.點選進入子部門
@Override
public void onPageItemClick(View itemView, GeneralNode bean, int position, int currentPage) {
lastParentId = holder.id;
mPresenter.loadChildNodes(lastParentId);
View tv = mPathView.addView(holder.labelTxt, holder.id);
stack.add(new StackNodeInfo(lastParentId, tv));
}
//2.back鍵回退
public void onBackPressed() {
if("0".equals(lastParentId)){
setCheckedContactsResult();
super.onBackPressed();
}else{
upLevel();
}
}
@Override
public String getLastParentId() {
return lastParentId;
}
//3.回退到上一級
@Override
public void upLevel() {
if(stack.size()>1){
stack.pop();
StackNodeInfo nodeInfo = stack.peek();
nodeInfo.getView().performClick();
}
}
//4.回退到任意部門
@Override
public void pop(String dpt) {
if(stack.size()<1){
return;
}
StackNodeInfo nodeInfo = stack.peek();
while (!nodeInfo.getNodeId().equals(dpt)){
if(stack.size()<1){
break;
}
stack.pop();
if(stack.size()<1){
break;
}
nodeInfo = stack.peek();
}
}
- 2.層級資料加載及資料節點操作
将同一部門下的子部門及聯系人抽象成節點進行操作,将整個通訊錄抽象成一顆樹,進而相關的操作都可以
使用樹資料結構進行實作,樹的節點由唯一的Id進行辨別。
加載某個部門下的所有節點資料的sql語句如下,這裡使用的是Room架構
參考
UserDao
@Query("SELECT _id, id, name, usrNum AS summary, pinyin, 1 AS type FROM depart WHERE pid = :pid UNION SELECT _id, id, name, mobile AS summary, pinyin, 0 AS type FROM user WHERE dptId = :pid ORDER BY type, name ASC")
Cursor queryNodesSync(String pid);
節點複選操作資料結構, 節點區分為分支節點及葉子節點
分支節點資料結構參考
ContactsLocalDataSource
中
allTreeNodes
及
allTreeMap
葉子節點資料結構參考
ContactsLocalDataSource
中
checkedLeafNodeList
企業通訊錄啟動時,需要生成分支節點的樹資料結構,以便為後續分支節點操作(選中、取消選中、加載人數)提供遞歸算法,
參考
ContactsLocalDataSource
中
generateEnpTree
由于節點操作可能會涉及遞歸算法或者資料庫查詢,是以都是放到異步操作中完成的,這裡使用RxJava或者LiveData的響應式操作完成。
請參考相關的Menu功能代碼了解相關的實作。
6.2 會控應用實作優化
- 1.會議服務啟動
以前的方式比較繞,這裡使用
CmccConfCtlService
啟動Binder執行個體,使用
CmccConfSystem
單例來管理。
- 2.會議Api1及Api2切換
通過P值控制,會議執行個體初始化時控制,參考
CmccConfCtlService
中的
initializeCmccConfSystem
函數
- 3.Api1與Api2資料結構轉化
由于Api2傳回的Json格式與Api1傳回的Json格式差異較大,為了複用Api1的Bean,需要做一下
轉化,通過
BeanConverter
及
Api2ResultConverter
兩個類完成,參考相關代碼實作。
- 4.傳遞實作了
Parcelable
接口的Bean
采用泛型設計,可以傳遞任何實作了
Parcelable
接口的業務Bean
參考
類。Result
public class Result<T extends Parcelable> implements Parcelable {
public static final int RESULT_OK = 0;
public static final Creator<Result> CREATOR = new Creator<Result>() {
@Override
public Result createFromParcel(Parcel source) {
return new Result(source);
}
@Override
public Result[] newArray(int size) {
return new Result[size];
}
};
private static final String TAG = Result.class.getSimpleName();
private String msg;
private Class classType;
private T result;
public Result(String msg, Class classType, T result) {
this.msg = msg;
this.classType = classType;
this.result = result;
}
protected Result(Parcel in) {
readFromParcel(in);
}
public T getResult() {
return result;
}
public String getMsg() {
return msg;
}
public Class getClassType() {
return classType;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(msg);
dest.writeSerializable(classType);
dest.writeValue(result);
}
private void readFromParcel(Parcel in) {
this.msg = in.readString();
Logger.d(TAG, "msg = " + msg);
this.classType = (Class) in.readSerializable();
Logger.d(TAG, "classType = " + classType);
this.result = (T) in.readValue(classType.getClassLoader());
}
}
使用
if(callBack != null && result instanceof Parcelable){
String className = result.getClass().getSimpleName();
callBack.onSuccess(new Result(className,result.getClass(),(Parcelable) result));
}
7. MVP/MVVM架構類圖實作
下面是MVVM及MVP架構抽象模型設計類圖