关于组件化解耦的架构设计思考

目录

组件化

最近几天在整理项目中的要点,组件化相信大家都不陌生,还是复用以前的一张项目架构图,可以看到,项目的架构目前看起来比较清晰了,在最下层沉淀的是我们的公共库,比如网络库图片库工具类......等等

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阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容