1. 程式人生 > >元件化解耦的架構設計思考

元件化解耦的架構設計思考

目錄

元件化

最近幾天在整理專案中的要點,元件化相信大家都不陌生,還是複用以前的一張專案架構圖,可以看到,專案的架構目前看起來比較清晰了,在最下層沉澱的是我們的公共庫,比如網路庫圖片庫工具類......等等

上層的業務,比如短視訊模組分享模組直播間模組等等,彼此直接並不會相互依賴,但是今天想說的是解耦的問題

一個需求引發的思考

由於公司另外一個專案組需要使用我們的核心功能,比如直播間短視訊等業務模組,其他的會砍掉,當然目前筆者已經踩坑過了關於

多元件分包合包的方案

現在問題來了,另外一個組是手機電視類的專案,它們的App內部已經有依賴ijkplayer實現的播放器了,但是我們內部使用的是阿里雲播放器,當然了直接合並使用我們的一整套短視訊業務模組,也沒有問題,但是無形當中會大幅增加apk包的體積(由於兩者下層都是基於ffmeng庫封裝的),相當於一個應用內重複包含了幾個播放庫,那能不能複用同一套呢?換句話說,能否實現我們的專案編譯打包apk的時候,載入的是阿里雲播放器的實現類,而給其他專案組合包成aar之後,他們載入自己的ijkplayer實現類呢?

業務與實現分離

以最典型的短視訊模組為例子,開發階段,新建兩個module

,分別對應video業務模組和video-impl播放器實現類模組,讓video-impl元件只依賴common元件和video業務元件,然後讓video-implapplication的方式執行,開發。

筆者這裡簡化了專案模型,但是基本原理是一致的。

在我們自己的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合包的時候,對我們的主業務也沒有任何侵入性,擴充套件性很強,當然有的人可能認為,反射會影響一定的效能,但是怎麼說呢?首先這個反射並不是平凡呼叫,我們在內部會有快取例項的機制,第二點,我覺得在架構方面,效能可以適當的給擴充套件性讓一讓步,很多時候我們過分的追求效能,往往會讓整個專案進入死衚衕

大家可以去看看我之前寫的一篇部落格
元件化分包合包方案的坑

模擬元件解耦
https://github.com/byhook/module-design