目錄
組件化
最近幾天在整理項目中的要點,組件化相信大家都不陌生,還是復用以前的一張項目架構圖,可以看到,項目的架構目前看起來比較清晰了,在最下層沉淀的是我們的公共庫,比如網絡庫
,圖片庫
,工具類
,......
等等
上層的業務,比如短視頻模塊
,分享模塊
,直播間模塊
等等,彼此直接并不會相互依賴,但是今天想說的是解耦
的問題
一個需求引發的思考
由于公司另外一個項目組需要使用我們的核心功能,比如直播間
,短視頻
等業務模塊,其他的會砍掉,當然目前筆者已經踩坑過了關于多組件分包合包的方案了
現在問題來了,另外一個組是手機電視
類的項目,它們的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合包
的時候,對我們的主業務也沒有任何侵入性
,擴展性很強,當然有的人可能認為,反射會影響一定的性能,但是怎么說呢?首先這個反射并不是平凡調用,我們在內部會有緩存實例的機制,第二點,我覺得在架構方面,性能可以適當的給擴展性讓一讓步,很多時候我們過分的追求性能,往往會讓整個項目進入死胡同
。
大家可以去看看我之前寫的一篇博客
組件化分包合包方案的坑