Markdown版本笔记 | 个人GitHub首页 | 个人博客 | 个人微信 | 个人邮箱 |
---|---|---|---|---|
MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
组件化 获得 DDComponent JIMU 模块 插件 MDjava
原始的获得官网的项目(2.7K):DDComponentForAndroid
项目做者离职后维护的项目(1.4K):JIMU
我的实践Demo
官方Demo简化android
已实现的功能git
参考:浅谈Android组件化github
模块化、插件化和组件化的关系
在技术开发领域,模块化
是指分拆代码,即当咱们的代码特别臃肿的时候,用模块化将代码分而治之、解耦分层。具体到 android 领域,模块化的具体实施方法分为插件化和组件化
。编程
插件化和组件化的区别
一套完整的插件化或组件化都必须可以实现单独调试、集成编译、数据传输、UI 跳转、生命周期和代码边界
这六大功能。插件化和组件化最重要并且是惟一的区别的就是:插件化能够动态
增长和修改线上的模块,组件化的动态能力相对较弱,只能对线上已有模块进行动态的加载和卸载
,不能新增和修改
.。api
如何取舍插件化和组件化?
在插件化和组件化取舍的一个重要原则是:APP 是否有动态增长或修改
线上模块的需求,若是这种动态性的需求很弱,就不须要考虑插件化,通常说来,电商类或广告类产品对这个需求比较强烈,而相似“获得 APP”这类的知识服务产品,每一个功能的推出都是通过精细打磨的,对这种即时的动态性要求不高,因此不须要采用插件化。微信
若是你的产品对动态性的要求比较高,那么在选择插件化以前也须要从两个方面权衡一下:网络
所以,对大多数产品来讲,组件化都是一个不错甚至最佳的选择,它没有兼容性,能够更方便地拆分,而且几乎没有技术障碍,能够更顺利地去执行。特别是对急需拆分的产品来讲,组件化是一个可退可守的方案,能够更快地执行下去,而且未来要是迁移到插件化,组件化拆分也是必经的一步。架构
何为“完全”组件化
之因此称这个方案是完全的组件化,主要是为了更好地强调组件之间代码边界
的问题,组件之间的直接引用(compile)是要坚定避免的,一旦这么作了,就不免会致使直接使用其余组件的具体实现类,这样针对接口编程的要求就成了一句空话。更严重的是,一旦决定对组件进行动态地加载或卸载,就会致使严重地崩溃。因此只有作到了代码隔离,这个组件化方案才能够称之为“完全”的。app
在如今的方案中能够作到代码编写期间组件之间是彻底不可见的,所以杜绝了直接使用具体的实现类的状况,可是在编译打包的时候,又会自动把依赖的组件打包进去。该方案是一个集合了六大功能的完整方案,覆盖了组件化中须要考虑的所有状况。
既然是“完全”组件化,那么代码解耦以后,怎样才能让主项目间接引用
各个独立的组件呢?
方案采用的是一个配置文件,每一个组件声明本身所须要的其余组件,配置分为 debug 和 release 两种,能够在平常开发和正式打包之间更灵活的切换。
方案自定义了一个 gradle 插件,它去读取每一个组件的配置文件,构建出组件之间的依赖关系
。这个插件更“智能”的地方在于,它分析运行的 task 命令,判断是不是打包命令
,是的话(例如 assembleRelease)自动根据配置引入,不是(例如正常的 sync /build)等则不引入,也就是在代码编写期间
组件之间是彻底不可见的,所以杜绝了直接使用具体的实现类的状况。可是在编译打包
的时候,又会自动把依赖的组件打包进去。固然这里面还会涉及到组件之间如何经过“接口 + 实现”的方式进行数据传输,每一个组件若是进行加载等问题,这些在 方案 中都有成熟的解决方式。
方案中自定义的 gradle 插件还有一个比较好的功能就是能够自动的识别和修改组件的属性,它能够识别出当前调试的是哪一个组件,而后把这个组件修改成 application 项目,而其余组件则默默的修改为 library 项目。所以不管是要单独编译一个组件仍是要把这个组件集成到其余组件中调试,都是不须要作任何的手动修改,使用起来至关的方便。
在刚开始对“获得 APP”Android 端的代码进行组件化拆分的时候,“获得 APP”已是一个千万用户级的产品。通过那么长时间的积累,几十万行代码堆积在一块儿,编译一次大约须要 10 分钟的时间,这严重影响了开发效率。
因为业务复杂,代码交织在一块儿,可谓牵一发而动全身,所以每一个人在写新需求的时候都有严重的代码包袱,瞻前顾后,花费在熟悉以前的代码的时间甚至大于新需求的开发时间。而且每一个改动都须要测试人员进行大范围的回归,因此整个开发团队的效率都受到了影响。在这种状况下,实施组件化是迫在眉睫了。
因为国内对插件化
的研究是比较火爆的,而对组件化的研究热情就相对淡了不少。在设计“获得 APP”组件化方案的时候,几乎查阅了所有的组件化文章,都没有找到一个完整的支持上面说的六大功能的方案,因此不得不从头开始设计,“完全组件化”的方案可跳转阅读 Android 完全组件化方案实践 和 Android 完全组件化 demo
让方案从纸上运用到实际,是一个比较困难的过程,这期间要注意两个方面的问题:一是技术细节上的不断完善,二是团队的共识建设问题。
技术上的问题主要是如何让方案更灵活,需求老是比预期要复杂,遇到特殊的需求,以前的设计可能就无法实现,或者必须得突破以前肯定的拆分原则。这时候就须要回过头再审视整个方案,看看可否在某些方面作一些调整。方案中数据传输和 UI 跳转是分开的两个功能,这是在实际拆分中才作出的选择,由于数据传输更为频繁,且交互形式更多样,使用一个标准化的路由协议难以知足,所以把数据传输改为了接口 + 实现的形式,针对接口编程就能够更加灵活地处理这些状况。
除了技术上的,更重要的是团队的共识问题。要执行一个组件化拆分这样的大工程,须要团队的每一个人达成共识,不管是在方案仍是在技术的实现细节上,你们都能有一个统一的方向。
为此,在拆分以前多作几回组内的分享讨论,从方案的制定到每一次的实施,都让团队的大部分红员参与进来。正所谓磨刀不误砍柴工,在这种前提下,团队的共识建设会对后期工做效率的提升产生很大的价值。确立了共识,还须要确立统一的规则,虽然说条条大路通罗马,可是在一个产品里,仍是须要选择统一的道路,不然即使作了拆分,效果也会大打折扣。
不管是 Android 仍是 iOS,要解决的问题都是同样的,所以在组件化方案上要实现的功能(即上面所说的上面六种功能)也都是同样的,因此二者的组件化大致上来讲是基本相同的。
有一个微小的区别在于技术实现方式的不一样,因为两个平台用到的开发技术是不一样的,Android 的组件化可能须要考虑向插件化的迁移,后期一旦有动态变更功能的强需求,能够快速地切换。而目前苹果官方是不容许这种动态性的,因此这方面的考虑就会少一点。可是 iOS 一样能够作到动态地加载和卸载组件的,所以在诸如生命周期、代码边界等问题上也须要格外注意,只是目前一些 iOS 组件化方案在这方面可能考虑的相对少一点。
组件化后的代码结构很是清晰,分层结构以及之间的交互很明了,团队中的任何一我的均可以很轻松的绘制出代码结构图,这个在以前是无法作到的,而且每一个组件的编译时间从 10 分钟降到了几十秒,工做效率有了很大地提高,最关键的仍是解耦以后,每次开发需求的时候,面对的代码愈来愈少,不用背负那么重的代码包袱,能够说达到了“代码越写越少”的理想状况。
其实组件化对外输出也是很可观的,如今一个版本开发完成后,咱们能够跟测试说这期就回归“天天听本书”组件,其余的不须要回归。这种自信在以前是绝对没有的,测试的压力也能够小不少。更重要的是咱们的组件能够复用,“获得 APP”会上线新的产品,他们能够直接使用已有的组件,省去了不少重复造轮子的工做,这点对整个公司效率的提高也是颇有帮助的。
项目发展到必定程度,随着人员的增多,代码愈来愈臃肿,这时候就必须进行模块化的拆分。在我看来,模块化是一种指导理念,其核心思想就是分而治之、下降耦合。而在Android工程中如何实施,目前有两种途径,也是两大流派,一个是组件化,一个是插件化。
提起组件化和插件化的区别,有一个很形象的图:
上面的图看上去彷佛比较清晰,其实容易致使一些误解,有下面几个小问题,图中说的就不太清楚:
本文主要集中讲的是组件化的实现思路,对于插件化的技术细节不作讨论,咱们只是从上面的问答中总结出一个结论:组件化和插件化的最大区别(应该也是惟一区别)就是组件化在运行时不具有动态添加和修改组件的功能
,可是插件化是能够的。
暂且抛弃对插件化“道德”上的批判,我认为对于一个Android开发者来说,插件化的确是一个福音,这将使咱们具有极大的灵活性。可是苦于目前尚未一个彻底合适、完美兼容的插件化方案,特别是对于已经有几十万代码量的一个成熟产品来说,套用任何一个插件化方案都是很危险的工做。因此咱们决定先从组件化作起,本着作一个最完全的组件化方案的思路去进行代码的重构,下面是最近的思考结果,欢迎你们提出建议和意见。
要实现组件化,不论采用什么样的技术路径,须要考虑的问题主要包括下面几个:
把庞大的代码进行拆分,AndroidStudio可以提供很好的支持,使用IDE中的multiple module
这个功能,咱们很容易把代码进行初步的拆分。在这里咱们对两种module进行区分:
基础库
library,这些代码被其余组件直接引用
(是次方案中很是核心的设计理念),好比网络库module能够认为是一个library。完整的功能模块
,好比读书或者分享module就是一个Component。为了方便,咱们统一把library称之为依赖库,而把Component称之为组件,咱们所讲的组件化也主要是针对Component这种类型。而负责拼装这些组件以造成一个完成app的module,通常咱们称之为主项目、主module或者Host
,方便起见咱们也统一称为主项目。
通过简单的思考,咱们可能就能够把代码拆分红下面的结构:
这种拆分都是比较容易作到的,从图上看,读书、分享等都已经拆分组件,并共同依赖于公共的依赖库(简单起见只画了一个),而后这些组件都被主项目所引用。读书、分享等组件之间没有直接的联系,咱们能够认为已经作到了组件之间的解耦。
可是这个图有几个问题须要指出:
主项目对组件的直接引用是不能够的
,可是咱们的读书组件最终是要打到apk里面,不只代码要和并到claases.dex里面,资源也要通过meage操做合并到apk的资源里面,怎么避免这个矛盾呢?这些问题咱们后面一个个来解决,首先咱们先看代码解耦要作到什么效果,像上面的直接引用并使用其中的类确定是不行的了。因此咱们认为代码解耦的首要目标就是组件之间的彻底隔离
,咱们不只不能直接使用其余组件中的类,最好能根本不了解其中的实现细节。只有这种程度的解耦才是咱们须要的。
其实单独调试比较简单,只须要把 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
,从而让组件的代码运行在其中,因此咱们能够再优化一下咱们上面的框架图。
上面咱们讲到,主项目和组件、组件与组件之间不能直接使用类的相互引用来进行数据交互。那么如何作到这个隔离呢?
在这里咱们采用接口+实现
的结构。每一个组件声明本身提供的服务Service(抽象类或者接口),组件负责将这些Service实现并注册到一个统一的路由Router中去。若是要使用某个组件的功能,只须要向Router请求这个Service的实现,具体的实现细节咱们全然不关心(也没有暴露),只要能返回咱们须要的结果就能够了。这与Binder的C/S架构很相像。
由于咱们组件之间的数据传递都是基于接口编程的,接口和实现是彻底分离的,因此组件之间就能够作到解耦,咱们能够对组件进行替换、删除等动态管理。
这里面有几个小问题须要明确:
componentservice
(另外一个核心设计)的依赖库,里面定义了每一个组件向外提供的service和一些公共model。将全部组件的service整合在一块儿,是为了在拆分初期操做更为简单,后面须要改成自动化的方式来生成。这个依赖库须要严格遵循开闭原则,以免出现版本兼容等问题。下面就是加上数据传输功能以后的架构图:
能够说UI的跳转也是组件提供的一种特殊的服务,能够归属到上面的数据传递中去。不过通常UI的跳转咱们会单独处理,通常经过短链
的方式来跳转到具体的Activity。每一个组件能够注册本身所能处理的短链的scheme和host
,并定义传输数据的格式。而后注册到统一的UIRouter
中,UIRouter经过scheme和host的匹配关系负责分发路由。
UI跳转部分的具体实现是经过在每一个Activity上添加注解,而后经过apt(Annotation Processing Tool, 注解处理器)
造成具体的逻辑代码。这个也是目前Android中UI路由的主流实现方式。
具体的功能介绍和使用规范,请你们参见文章:android完全组件化—UI跳转升级改造
因为咱们要动态的管理组件,因此给每一个组件添加几个生命周期状态:加载、卸载和降维
。为此咱们给每一个组件增长一个ApplicationLike
类,里面定义了onCreate和onStop
两个生命周期函数。
一个小的细节是,主项目负责加载组件,因为主项目和组件之间是隔离的,那么主项目如何调用组件ApplicationLike的生命周期方法呢?
目前咱们采用的是基于编译期字节码插入
的方式,扫描全部的ApplicationLike
类(其有一个共同的父类),而后经过Javassist
(Java assist ,一个开源的分析、编辑和建立Java字节码的类库)在主项目的onCreate中插入调用 ApplicationLike.onCreate 的代码
。
咱们再优化一下组件化的架构图:
每一个组件单独调试经过并不意味着集成在一块儿没有问题,所以在开发后期咱们须要把几个组件机集成到一个app里面去验证。因为咱们上面的机制保证了组件之间的隔离,因此咱们能够任意选择几个组件参与集成。这种按需索取的加载机制能够保证在集成调试中有很大的灵活性,而且能够极大的加快编译速度。
咱们的作法是这样的,每一个组件开发完成以后,发布一个relaese的aar
到一个公共仓库,通常是本地的maven库。而后主项目经过参数配置要集成的组件就能够了。
因此咱们再稍微改动一下组件与主项目之间的链接线,造成的最终组件化架构图以下:
此时再回顾咱们在刚开始拆分组件化时提出的三个问题,应该说都找到了解决方式,可是还有一个隐患没有解决,那就是咱们可使用compile project(xxx:reader.aar)
来引入组件吗?虽然咱们在数据传输章节使用了接口+实现
的架构,组件之间必须针对接口编程,可是一旦咱们引入了reader.aar,那咱们就彻底能够直接使用到其中的实现类啊,这样咱们针对接口编程的规范就成了一纸空文。千里之堤毁于蚁穴,只要有代码(不管是有意仍是无心)是这么作了,咱们前面的工做就白费了。
咱们但愿只在assembleDebug或者assembleRelease
的时候把aar引入进来(也就是在执行打包
命令的时候,首先经过compile引入组件),而在开发阶段,全部组件都是看不到的(由于开发阶段咱们并无经过compile引入组件),这样就从根本上杜绝了引用实现类的问题。
为了实现这个目的,咱们是这么作的:咱们把这个问题交给gradle
来解决,咱们建立一个gradle插件,而后每一个组件都apply这个插件,插件的配置代码也比较简单,就是在执行打包命令的时候,根据配置添加各类组件依赖,而且自动化生成组件加载代码:
apply plugin: 'com.dd.comgradle' //根据配置添加各类组件依赖,而且自动化生成组件加载代码 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") //添加组件依赖 //字节码插入的部分也在这里实现 } } //获取正在执行的Task的信息 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 }
拆分原则(只是建议)
组件化的拆分是个庞大的工程,特别是从几十万行代码的大工程拆分出去,所要考虑的事情千头万绪。为此我以为能够分红三步:
组件化的动态需求(然并不支持)
最开始咱们讲到,理想的代码组织形式是插件化的方式,届时就具有了完备的运行时动态化。在向插件化迁徙的过程当中,咱们能够经过下面的集中方式来实现编译速度的提高和动态更新。
获得组件化改造大流程
本文是笔者在设计"获得app"的组件化中总结一些想法(目前已经离职加入头条),在设计之初参考了目前已有的组件化和插件化方案,站在巨人的肩膀上又加了一点本身的想法,主要是组件化生命周期以及彻底的代码隔离方面。特别是最后的代码隔离,不只要有规范上的约束(针对接口编程),更要有机制保证开发者不犯错,我以为只有作到这一点才能认为是一个完全的组件化方案。
2018-4-22