元件化解耦的架構設計思考
目錄
元件化
最近幾天在整理專案中的要點,元件化相信大家都不陌生,還是複用以前的一張專案架構圖,可以看到,專案的架構目前看起來比較清晰了,在最下層沉澱的是我們的公共庫,比如網路庫
,圖片庫
,工具類
,......
等等
上層的業務,比如短視訊模組
,分享模組
,直播間模組
等等,彼此直接並不會相互依賴,但是今天想說的是解耦
的問題
一個需求引發的思考
由於公司另外一個專案組需要使用我們的核心功能,比如直播間
,短視訊
等業務模組,其他的會砍掉,當然目前筆者已經踩坑過了關於
現在問題來了,另外一個組是手機電視
類的專案,它們的App內部已經有依賴ijkplayer
實現的播放器了,但是我們內部使用的是阿里雲播放器
,當然了直接合並使用我們的一整套短視訊
業務模組,也沒有問題,但是無形當中會大幅增加apk包
的體積(由於兩者下層都是基於ffmeng庫封裝的
),相當於一個應用內重複包含了幾個播放庫,那能不能複用同一套呢?換句話說,能否實現我們的專案編譯打包apk
的時候,載入的是阿里雲播放器
的實現類,而給其他專案組合包成aar
之後,他們載入自己的ijkplayer
實現類呢?
業務與實現分離
以最典型的短視訊
模組為例子,開發階段,新建兩個module
video
業務模組和video-impl
播放器實現類模組,讓video-impl
元件只依賴common
元件和video
業務元件,然後讓video-impl
以application
的方式執行,開發。
筆者這裡簡化了專案模型,但是基本原理是一致的。
在我們自己的video元件
中抽象我們的播放器的一個IVideoPlay
的介面
public interface IVideoPlay extends ILifeCycle {
/**
* 繫結視訊顯示容器
*/
View bindVideoView();
/**
* 初始化播放器
*/
void initPlayer(Context context);
/**
* 視訊源
*
* @param url
*/
void setRemoteSource(String url);
/**
* 重置
*/
void reset();
/**
* 停止播放
*/
void stop();
/**
* 遠端視訊源
*
* @param vid
* @param auth
*/
void setRemoteSource(String vid, String auth);
/**
* 視訊播放回調
*/
void setVideoPlayCallback(VideoPlayCallback videoPlayCallback);
/**
* 獲取視訊寬度
*
* @return
*/
int getVideoWidth();
/**
* 獲取視訊高度
*
* @return
*/
int getVideoHeight();
/**
* 喚起
*/
void onResume();
/**
* 掛起
*/
void onPause();
}
然後在依賴它的上層元件video-impl
中實現該該介面,如MediaVideoPlayImpl
,筆者這裡為了簡化,直接使用系統類來實現的,看下圖比較直觀:
但是有個新問題,那就是我們的video元件
內部VideoPlayActivity
都是在下層,如何拿到上層的MediaVideoPlayImpl
的實現類,例項化,然後播放視訊呢?如果直接在下層通過new
操作符,必然會產生強依賴
,上層播放器實現類依賴下層介面
,而下層業務又需要上層的實現類
,這種迴圈依賴的尷尬局面。
當然了,筆者經過縝密的思考(反編譯某廠SDK)後,確定了一種可行的方案:動態代理
public static <T> T getService(final Class<T> targetClazz) {
if (!targetClazz.isInterface()) {
throw new IllegalArgumentException("only accept interface: " + targetClazz);
}
return (T) Proxy.newProxyInstance(targetClazz.getClassLoader(), new Class<?>[]{targetClazz}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
try {
return invokeProxy(targetClazz, proxy, method, args);
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
return null;
}
});
}
相當於我們自己通過系統提供的Proxy.newProxyInstance
拿到對應介面的代理實現類,預設都是空實現,然後在自定義的InvocationHandler
中的invoke
方法替換成我們目標的實現類,如果存在則通過反射例項化,執行返回結果
。
如何才能在執行期間拿到對應介面的實現類呢?
- 第一步:我們可以在最下層的
common
元件中,定義一個IPlugin
介面,內容為
/**
* @anchor: andy
* @date: 2017-08-22
* @description:
*/
public interface IPlugin {
/**
* 待掃描的外掛包目錄
*/
String PLUGIN_PACKAGE = "com.onzhou.design.plugin";
/**
* 初始化外掛
*
* @param applicationContext
*/
void initPlugin(Context applicationContext);
/**
* 獲取該外掛模組的
* 所有對映
*
* @return
*/
Map<Class<?>, Class<?>> loadPluginMapping();
}
- 第二步:在我們目標的
video-impl
元件中新建包名com.onzhou.design.plugin(這個包名是約定統一好的,後面進行dex掃描會用到)
,然後新建實現類VideoPlugin
如下:
/**
* @anchor: andy
* @date: 2018-10-24
* @description: 會被自動掃描載入
*/
public class VideoPlugin implements IPlugin {
@Override
public void initPlugin(Context applicationContext) {
}
@Override
public Map<Class<?>, Class<?>> loadPluginMapping() {
Map<Class<?>, Class<?>> map = new HashMap<>();
map.put(IVideoPlay.class, MediaVideoPlayImpl.class);
return map;
}
}
- 第三步.:應用啟動的時候,我們只需要在
Application
中的onCreate
方法中,掃描((具體的掃描方法和工具類,大家可以去看ARouter的原始碼中都有
)當前dex
檔案中指定包名com.onzhou.design.plugin
下的所有IPlugin
外掛的實現類,然後通過對應的loadPluginMapping
方法獲取到每個介面對應實現類的對映
快取在我們應用內,可以通過在應用內部維護一個單例
快取起來,注意:此時僅僅只是掃描出了介面與實現類之間的對映關係,並未例項化對應的實現類
最後在我們的video
業務元件中就可以通過
getService(IVideoPlay.class).initPlayer(context);
的方式就可以拿到上層的播放器實現類MediaVideoPlayImpl
,由於依賴的第三方播放器庫都在video-impl
這個元件中,因此它可以很好的和下層的業務元件分離,僅僅只是完成它播放的核心功能。
為啥要這麼做呢?
對於一般的應用而言,無論你最終分離多少個業務元件,最終都是在最上層合併成一個apk
檔案,因為最上層的app
元件,全部都會依賴下層的所有元件:
compile project(':common')
compile project(':share')
compile project(':share-impl')
compile project(':video')
compile project(':video-impl')
......
那分離的意義和價值又在哪裡呢?其實這個問題又回到了我之前說到的一個業務上的需求
上去了,因為公司的業務特殊,我們給另外一個組的SDK包
可能只包含我們的部分業務功能,要做到體積儘可能小,而且不能侵入我們的核心業務
embedded project(':common')
embedded project(':share')
embedded project(':video')
相當於,我們只把我們的業務元件和介面
合併成一個最終的aar包
,那麼對於其他使用的人來說,他只需要幾個步驟即可:
- 第一步:通過maven的方式依賴我們的
SDK包
- 第二步:用他們自己內部的播放器,比如
ijkplayer
來實現我們的IVideoPlay
介面 - 第三步:在他們內部
com.onzhou.design.plugin
包下面,實現IPlugin
介面,定義好介面和實現類的對映
這樣在他們的應用啟動的時候,呼叫我們的工具類可以掃描到dex
檔案中的IPlugin
實現類,進而快取到所有的介面和實現類的對映
,那麼在進入我們SDK內部的短視訊模組
的時候,我們就可以通過動態代理的方式,拿到對應的實現類,例項化之後完成呼叫。
元件之間的通訊
元件之間的通訊方式很多種,最常見的就是Activity
之間的挑戰,這個我們可以直接使用ARouter
來完成,避免元件之間的強依賴
,還可以通過廣播
,事件匯流排框架
等等完成通訊。
小結:
目前這種方案在專案中已經實踐一年多了,不僅能保證我們主專案業務的並行高效開發
,業務元件與業務元件除了對下層公共庫由依賴,彼此之間沒有直接依賴
,同時在提供SDK合包
的時候,對我們的主業務也沒有任何侵入性
,擴充套件性很強,當然有的人可能認為,反射會影響一定的效能,但是怎麼說呢?首先這個反射並不是平凡呼叫,我們在內部會有快取例項的機制,第二點,我覺得在架構方面,效能可以適當的給擴充套件性讓一讓步,很多時候我們過分的追求效能,往往會讓整個專案進入死衚衕
。
大家可以去看看我之前寫的一篇部落格
元件化分包合包方案的坑