關于組件化解耦的架構設計思考

目錄

組件化

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

image

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

一個需求引發的思考

由于公司另外一個項目組需要使用我們的核心功能,比如直播間短視頻等業務模塊,其他的會砍掉,當然目前筆者已經踩坑過了關于多組件分包合包的方案

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

業務與實現分離

以最典型的短視頻模塊為例子,開發階段,新建兩個module,分別對應video業務模塊和video-impl播放器實現類模塊,讓video-impl組件只依賴common組件和video業務組件,然后讓video-implapplication的方式運行,開發。

筆者這里簡化了項目模型,但是基本原理是一致的。

image

在我們自己的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,筆者這里為了簡化,直接使用系統類來實現的,看下圖比較直觀:

image

但是有個新問題,那就是我們的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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容