本文提出的组件化方案demo已经开源,参见文章Android彻底组件化方案开源

文末有罗辑思维“得到app”的招聘广告,欢迎各路牛人加入!!

一、模块化、组件化与插件化

  项目发展到一定程度,随着人员的增多,代码越来越臃肿,这时候就必须进行模块化的拆分。在我看来,模块化是一种指导理念,其核心思想就是分而治之、降低耦合。而在Android工程中如何实施,目前有两种途径,也是两大流派,一个是组件化,一个是插件化。

  提起组件化和插件化的区别,有一个很形象的图:



  上面的图看上去比较清晰,其实容易导致一些误解,有下面几个小问题,图中可能说的不太清楚:

  • 组件化是一个整体吗?去了头和胳膊还能存在吗?左图中,似乎组件化是一个有机的整体,需要所有器官都健在才可以存在。而实际上组件化的目标之一就是降低整体(app)与器官(组件)的依赖关系,缺少任何一个器官app都是可以存在并正常运行的。
  • 头和胳膊可以单独存在吗?左图也没有说明白,其实答案应该是肯定的。每个器官(组件)可以在补足一些基本功能之后都是可以独立存活的。这个是组件化的第二个目标:组件可以单独运行。
  • 组件化和插件化可以都用右图来表示吗?如果上面两个问题的答案都是YES的话,这个问题的答案自然也是YES。每个组件都可以看成一个单独的整体,可以按需的和其他组件(包括主项目)整合在一起,从而完成的形成一个app
  • 右图中的小机器人可以动态的添加和修改吗?如果组件化和插件化都用右图来表示,那么这个问题的答案就不一样了。对于组件化来讲,这个问题的答案是部分可以,也就是在编译期可以动态的添加和修改,但是在运行时就没法这么做了。而对于插件化,这个问题的答案很干脆,那就是完全可以,不论实在编译期还是运行时!

      本文主要集中讲的是组件化的实现思路,对于插件化的技术细节不做讨论,我们只是从上面的问答中总结出一个结论:组件化和插件化的最大区别(应该也是唯一区别)就是组件化在运行时不具备动态添加和修改组件的功能,但是插件化是可以的。

      暂且抛弃对插件化“道德”上的批判,我认为对于一个Android开发者来讲,插件化的确是一个福音,这将使我们具备极大的灵活性。但是苦于目前还没有一个完全合适、完美兼容的插件化方案(RePlugin的饥饿营销做的很好,但还没看到疗效),特别是对于已经有几十万代码量的一个成熟产品来讲,套用任何一个插件化方案都是很危险的工作。所以我们决定先从组件化做起,本着做一个最彻底的组件化方案的思路去进行代码的重构,下面是最近的思考结果,欢迎大家提出建议和意见。

二、如何实现组件化

  要实现组件化,不论采用什么样的技术路径,需要考虑的问题主要包括下面几个:

  • 代码解耦。如何将一个庞大的工程拆分成有机的整体?
  • 组件单独运行。上面也讲到了,每个组件都是一个完整的整体,如何让其单独运行和调试呢?
  • 数据传递。因为每个组件都会给其他组件提供的服务,那么主项目(Host)与组件、组件与组件之间如何传递数据?
  • UI跳转。UI跳转可以认为是一种特殊的数据传递,在实现思路上有啥不同?
  • 组件的生命周期。我们的目标是可以做到对组件可以按需、动态的使用,因此就会涉及到组件加载、卸载和降维的生命周期。
  • 集成调试。在开发阶段如何做到按需的编译组件?一次调试中可能只有一两个组件参与集成,这样编译的时间就会大大降低,提高开发效率。
  • 代码隔离。组件之间的交互如果还是直接引用的话,那么组件之间根本没有做到解耦,如何从根本上避免组件之间的直接引用呢?也就是如何从根本上杜绝耦合的产生呢?只有做到这一点才是彻底的组件化。

2-1 代码解耦

  把庞大的代码进行拆分,Androidstudio能够提供很好的支持,使用IDE中的multiple module这个功能,我们很容易把代码进行初步的拆分。在这里我们对两种module进行区分,

  • 一种是基础库library,这些代码被其他组件直接引用。比如网络库module可以认为是一个library。
  • 另一种我们称之为Component,这种module是一个完整的功能模块。比如读书或者分享module就是一个Component。

      为了方便,我们统一把library称之为依赖库,而把Component称之为组件,我们所讲的组件化也主要是针对Component这种类型。而负责拼装这些组件以形成一个完成app的module,一般我们称之为主项目、主module或者Host,方便起见我们也统一称为主项目。

      经过简单的思考,我们可能就可以把代码拆分成下面的结构:



      这种拆分都是比较容易做到的,从图上看,读书、分享等都已经拆分组件,并共同依赖于公共的依赖库(简单起见只画了一个),然后这些组件都被主项目所引用。读书、分享等组件之间没有直接的联系,我们可以认为已经做到了组件之间的解耦。但是这个图有几个问题需要指出:

    ● 从上面的图中,我们似乎可以认为组件只有集成到主项目才可以使用,而实际上我们的希望是每个组件是个整体,可以独立运行和调试,那么如何做到单独的调试呢?

    ● 主项目可以直接引用组件吗?也就是说我们可以直接使用compile project(:reader)这种方式来引用组件吗?如果是这样的话,那么主项目和组件之间的耦合就没有消除啊。我们上面讲,组件是可以动态管理的,如果我们删掉reader(读书)这个组件,那么主项目就不能编译了啊,谈何动态管理呢?所以主项目对组件的直接引用是不可以的,但是我们的读书组件最终是要打到apk里面,不仅代码要和并到claases.dex里面,资源也要经过meage操作合并到apk的资源里面,怎么避免这个矛盾呢?

    ● 组件与组件之间真的没有相互引用或者交互吗?读书组件也会调用分享模块啊,而这在图中根本没有体现出来啊,那么组件与组件之间怎么交互呢?

      这些问题我们后面一个个来解决,首先我们先看代码解耦要做到什么效果,像上面的直接引用并使用其中的类肯定是不行的了。所以我们认为代码解耦的首要目标就是组件之间的完全隔离,我们不仅不能直接使用其他组件中的类,最好能根本不了解其中的实现细节。只有这种程度的解耦才是我们需要的。

2-2 组件的单独调试

  其实单独调试比较简单,只需要把apply plugin: 'com.android.library'切换成apply plugin: 'com.android.application'就可以,但是我们还需要修改一下AndroidManifest文件,因为一个单独调试需要有一个入口的actiivity。

  我们可以设置一个变量isRunAlone,标记当前是否需要单独调试,根据isRunAlone的取值,使用不同的gradle插件和AndroidManifest文件,甚至可以添加Application等Java文件,以便可以做一下初始化的操作。

  为了避免不同组件之间资源名重复,在每个组件的build.gradle中增加resourcePrefix "xxx_",从而固定每个组件的资源前缀。下面是读书组件的build.gradle的示例:

if(isRunAlone.toBoolean()){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}
.....
resourcePrefix "readerbook_"
sourceSets {
main {
if (isRunAlone.toBoolean()) {
manifest.srcFile 'src/main/runalone/AndroidManifest.xml'
java.srcDirs = ['src/main/java','src/main/runalone/java']
res.srcDirs = ['src/main/res','src/main/runalone/res']
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}

  通过这些额外的代码,我们给组件搭建了一个测试Host,从而让组件的代码运行在其中,所以我们可以再优化一下我们上面的框架图。

2-3 组件的数据传输

  上面我们讲到,主项目和组件、组件与组件之间不能直接使用类的相互引用来进行数据交互。那么如何做到这个隔离呢?在这里我们采用接口+实现的结构。每个组件声明自己提供的服务Service,这些Service都是一些抽象类或者接口,组件负责将这些Service实现并注册到一个统一的路由Router中去。如果要使用某个组件的功能,只需要向Router请求这个Service的实现,具体的实现细节我们全然不关心,只要能返回我们需要的结果就可以了。这与Binder的C/S架构很相像。

  因为我们组件之间的数据传递都是基于接口编程的,接口和实现是完全分离的,所以组件之间就可以做到解耦,我们可以对组件进行替换、删除等动态管理。这里面有几个小问题需要明确:

● 组件怎么暴露自己提供的服务呢?在项目中我们简单起见,专门建立了一个componentservice的依赖库,里面定义了每个组件向外提供的service和一些公共model。将所有组件的service整合在一起,是为了在拆分初期操作更为简单,后面需要改为自动化的方式来生成。这个依赖库需要严格遵循开闭原则,以避免出现版本兼容等问题。

● service的具体实现是由所属组件注册到Router中的,那么是在什么时间注册的呢?这个就涉及到组件的加载等生命周期,我们在后面专门介绍。

● 一个很容易犯的小错误就是通过持久化的方式来传递数据,例如file、sharedpreference等方式,这个是需要避免的。

  下面就是加上数据传输功能之后的架构图:

2-4 组件之间的UI跳转

  可以说UI的跳转也是组件提供的一种特殊的服务,可以归属到上面的数据传递中去。不过一般UI的跳转我们会单独处理,一般通过短链的方式来跳转到具体的Activity。每个组件可以注册自己所能处理的短链的schme和host,并定义传输数据的格式。然后注册到统一的UIRouter中,UIRouter通过schme和host的匹配关系负责分发路由。

  UI跳转部分的具体实现是通过在每个Activity上添加注解,然后通过apt形成具体的逻辑代码。这个也是目前Android中UI路由的主流实现方式。

2-5 组件的生命周期

  由于我们要动态的管理组件,所以给每个组件添加几个生命周期状态:加载、卸载和降维。为此我们给每个组件增加一个ApplicationLike类,里面定义了onCreate和onStop两个生命周期函数。

  1. 加载:上面讲了,每个组件负责将自己的服务实现注册到Router中,其具体的实现代码就写在onCreate方法中。那么主项目调用这个onCreate方法就称之为组件的加载,因为一旦onCreate方法执行完,组件就把自己的服务注册到Router里面去了,其他组件就可以直接使用这个服务了。
  2. 卸载:卸载与加载基本一致,所不同的就是调用ApplicationLike的onStop方法,在这个方法中每个组件将自己的服务实现从Router中取消注册。不过这种使用场景可能比较少,一般适用于一些只用一次的组件。
  3. 降维:降维使用的场景更为少见,比如一个组件出现了问题,我们想把这个组件从本地实现改为一个wap页。降维一般需要后台配置才生效,可以在onCreate对线上配置进行检查,如果需要降维,则把所有的UI跳转到配置的wap页上面去。

      一个小的细节是,主项目负责加载组件,由于主项目和组件之间是隔离的,那么主项目如何调用组件ApplicationLike的生命周期方法呢,目前我们采用的是基于编译期字节码插入的方式,扫描所有的ApplicationLike类(其有一个共同的父类),然后通过javassisit在主项目的onCreate中插入调用ApplicationLike.onCreate的代码。

      我们再优化一下组件化的架构图:

2-6 集成调试

  每个组件单独调试通过并不意味着集成在一起没有问题,因此在开发后期我们需要把几个组件机集成到一个app里面去验证。由于我们上面的机制保证了组件之间的隔离,所以我们可以任意选择几个组件参与集成。这种按需索取的加载机制可以保证在集成调试中有很大的灵活性,并且可以加大的加快编译速度。

  我们的做法是这样的,每个组件开发完成之后,发布一个relaese的aar到一个公共仓库,一般是本地的maven库。然后主项目通过参数配置要集成的组件就可以了。所以我们再稍微改动一下组件与主项目之间的连接线,形成的最终组件化架构图如下:

2-7 代码隔离

  此时在回顾我们在刚开始拆分组件化是提出的三个问题,应该说都找到了解决方式,但是还有一个隐患没有解决,那就是我们可以使用compile project(xxx:reader.aar)来引入组件吗?虽然我们在数据传输章节使用了接口+实现的架构,组件之间必须针对接口编程,但是一旦我们引入了reader.aar,那我们就完全可以直接使用到其中的实现类啊,这样我们针对接口编程的规范就成了一纸空文。千里之堤毁于蚁穴,只要有代码(不论是有意还是无意)是这么做了,我们前面的工作就白费了。

  我们希望只在assembleDebug或者assembleRelease的时候把aar引入进来,而在开发阶段,所有组件都是看不到的,这样就从根本上杜绝了引用实现类的问题。我们把这个问题交给gradle来解决,我们创建一个gradle插件,然后每个组件都apply这个插件,插件的配置代码也比较简单:

    //根据配置添加各种组件依赖,并且自动化生成组件加载代码
if (project.android instanceof AppExtension) {
AssembleTask assembleTask = getTaskInfo(project.gradle.startParameter.taskNames)
if (assembleTask.isAssemble
&& (assembleTask.modules.contains("all") || assembleTask.modules.contains(module))) {
//添加组件依赖
project.dependencies.add("compile","xxx:reader-release@aar")
//字节码插入的部分也在这里实现
}
} private AssembleTask getTaskInfo(List<String> taskNames) {
AssembleTask assembleTask = new AssembleTask();
for (String task : taskNames) {
if (task.toUpperCase().contains("ASSEMBLE")) {
assembleTask.isAssemble = true;
String[] strs = task.split(":")
assembleTask.modules.add(strs.length > 1 ? strs[strs.length - 2] : "all");
}
}
return assembleTask
}

三、组件化的拆分步骤和动态需求

3-1 拆分原则

  组件化的拆分是个庞大的工程,特别是从几十万行代码的大工程拆分出去,所要考虑的事情千头万绪。为此我觉得可以分成三步:

  • 从产品需求到开发阶段再到运营阶段都有清晰边界的功能开始拆分,比如读书模块、直播模块等,这些开始分批先拆分出去
  • 在拆分中,造成组件依赖主项目的依赖的模块继续拆出去,比如账户体系等
  • 最终主项目就是一个Host,包含很小的功能模块(比如启动图)以及组件之间的拼接逻辑

3-2 组件化的动态需求

  最开始我们讲到,理想的代码组织形式是插件化的方式,届时就具备了完备的运行时动态化。在向插件化迁徙的过程中,我们可以通过下面的集中方式来实现编译速度的提升和动态更新。

  • 在快速编译上,采用组件级别的增量编译。在抽离组件之前可以使用代码级别的增量编译工具如freeline(但databinding支持较差)、fastdex等
  • 动态更新方面,暂时不支持新增组件等大的功能改进。可以临时采用方法级别的热修复或者功能级别的Tinker等工具,Tinker的接入成本较高。

四、总结

  本文是笔者在设计“得到app”的组件化中总结一些想法,在设计之初参考了目前已有的组件化和插件化方案,站在巨人的肩膀上又加了一点自己的想法,主要是组件化生命周期以及完全的代码隔离方面。特别是最后的代码隔离,不仅要有规范上的约束(针对接口编程),更要有机制保证开发者不犯错,我觉得只有做到这一点才能认为是一个彻底的组件化方案。

【广告时间】

  罗辑思维“得到app”发展迅猛,我们急需各路Android和iOS大牛加入,只要你满足下面几点,就是我们所需要的:

1、热爱移动端开发(android/ios/rn),最好有2到3年的app开发经验

2、对技术有极致追求,并能踏实的推动技术驱动,致力于实现自己更实现他人

3、如果你也喜欢知识付费类的产品,有一些自己的产品想法,那更是我们需要的人

  我们的办公地点就在长安街沿线的一座装修精美的独栋里面,环境十分优美。至于待遇,绝对是业内领先的,从我们的内推奖是豪华的苹果三件套就可见一斑。如果你感兴趣,请发送简历到zhangmingqing@luojilab.com。

Android彻底组件化方案实践的更多相关文章

  1. Android组件化方案及组件消息总线modular-event实战

    背景 组件化作为Android客户端技术的一个重要分支,近年来一直是业界积极探索和实践的方向.美团内部各个Android开发团队也在尝试和实践不同的组件化方案,并且在组件化通信框架上也有很多高质量的产 ...

  2. Android 组件化方案探索与思考

    Android 组件化方案探索与思考 组件化项目,通过gradle脚本,实现module在编译期隔离,运行期按需加载,实现组件间解耦,高效单独调试. 本项目github地址 https://githu ...

  3. iOS组件化方案的几种实现

    最近研究了一下项目的组件化,把casa.bang.limboy的有关组件化的博客看了一遍,学到了不少东西,对目前业界的组件化方案有了一定的了解.这些高质量的博客大致讨论了组件化的三种方案:url-bl ...

  4. atitit.atiHtmlUi web组件化方案与规范v1

    atitit.atiHtmlUi web组件化方案与规范v1 1. 如何在现有html 标签基础上定义自己的组件1 2. 组件的构成与定义1 3. 组件的加载1 4. 组件css的加载2 5. 操作组 ...

  5. android组件化方案、二维码扫码、Kotlin新闻客户端、动画特效等源码

    Android精选源码 CalendarView日历选择器 android下拉刷新动画效果代码 一个非常方便的fragment页面框架 android组件化方案源码 Zxing实现二维码条形码的扫描和 ...

  6. Vue.js:轻量高效的前端组件化方案(转载)

    摘要:Vue.js通过简洁的API提供高效的数据绑定和灵活的组件系统.在前端纷繁复杂的生态中,Vue.js有幸受到一定程度的关注,目前在GitHub上已经有5000+的star.本文将从各方面对Vue ...

  7. Android 业务组件化开发实践

    组件化并不是新话题,其实很早很早以前我们开始为项目解耦的时候就讨论过的.但那时候我们说的是功能组件化.比如很多公司都常见的,网络请求模块.登录注册模块单独拿出来,交给一个团队开发,而在用的时候只需要接 ...

  8. iOS 模块化、组件化方案探索(利用cocoapods 、git 创建私有仓库)

    来自bang's blog http://blog.cnbang.net/tech/3080/ 模块化 简单来说,模块化就是将一个程序按照其功能做拆分,分成相互独立的模块,以便于每个模块只包含与其功能 ...

  9. iOS 组件化方案探索

    来自bang's blog http://blog.cnbang.net/tech/3080/

随机推荐

  1. openstack windows2012r2 glance镜像制作

    镜像实现: 密码注入 修改密码 根分区扩展 1.下载windows iso镜像 下载地址:http://imsdn.com/MSDN-1.html 例如:cn_windows_server_2012_ ...

  2. Android BottomSheet:以选取图片为例(2)

     Android BottomSheet:以选取图片为例(2) 附录文章5简单介绍了常见的分享面板在BottomSheet中的具体应用.本文再以常见的选取图片为例写一个例子. 布局文件: < ...

  3. 4种OSS的应用架构及核心技术

        基础型 架构描述:OSS作为文件存储源,用户上传下载数据均经过ECS与OSS通信. 解决用户问题:文件空间大,ECS磁盘存储空间有限:多ECS间无法同步数据. 适用场景描述:文件较多,但文件调 ...

  4. [COGS311] Redundant Paths

    ★★☆   输入文件:rpaths.in   输出文件:rpaths.out   简单对比 时间限制:1 s   内存限制:128 MB Description In order to get fro ...

  5. 【Eclipse】Eclipse 快捷键

    Eclipse 快捷键 关于快捷键 Eclipse 的很多操作都提供了快捷键功能,我们可以通过键盘就能很好的控制 Eclipse 各个功能: 使用快捷键关联菜单或菜单项 使用快捷键关联对话窗口或视图或 ...

  6. Win32编程API 基础篇 -- 4.消息循环

    消息循环 理解消息循环 为了编写任何即使是最简单的程序,了解windows程序的消息循环和整个消息发送结构是非常有必要的.既然我们已经尝试了一点消息处理的东西,我们应该对整个程序有更深入的理解,如果你 ...

  7. Ubuntu查看和写入系统日志

    一.背景 Linux将大量事件记录到磁盘上,它们大部分以纯文本形式存储在/var/log目录中.大多数日志条目通过系统日志守护进程syslogd,并被写入系统日志. Ubuntu包括以图形方式或从命令 ...

  8. 正确移除List中对象

    list是一个ArrayList的对象,哪个选项的代码填到//todo delete处.能够在Iterator遍历的过程中正确并安全的删除一个list中保存的对象?() Iterator it = l ...

  9. 【UI自动化方面】

    1.自动化执行失败如何排查故障. 答:1).查看log,错误原因.[log不详细的话,可以优化] 2).排查是否真的有bug,若不是bug查看是否是新版本引入了新的变更. 3).调试脚本看自己脚本是不 ...

  10. ios逆向工程

    原 ios逆向工程-内部钩子(Method Swizzling)   Method+Swizzling ios hook Method Swizzling(方法调配) 怎么说呢,先了解什么是钩子为什么 ...