从去年开始,就陆陆续续的越来越多的app开始进行了组件化重构。也有很多非常好的组件化方案博客分享,所以这篇文章并不以介绍组件化方案作为主题,而是我们应该如何一步步的从一个古老的项目,慢慢一步步拆分,完成组件化重构的。
组件化的思想是好的,但是并不是所有的项目都适合使用组件化的方式进行开发,所以一般需要使用组件化的项目。基本都是具备项目迭代时期久远、项目大而臃肿、项目组成员多沟通成本大、项目复杂维护成本很高等特点。这类的项目才会有组件化的用武之地。
而其他的一些人员少、功能简单的小项目。就别去直接考虑组件化了。老老实实直接撸码就行了。强行使用组件化只会增加维护成本与开发成本。得不偿失~
组件化结构
组件化从来不是一个说重构就能重构的东西,在进行组件化重构之前。最好先对组件化的结构有一个基本的理解:
上图为组件化最基本的结构。大致可以看出。组件化主要分为三层:
-
app壳:
此为组件化的运行容器,壳中定义app入口,依赖业务组件进行运行。
-
业务组件:
此为组件化的中间层,在一个大型项目组中。都有细分下来的不同的业务组,比如管登录的、管购物的、管视频的等等。这些不同业务组分别维护一个各自的业务组件,以达到各自业务组业务解耦的效果。
原则上来说:各个业务组件之间不能有直接依赖!所有的业务组件均需要可以做到独立运行的效果。对于测试的时候,需要依赖多个业务组件的功能进行集成测试的时候。可以使用app壳进行多组件依赖管理运行。
-
基础组件:
基础组件也叫基础功能组件。此部分组件为上层业务组件提供基本的功能支持。如基础网络组件、基础视图组件、基础数据存取组件等,以及组件化的核心通信组件:路由组件。
以上即是组件化的最基本结构,当然在真正的项目之中,不可能会存在这么简单的结构,都是需要根据你的具体现状进行扩充的。比如你可以在基础组件与业务组件之间,添加一层特殊的功能组件。此层的功能组件只被一个或者多个组件进行依赖,只要不破坏这层由下到上单向依赖链即可:
准备
组件化重构从来都不是说重构就能重构的,首先得有个强有力的领导去支持执行,然后你才可能去具体的进行重构。
其次,你得提前对你们的项目进行大方向的分层结构划分,哪些东西需要放在什么层。需要提前有个明确的划分。
重构你的基础组件:即你的各种基础功能框架需要提前从项目中拆分出来。包括网络、图片加载、数据存储、埋点、路由等。
建立组件化项目结构
建立基础组件合集
你需要创建一个基础library module。用于依赖所有的基础功能组件,如baselib。
作用:用于统一依赖基础功能库,并统筹、关联好各功能框架的关系,做好各功能库的初始化封装操作。提供上层业务组件直接调用。
创建各自业务线的业务组件及app壳
与普通的组件化方案做法不同,普通的组件化方案是使用一个变量进行控制。使得业务组件可以在application与library之间进行灵活切换。使得组件也是application。application也是组件。
但是这种做法,因为总是在libraryModule与applicationModule之间进行切换。很容易导致各种混乱问题:比如Manifest冲突,R文件冲突等。
所以我们采用的是多app壳分组加载的方式:
可以看到,每个业务线的业务组件。都分别有一个各自的app壳模块。而主app壳依赖所有的业务组件. 在进行业务开发时。各自业务组成员可以直接运行各自的app壳模块进行测试,主app壳进行全量打包。
在拆分初期,这个时候的建议以原本的项目application作为主app壳。
预留一个核心业务组件出来。比如登录组件:此类组件为业务组件,但是又被所有其他组件所需要,所以将其单独作为核心业务组件独立出来。然后别的业务组件。通过各自的app壳工程。依赖进入即可:再次提醒业务组件之间不能直接进行依赖:
这种分层结构的好处有:
- 业务组件不再在library与application之间进行切换。开发环境统一,不易出现环境切换冲突
- 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等。