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

目录

  • 组件化
  • 一个需求引发的思考
  • 业务与实现分离
  • 为啥要这么做呢?
  • 组件之间的通信
  • 小结

组件化

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

组件化解耦的架构设计思考_第1张图片

上层的业务,比如短视频模块分享模块直播间模块等等,彼此直接并不会相互依赖,但是今天想说的是解耦的问题

一个需求引发的思考

由于公司另外一个项目组需要使用我们的核心功能,比如直播间短视频等业务模块,其他的会砍掉,当然目前笔者已经踩坑过了关于多组件分包合包的方案了

现在问题来了,另外一个组是手机电视类的项目,它们的App内部已经有依赖ijkplayer实现的播放器了,但是我们内部使用的是阿里云播放器,当然了直接合并使用我们的一整套短视频业务模块,也没有问题,但是无形当中会大幅增加apk包的体积(由于两者下层都是基于ffmeng库封装的),相当于一个应用内重复包含了几个播放库,那能不能复用同一套呢?换句话说,能否实现我们的项目编译打包apk的时候,加载的是阿里云播放器的实现类,而给其他项目组合包成aar之后,他们加载自己的ijkplayer实现类呢?

业务与实现分离

以最典型的短视频模块为例子,开发阶段,新建两个module,分别对应video业务模块和video-impl播放器实现类模块,让video-impl组件只依赖common组件和video业务组件,然后让video-implapplication的方式运行,开发。

笔者这里简化了项目模型,但是基本原理是一致的。

组件化解耦的架构设计思考_第2张图片

在我们自己的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,笔者这里为了简化,直接使用系统类来实现的,看下图比较直观:

组件化解耦的架构设计思考_第3张图片

但是有个新问题,那就是我们的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> loadPluginMapping() {
        Map, 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

你可能感兴趣的:(组件化解耦的架构设计思考)