聚美组件化实践之路

从去年开始,就陆陆续续的愈来愈多的app开始进行了组件化重构。也有不少很是好的组件化方案博客分享,因此这篇文章并不以介绍组件化方案做为主题,而是咱们应该如何一步步的从一个古老的项目,慢慢一步步拆分,完成组件化重构的。java

组件化的思想是好的,可是并非全部的项目都适合使用组件化的方式进行开发,因此通常须要使用组件化的项目。基本都是具有项目迭代时期久远、项目大而臃肿、项目组成员多沟通成本大、项目复杂维护成本很高等特色。这类的项目才会有组件化的用武之地。android

而其余的一些人员少、功能简单的小项目。就别去直接考虑组件化了。老老实实直接撸码就好了。强行使用组件化只会增长维护成本与开发成本。得不偿失~git

组件化结构

组件化历来不是一个说重构就能重构的东西,在进行组件化重构以前。最好先对组件化的结构有一个基本的理解:github

上图为组件化最基本的结构。大体能够看出。组件化主要分为三层:json

  1. app壳:api

    此为组件化的运行容器,壳中定义app入口,依赖业务组件进行运行。安全

  2. 业务组件:cookie

    此为组件化的中间层,在一个大型项目组中。都有细分下来的不一样的业务组,好比管登陆的、管购物的、管视频的等等。这些不一样业务组分别维护一个各自的业务组件,以达到各自业务组业务解耦的效果。网络

    原则上来讲:各个业务组件之间不能有直接依赖!全部的业务组件均须要能够作到独立运行的效果。对于测试的时候,须要依赖多个业务组件的功能进行集成测试的时候。可使用app壳进行多组件依赖管理运行。app

  3. 基础组件:

    基础组件也叫基础功能组件。此部分组件为上层业务组件提供基本的功能支持。如基础网络组件、基础视图组件、基础数据存取组件等,以及组件化的核心通讯组件:路由组件。

以上便是组件化的最基本结构,固然在真正的项目之中,不可能会存在这么简单的结构,都是须要根据你的具体现状进行扩充的。好比你能够在基础组件与业务组件之间,添加一层特殊的功能组件。此层的功能组件只被一个或者多个组件进行依赖,只要不破坏这层由下到上单向依赖链便可

准备

组件化重构历来都不是说重构就能重构的,首先得有个强有力的领导去支持执行,而后你才可能去具体的进行重构。

其次,你得提早对大家的项目进行大方向的分层结构划分,哪些东西须要放在什么层。须要提早有个明确的划分。

重构你的基础组件:即你的各类基础功能框架须要提早从项目中拆分出来。包括网络、图片加载、数据存储、埋点、路由等。

创建组件化项目结构

创建基础组件合集

你须要建立一个基础library module。用于依赖全部的基础功能组件,如baselib。

做用:用于统一依赖基础功能库,并统筹、关联好各功能框架的关系,作好各功能库的初始化封装操做。提供上层业务组件直接调用。

建立各自业务线的业务组件及app壳

与普通的组件化方案作法不一样,普通的组件化方案是使用一个变量进行控制。使得业务组件能够在application与library之间进行灵活切换。使得组件也是application。application也是组件。

可是这种作法,由于老是在libraryModule与applicationModule之间进行切换。很容易致使各类混乱问题:好比Manifest冲突,R文件冲突等。

因此咱们采用的是多app壳分组加载的方式:

能够看到,每一个业务线的业务组件。都分别有一个各自的app壳模块。而主app壳依赖全部的业务组件. 在进行业务开发时。各自业务组成员能够直接运行各自的app壳模块进行测试,主app壳进行全量打包。

在拆分初期,这个时候的建议以本来的项目application做为主app壳

预留一个核心业务组件出来。好比登陆组件:此类组件为业务组件,可是又被全部其余组件所须要,因此将其单独做为核心业务组件独立出来。而后别的业务组件。经过各自的app壳工程。依赖进入便可:再次提醒业务组件之间不能直接进行依赖

这种分层结构的好处有:

  1. 业务组件再也不在library与application之间进行切换。开发环境统一,不易出现环境切换冲突
  2. app壳单独独立出来。能够在壳工程中添加一些特有的独立代码,因为各自的壳功能不会参与到主app壳中去进行编译,全部这里面你能够针对各自的业务。添加一些独立的入口管理类。好比添加一个RootActivity,在此添加一个能够跳转到任意页面的列表,方便进行测试运行等。

gradle统一配置管理

组件化重构后,module变多了,因此就须要对全部module的一些gradle脚本进行统一配置管理。避免混乱。

  • 新建dependencies.gradle脚本。添加统一的依赖版本号管理:
ext {
    COMPILE_SDK_VERSION = 25
    BUILD_TOOLS_VERSION = '25.0.0'
    MIN_SDK_VERSION = 16
    TARGET_SDK_VERSION = 19
    
    // SUPPORT
    SUPPORT_VERSION = '23.2.0'
    SUPPORTDEPS = [
            supportV4 : "com.android.support:support-v4:${SUPPORT_VERSION}",
            supportV13 : "com.android.support:support-v13:${SUPPORT_VERSION}",
            appcompatV7 : "com.android.support:appcompat-v7:${SUPPORT_VERSION}",
            cardview : "com.android.support:cardview-v7:${SUPPORT_VERSION}",
            design : "com.android.support:design:${SUPPORT_VERSION}",
            annotations : "com.android.support:support-annotations:${SUPPORT_VERSION}",
            multidex : 'com.android.support:multidex:1.0.1'
    ]
    ...
}
复制代码

此脚本统一配置管理全部的版本号相关的数据。外部须要使用版本号及依赖时,须要统一今后文件配置属性中进行读取。好比要依赖supportV4包:

compile "${SUPPORTDEPS.supportV4}"
复制代码
  • 定义baseConfig.gradle。统一配置组件基础编译脚本
boolean isAppModule = project.plugins.hasPlugin('com.android.application')
android {
    compileSdkVersion Integer.parseInt("${COMPILE_SDK_VERSION}")
    buildToolsVersion "${BUILD_TOOLS_VERSION}"

    lintOptions {
        abortOnError false
    }
    defaultConfig {
        if (isAppModule) {
            applicationId "com.haoge.component.demo"
        }
        minSdkVersion Integer.parseInt("${MIN_SDK_VERSION}")
        targetSdkVersion Integer.parseInt("${TARGET_SDK_VERSION}")

        versionCode Integer.parseInt("${DEFAULE_CONFIG.versionCode}")
        versionName "${DEFAULE_CONFIG.versionName}"
    }
}
复制代码

这样。就可使用apply语法。让全部组件module。都统一依赖此gradle脚本。进行统一环境配置了。

细心点的能够发现。我在baseConfig中,添加了默认的applicationId的指定。这是由于对于大部分应用而言。都有用过各类的第三方sdk。特别是第三方登陆,这种的sdk框架。不少都会须要进行包名验证的,因此建议有此种状况的,在此添加上默认的applicationId指定较好。

若是有嫌麻烦的又动手能力强的。能够考虑本身封装个gradle插件来进行统一配置管理

为组件添加资源前缀

咱们须要对各自的组件,分别设置他自身的资源前缀来做为命名约束,避免出现不一样的组件对不一样的资源起了同一个命名,致使编译冲突等问题。

android {
    resourcePrefix 'lg_'
}
复制代码

这个资源前缀的做用是:当你在该module下建立了一个资源命名时,若名字不能与此前缀进行匹配,则将会进行即时提醒。避免冲突。

大文件资源、图片资源统一管理

组件化以后。资源管理也是个问题,图片资源、assets资源、raw文件资源等。都具备占用资源大、基本不多修改等特色。因此这里最好将其单独拆分出来。统一提供给全部组件进行使用:

因此,能够考虑将此类大文件资源,统一放入组件化的最底层。使得不一样组件不用本身单独维护一份此大文件资源。避免资源浪费的现象。好比能够直接将此部分资源。直接放入baselib中,做为基础功能提供库进行使用。

作好各组件的application派发

可能有人会问:为何要作组件的application的生命周期派发?

举个栗子:都知道。网络库、图片加载库等,都须要进行对应的初始化操做才能进行使用的,可是在组件化中,若是不进行各自application的派发。不能进行一个统一流程的初始化操做。那么可能你组件A须要本身手动写基础库的初始化操做。组件B、组件C也须要。最后你的主app壳也须要,这个时候。就容易乱了!

因此须要有个结构。来让各自的组件。分别完成自身的组件的功能初始化。

好比基础功能组件:初始化网络、图片框架等,上层的业务组件A,初始化自身的其余功能操做。各自的组件分别只初始化自身这部分的操做。而不用管所依赖的其余组件须要进行什么初始化。

这部分的生命周期派发能够参考demo中的baselib的delegate包下的类:

demo连接放在了文章末尾。

组件间通讯

路由通讯

组件间通讯的核心是路由框架,这部分框架须要放置在最底层的基础功能组件中,提供上层进行使用,这里我使用的是我本身的路由框架Router:一款单品、组件化、插件化全支持的路由框架

此路由框架支持在单品、组件化、插件化中均能使用。若是你想要为组件化以后,能在后期有须要的状况下,方便的从组件化切换到插件化的环境中去,建议使用此Router

若是大家项目中已经有使用本身的路由框架,且也直接支持组件化环境使用。建议这块就最好别考虑换了。实话说换一个路由框架任务仍是挺重的。

由于基本全部的介绍组件化的blog,都对其中的路由框架,作了很是详细的说明,因此这块我就不许备展开进行详细的赘述了,若有感兴趣的,能够参考上方的连接进行了解使用。

事件通讯

与路由通讯不一样的是:路由主要用于作界面跳转通讯,对于普通的事件通讯做用不大。好比说我是组件A,须要调组件B中的某个接口,并获取返回数据进行操做。这个时候,就须要别的方式来进行实现了。

不少人一说到事件通讯。可能就会想起使用EventBus了。的确EventBus是个很好的事件通讯框架,可是相信用过的人都知道。一旦EventBus被滥用。随着时间的迭代,因为其独特的解耦特性,会使得你的代码很难进行调试、维护。

因此这个时候,咱们摒弃了使用EventBus来做为组件间时间通讯的桥梁。而是简单的使用控制反转的手段。将组件间通讯协议定义在底层基础组件中,上层的业务组件分别实现底层对应的各自的协议接口来进行通讯。

咱们以登陆组件为例:

首先,在基础组件层添加一个协议接口。这个接口用于定义登陆组件所对外提供的时间通讯入口,好比退出登陆、清理cookie等:

public interface LoginPipe extends Pipe{
    void logout();
    void clearCookie();
}
复制代码

而后。在登陆组件中。实现此协议接口。并注册入对应的通讯管理器:

// 实现协议接口。
public final class LoginPipeImpl implements LoginPipe {
    @Override
    void logout() {
        // do something
    }
    
    void clearCookie() {
        // do something
    }
}
复制代码
// 注册此实现进协议管理器中
// PipeManager也位于基础组件中。
PipeManager.register(LoginPipe.class, new LoginPipeImpl());
复制代码

而后便可在别的组件中。经过此PipeManager协议管理器。根据协议类。获取到对应的实现类进行直接调用了:

PipeManager.get(LoginPipe.class).logout();
复制代码

上面这种作法,虽然的确很简单,可是具有如下几点优势:

  • 提升各组件协议的内聚性。更适于各自组件对各自的协议接口进行统一管理维护。
  • 实现方案简单易懂,易于调试。
  • 在组件化拆分进程中,便于方便后期对主app壳无关代码进行删除。

最后一条可能相对比较复杂一点。因此下面咱们针对这条进行展开描述:

上面咱们提到了。在对老旧项目进行组件化重构的时候。使用主module做为的主app壳,而app壳实际上是须要没有具体的业务代码的。因此这个地方存在冲突。可是咱们组件化拆分也不是能够一蹴而就的,只能慢慢一步步、一个页面一个页面的进行拆分并测试。因此拆分过程实际上是个漫长的痛苦的过程。

而在拆分过程当中,很难避免的就是新旧代码均须要同时存在的尴尬场面。而这种尴尬的场面会一直持续到全部组件均拆分完毕以后。

而后拆分过程当中,你也会遇到另一个问题:就是各业务组的拆分计划实际上是不一样步的,也就是说极可能你当前拆的业务。须要调用到别的业务组的功能,而这个功能这个时候。极可能还根本没有被提交到拆分计划表上来。因此这个时候。你就必需要在你拆分的组件中,仍是先直接调用老项目中的逻辑代码。

因此使用上面的事件通讯机制。你会须要在主app中建议一个临时的协议接口。好比:

public interface MainPipe {
    void doSomething();
}
复制代码

而后主项目实现并注册它。提供给你的组件进行使用。而其余组件遇到此种相似问题时,也于此相似。在此MainPipe种继续添加对应的通讯协议方法并实现便可。

因为这样的作法。将全部的主app的临时协议接口。均放置于此MainPipe中。提高了协议的内聚性。当全部业务组均完成组件化重构以后。那么就能够统一的直接对此MainPipe进行重构,将其中各自组件的协议迁移至各自组件的协议类中,而后就能够安全地进行主app中无关业务代码统一删除了。使其成为真正的主app壳工程。

数据通讯

不少时候,其实组件间通讯。传递的数据都是普通的简单数据,可是也有一些时候。会须要传递复杂数据。好比进行跨组件调用api接口并获取返回数据时,或者说读取用户完整数据时。

以读取用户完整数据为例,数据通讯的协议定义仍旧以上方的事件通讯机制做为实现载体:

public class User {
    String uid;
    String nickname;
    String email;
    String phone;
    ...
}
复制代码

这个User类包含了全部的用户信息在里面。而后如今须要将此user实例进行跨组件传递时。你就须要定义一个协议方法。提供获取此User实例的入口:

public interface LoginPipe {
    User getUser();
}
复制代码

这是正常的作法,可是这样作的话,你就须要将此User实例也一块儿拷贝到协议定制层,即基础组件中来。

而在开发过程当中,这种现象很常见。并且不少时候,随着需求一更改,所须要传递的数据也不同。也不可能每次都去将对应的实体bean进行迁移,放入协议定制层。这样就太麻烦了。

因此对于这种跨组件通讯的作法。建议的方式是经过json数据来进行数据通讯

json通讯的机制,便可完美的避免实体bean迁移的问题。也能让接收方按需解析读取数据:

好比我接收方的组件。当前只须要nickname与uid两个数据。其余数据我无论。那么我就能够只解析此两个字段的数据便可。作到按需解析。

说到这里。推荐一波个人另外一个框架Parceler, 此框架是封装的Bundle的存取操做。也支持json的自动转换功能。具体用法能够参考我另外一篇博客,有兴趣的能够看看。

Parceler: 优雅的使用Bundle进行数据存取就靠它了!(文章最后有关于组件化、插件化下应该如何使用此框架的说明)

优化加速

随着组件化拆分重构的进行。你会发现项目下的组件被拆分得愈来愈多,虽然你已经对组件的拆分粒度。进行过把控了。可是组件化后module持续增长是不争的事实,这个时候。随着module的持续增长。你的项目编译时间也会出现暴涨。

咱们知道。项目编译流程中,第一步会将全部的library module先进行打包编译。生成对应的aar。提供给app进行使用,app等待全部module打包完毕后,再解压aar。进行资源、代码合并,并打包成apk执行运行。

因此咱们制做了一个gradle加速插件。用于提早将module进行aar编译好。跳过module打包aar的过程。实现编译加速的效果。

具体原理能够参考这篇文章:Speedup:专为项目下Library project过多所设计的加速插件

更多小贴士

由于在组件化开发环境下,你将会遇到的问题远远不止以上这么点,固然上面这些都是很主要的。

因此这里添加此小贴士环节,用于添加一些平时咱们开发时。可能会遇到的问题。或者说,一些在特定环境下的编码建议之类的。(这些要点极可能不能在demo中获得体现,因此请尽可能认真看下描述)

巧用ActivityLifecycleCallbacks作初始化

由于组件化有个特色: 各自业务组能够任意选择本身的开发模式,如mvp,mvvm,RN等。

Android组件化demo