Android 组件化之路

首先先分清楚两个概念:android

模块化

模块化编程是将一个程序按照功能拆分红相互独立的若干模块,它强调将程序的功能分离成独立的、可替换的模块。每一个模块内只有与其相关功能的内容。web

模块化编程和结构化编程与面向对象编程是密切相关的,它们的目的都是将大型软件程序划分红一个个更小的部分。模块化编程的粒度会更“粗”一些。在Java9中也在编译器层面提供了模块化的支持:Java Platform Module System 。面试

组件是一个相似的概念,但一般指更高的级别;组件是整个系统的一部分,而模块是单个程序的一部分。“模块”一词因语言而有很大差别;在Python中,它很是小,每一个文件都是一个模块,而在Java 9中,它是很是大的,其中模块是包的集合,包又是文件的集合。编程

在面向对象编程中,一般使用接口做为模块间通讯的桥梁,也就是基于接口的编程。安全

组件化

组件化开发是软件工程的一个分支,它强调对给定软件系统中普遍可用的功能进行分割。基于可重用的目的将一个大的软件系统拆分红多个独立的组件,减小系统耦合度。性能优化

组件化开发中认为组件做为系统的一部分,是可独立运行的服务,维基百科中举了一个例子:在web服务中,有一种面向服务的架构设计--service-oriented architectures (SOA),这种架构设计从业务角度出发,利用企业现有的各类软件体系,从新整合并构建起一套新的软件架构。这套软件架构能够随着业务的变化,随时灵活地结合现有服务,组成一个新的软件。增长应用系统的灵活性。服务器

组件能够产生或者消费事件,也能够应用于事件驱动架构。网络

  • 组件之间经过接口进行通讯
  • 组件是可替换的,若是后续组件知足初始组件的需求(经过接口表示),则组件能够替换另外一个组件(在设计时或运行时),所以能够用更新的版本或替代的版本替换组件,而不会破坏系统的运行。
  • 一个判断可替换组件的经验法则是:若是组件B至少提供了A提供的组件,而且使用的组件不超过A使用的组件,那么组件B能够当即替换组件A
  • 当组件直接与用户交互时,应该考虑基于组件的可用性测试。
  • 组件须要是彻底文档化、全面测试、具备全面的输入效度检查的。

模块化 or 组件化

无论是模块化仍是组件化,都不是一个新的设计思想,它们最先都是在20世纪60年代就已经被提出了,可是早期的移动应用因为相对简单,自己逻辑功能也很少,因此在移动端的应用反而没那么普遍。(虽然Java最开始的模块化是针对在移动和嵌入式设备上的应用设计的)。数据结构

从上面的概述来看其实组件化跟模块化没有明显的区别;一个登陆功能能够是一个模块也能够是一个组件,一个日期选择控件能够是一个模块,也能够是一个组件,由于无论是模块化仍是组件化,它们都有一个共同的目标:将一个大的软件系统细化成一个个模块或者组件,都是为了重用和解耦。所以没有一个明确的界线去区分它们。架构

网上不少文章对于组件和模块的定义也是不尽相同的,一些人认为组件的粒度更细,它只是具有单一功能与业务无关的组件,好比一个日历选择控件就认为是一个组件。而模块他们认为就是业务模块,顾名思义,就是按业务划分而成的模块。而另外一部分人则相反。

在维基百科对模块化的解释中有这么一句:

A component is a similar concept, but typically refers to a higher level; a component is a piece of a whole system, while a module is a piece of an individual program

也就是认为组件粒度较模块要更大,因此本文对组件和模块作出如下定义:

  • 组件:侧重于业务,可编译成单独的app,通常只负责单一业务,具有自身的生命周期(一般包含Android四大组件的一个或多个,因此称之为组件也更加贴切)
  • 模块:侧重于功能,与业务无关,好比自定义控件、网络请求库、图片加载库等

而从Android Studio推出以后,咱们在开发项目时也会有意识的将一些可重用的代码逻辑抽离成一个个的Module,这也就是模块化开发的雏形。固然,组件化开发也不是就尽善尽美的,下面列举了它的一些优缺点:

优势:一个复杂的系统能够由一个个组件集合而成,甚至于不一样的组合能够构建出不一样的系统。每一个组件有独立的版本,可独立编译、打包,大大提升了系统的灵活性以及开发人员的开发效率。应用的更新能够精细到组件,组件的升级替换不会影响到其它组件,也不会受其它组件的限制。

基于组件化架构设计的应用比传统的“单片”设计可重用性高得多,由于这些组件能够在其余项目中重用,并且开发人员无需了解整个应用,能够只专一于分配给他们的较小的任务,提升开发效率。

缺点:组件化的实施对开发人员和团队管理人员提出了更高水平的要求,项目管理难度更大。组件间如何进行通讯也是须要慎重考虑的。万事开头难,在对一个项目进行组件化分解时就好像庖丁解牛通常,你须要了解项目的“肌理筋骨”,才知道从何处下“刀”,才能更轻易的去分解项目,这就要求架构师对于项目的总体需求了如指掌。

下面就来谈谈个人组件化之路。。。

首先我负责的项目相似于一个远程控制应用,它与服务器创建Socket链接,接收服务器发送过来的指令,针对这些指令对当前Android设备执行关机、安装应用等操做。应用自己也会收集一些设备信息如应用运行日志,使用时长等,在某个指定的时间点上传至服务器。理想的组件间依赖关系是这样的:

其中基础模块不能脱离主工程独立运行,组件之间不能直接依赖,组件间通讯方式能够是接口也能够是事件总线。团队中的开发人员只须要关注自身负责的组件(在开发模式下各组件会转化为可单独运行的App,说白了就是在build.gradle文件中将apply plugin: 'com.android.library'改成apply plugin: 'com.android.application',网上有不少相关资料,在此就不赘述了)。

如今来了个开发需求须要改动组件Component1内部的逻辑,团队中的小A是负责该组件的开发人员,在接到需求后,小A啪啪啪一顿猛如虎的操做完成需求后,对该组件进行单元测试,检查组件输入输出,测试经过后提交代码,审核经过后构建平台构建、打包、发布,整个过程彻底没有“惊动”其余组件,Perfect!

然而现实是残酷的。。

因为组件间不可能彻底不通讯,因此现实状况组件之间的依赖关系有多是这样的:

对比上图,组件之间显得更加“亲密无间”了,并且这还不是糟糕的状况,当组件愈来愈多,各类相互依赖,循坏依赖的问题会让你痛不欲生。

由于组件之间不可避免的存在须要通讯的状况,好比 Component1须要调用Component2的方法通常状况下咱们都是直接经过类名或对象引用的方式去调用相应的方法。可是这种通讯方式正是致使组件之间高度耦合的罪魁祸首,因此必须杜绝这种通讯方式。

那么问题来了,怎么作到既能让组件间通讯又高度解耦呢?这就须要用到文章开头提到的面向接口编程思想和依赖注入(或者叫依赖查找)技术。举个🌰:

组件A中的Foo1类依赖组件BFoo2类中的bar方法,一种比较low的实现方式是:

//ComponentA
class Foo1 {
    private Foo2 mFoo2;
    public void main() {
        mFoo2 = new Foo2();
        mFoo2.bar();
    }
}

//ComponentB
class Foo2 {
    public void bar() {
        //nop
    }
}

这种实现方式违反了控制反转设计原则,耦合度高,假如这时需求变动了,须要使用组件C的Foo3类中的bar()方法去替换原来的实现,那这下乐子就大了。

而经过面向接口编程以及依赖注入技术咱们能很好的遵循控制反转设计原则:

//Common Component
interface IBar {
    void bar()
}

//ComponentA
class Foo1 {
    private IBar mBar;

    public void main() {
        if (mBar != null) {
            mBar.bar();
        }
    }

    public void setBar(IBar bar) {
        mBar = bar;
    }
}

//ComponentB
class Foo2 implements IBar {

    @Override
    public void bar() {
        //nop
    }
}

//ComponentC
class Foo3 implements IBar {

    @Override
    public void bar() {
        //nop
    }
}

这就是经典的实现了控制反转的示例代码,Foo1类只知道本身须要一个实现了IBar接口的实例,而后调用接口的bar()方法,至因而谁去实现的这个接口,很差意思,它压根不关心。

虽然你Foo1类是舒服了,把依赖关系交给外部去解决了,可是总要有人去负责这部分的工做吧。这时候依赖注入容器(IOC容器)就登场了,若是对web开发有所了解的同窗确定不会感到陌生,Spring就是一个IOC容器,这个容器把依赖查找,类实例化(其实就是根据类的路径名称经过反射进行实例化)这些脏活累活揽在身上,这样既实现了控制反转又极大提升了应用的灵活性和可维护性。

正由于依赖注入能有效地下降代码之间的耦合度,因此基于依赖注入实现的组件化框架(路由框架)也就应运而生了,目前主流的Android组件化框架有ARouter、CC、DDComponentForAndroid、ActivityRouter等等,我本身也使用Kotlin基于kapt技术实现了一个路由框架KRouter。

虽然相关的框架有不少,可是它们实现原理不外乎两种:

  • 一种是将分布在各个组件的类按照必定的规则在内部生成映射表,这个映射表的数据结构一般是一个Map,Key是一个字符串,Value是一个类或者是类的路径名称(用于经过反射进行类的实例化)。通俗来讲就是类的查找,这种实现方式要求调用方和被调用方都持有接口类,一般这些共同持有的接口类会被定义在一个Common基础模块中,并且在运行时这些相互通讯的组件必须打包到同一个APK中。这种实现方式致使没法真正实现代码隔离(须要通讯的两个组件仍然是存在依赖关系的),基于这种原理实现的组件化架构“自约束能力”很弱,由于没法约束开发人员经过直接引用的方式进行通讯的行为,虽然一开始设计人员想的很美好,可是开发人员在实现时作出来的产品却不是那样,由于“自约束能力”弱的架构设计是经过“编码规范”、“测试驱动”甚至是“人员熟练度”来保证开发人员实现的代码符合设计人员的设计初衷,并且这种架构也没法保证后续接手维护项目的开发人员可以贯彻本来的设计思想,随着时间推移,项目往愈来愈糟糕的方向演进(解决这个问题最好的方案就是从编译器层面进行约束,也就是把问题拦截在编码阶段,然而Java9才支持模块化开发,Android目前还处于支持部分Java8的特性的阶段,路还很长)。
  • 另外一种方案是基于事件总线的方式实现组件之间的通讯,再也不是面向接口编程,而是面向通讯协议编程,能够理解为组件间的调用相似http请求。这些框架会在内部创建跨进程通讯的链接(也就是事件总线),这条事件总线负责分发路由请求以及返回执行结果。这种实现方式的好处是真正能够实现代码隔离,组件能够运行在独立的进程中,可是只支持基本类型参数的转发。实现跨进程通讯有不少方案,好比Android原生的四大组件、Socket、FileObserver、MemoryFile、基于AIDL的Messager等等,使用Android原生的好处是安全性方面的工做由Android帮咱们完成了,而使用Socket则须要本身实现加密Socket。

第一种方案适合小型的项目,由于这些项目一般都是单进程的,虽然这样设计的架构“自约束能力”弱,可是目前大多数Android项目团队开发人数也不会太多,因此管理难度较小,而第二种实现方案则更适合跨进程组件化的项目(组件通常运行在独立的进程中甚至一个组件就是一个APP)。

在我看来Android的组件化是存在3个阶段的,第一个是从单工程项目过分到多模块的阶段;第二个是从多模块过分到多组件的阶段;第三个就是多组件独立进程的阶段。而目前大多数应用其实都是在第二个阶段或者介于第二和第三个阶段之间,因此对于这样的项目,选择一个既支持类查找方式,又支持事件总线的组件化框架是最合适的(这也是一开始设计KRouter想要达到的效果,虽然目前暂时不支持跨进程组件。。。)

在项目实施组件化过程当中,其实真正耗费时间、精力的不是编码,而是一开始组件的划分以及组件单元测试的代码的编写。有可能由于一开始对业务的不熟悉,致使后期开发时发现组件划分的不够准确,须要加以调整;或者是对接口抽象的不够好,致使维护时频繁修改接口;还有可能在编写单元测试时以为枯燥乏味而选择放弃。咱们不能由于遇到这些困难就半途而废,或者是质疑本身的架构设计能力,没有哪个架构设计是放之四海皆准的,有可能一个项目的架构设计放在另外一个项目中就显得不那么合适了。

因此好的架构设计还须要设计人员“因地制宜”的对一个比较通用的架构骨架进行查漏补缺,最后使其与实际项目更加契合。

祝你们都能成为一个优秀的架构设计师。

免费获取安卓开发架构的资料(包括Fultter、高级UI、性能优化、架构师课程、 NDK、Kotlin、混合式开发(ReactNative+Weex)和一线互联网公司关于android面试的题目汇总能够加:936332305 / 连接:点击连接加入【安卓开发架构】

相关文章
相关标签/搜索