得到Android组件化方案已经开源,参见Android组件化方案开源。方案的解读文章是一个小的系列,这是系列的第二篇文章:
1、Android彻底组件化方案实践
2、Android彻底组件化demo发布
3、Android彻底组件化-代码和资源隔离
4、Android彻底组件化—UI跳转升级改造
5、Android彻底组件化—如何使用Arouter
今年6月份开始,我开始负责对“得到app”的android代码进行组件化拆分,在动手之前我查阅了很多组件化或者模块化的文章,虽然有一些收获,但是很少有文章能够给出一个整体且有效的方案,大部分文章都只停留在组件单独调试的层面上,涉及组件之间的交互就很少了,更不用说组件生命周期、集成调试和代码边界这些最棘手的问题了。有感于此,我觉得很有必要设计一套完整的组件化方案,经过几周的思考,反复的推倒重建,终于形成了一个完整的思路,整理在我的第一篇文章中Android彻底组件化方案实践。这两个月以来,得到的Android团队按照这个方案开始了组件化的拆分,经过两个开发周期的努力,目前已经拆分五个大的业务组件以及数个底层lib库,并对之前的方案进行了一些完善。从使用效果上来看,这套方案完全可以达到了我们之前对组件化的预期,并且架构简单,学习成本低,对于一个急需快速组件化拆分的项目是很适合的。现在将这套方案开源出来,欢迎大家共同完善。代码地址:https://github.com/mqzhangw/JIMU
一、JIMU使用指南
首先我们看一下demo的代码结构,然后根据这个结构图再次从单独调试(发布)、组件交互、UI跳转、集成调试、代码边界和生命周期等六个方面深入分析,之所以说“再次”,是因为上一篇文章我们已经讲了这六个方面的原理,这篇文章更侧重其具体实现。
JIMU结构图.png代码中的各个module基本和图中对应,从上到下依次是:
- app是主项目,负责集成众多组件,控制组件的生命周期
- reader和share是我们拆分的两个组件
- componentservice中定义了所有的组件提供的服务
- basicres定义了全局通用的theme和color等公共资源
- basiclib中是公共的基础库,一些第三方的库(okhttp等)也统一交给basiclib来引入
图中没有体现的module有两个,一个是componentlib,这个是我们组件化的基础库,像Router/UIRouter等都定义在这里;另一个是build-gradle,这个是我们组件化编译的gradle插件,也是整个组件化方案的核心。
我们在demo中要实现的场景是:主项目app集成reader和share两个组件,其中reader提供一个读书的fragment给app调用(组件交互),share提供一个activity来给reader来调用(UI跳转)。主项目app可以动态的添加和卸载share组件(生命周期)。而集成调试和代码边界是通过build-gradle插件来实现的。
1 单独调试和发布
单独调试的配置与上篇文章基本一致,通过在组件工程下的gradle.properties文件中设置一个isRunAlone的变量来区分不同的场景,唯一的不同点是在组件的build.gradle中不需要写下面的样板代码:
if(isRunAlone.toBoolean()){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}
而只需要引入一个插件com.dd.comgradle(源码就在build-gradle),在这个插件中会自动判断apply com.android.library还是com.android.application。实际上这个插件还能做更“智能”的事情,这个在集成调试章节中会详细阐述。
单独调试所必须的AndroidManifest.xml、application、入口activity等类定义在src/main/runalone下面,这个比较简单就不赘述了。
如果组件开发并测试完成,需要发布一个release版本的aar文件到中央仓库,只需要把isRunAlone修改为false,然后运行module:assembleRelease命令就可以了。这里简单起见没有进行版本管理,大家如果需要自己加上就好了。值得注意的是,发布组件是唯一需要修改isRunAlone=false的情况,即使后面将组件集成到app中,也不需要修改isRunAlone的值,既保持isRunAlone=true即可。所以实际上在Androidstudio中,是可以看到三个application工程的,随便点击一个都是可以独立运行的,并且可以根据配置引入其他需要依赖的组件。这背后的工作都由com.dd.comgradle插件来默默完成。
项目中有三个application工程.png2 组件交互
在这里组件的交互专指组件之间的数据传输,在我们的方案中使用的是接口+实现的方式,组件之间完全面向接口编程。
在demo中我们让reader提供一个fragment给app使用来说明。首先reader组件在componentservice中定义自己的服务
public interface ReadBookService {
Fragment getReadBookFragment();
}
然后在自己的组件工程中,提供具体的实现类ReadBookServiceImpl:
public class ReadBookServiceImpl implements ReadBookService {
@Override
public Fragment getReadBookFragment() {
return new ReaderFragment();
}
}
提供了具体的实现类之后,需要在组件加载的时候把实现类注册到Router中,具体的代码在ReaderAppLike中,ReaderAppLike相当于组件的application类,这里定义了onCreate和onStop两个生命周期方法,对应组件的加载和卸载。
public class ReaderAppLike implements IApplicationLike {
Router router = Router.getInstance();
@Override
public void onCreate() {
router.addService(ReadBookService.class.getSimpleName(), new ReadBookServiceImpl());
}
@Override
public void onStop() {
router.removeService(ReadBookService.class.getSimpleName());
}
}
在app中如何使用如reader组件提供的ReaderFragment呢?注意此处app是看不到组件的任何实现类的,它只能看到componentservice中定义的ReadBookService,所以只能面向ReadBookService来编程。具体的实例代码如下:
Router router = Router.getInstance();
if (router.getService(ReadBookService.class.getSimpleName()) != null) {
ReadBookService service = (ReadBookService) router.getService(ReadBookService.class.getSimpleName());
fragment = service.getReadBookFragment();
ft = getSupportFragmentManager().beginTransaction();
ft.add(R.id.tab_content, fragment).commitAllowingStateLoss();
}
这里需要注意的是由于组件是可以动态加载和卸载的,因此在使用ReadBookService的需要进行判空处理。我们看到数据的传输是通过一个中央路由Router来实现的,这个Router的实现其实很简单,其本质就是一个HashMap,具体代码大家参见源码。
通过上面几个步骤就可以轻松实现组件之间的交互,由于是面向接口,所以组件之间是完全解耦的。至于如何让组件之间在编译阶段不不可见,是通过上文所说的com.dd.comgradle实现的,这个在第一篇文章中已经讲到,后面会贴出具体的代码。
3 UI跳转
页面(activity)的跳转也是通过一个中央路由UIRouter来实现,不同的是这里增加了一个优先级的概念。具体的实现就不在这里赘述了,代码还是很清晰的。
具体的功能介绍和使用规范,请大家参见文章:
android彻底组件化—UI跳转升级改造
4 集成调试
集成调试可以认为由app或者其他组件充当host的角色,引入其他相关的组件一起参与编译,从而测试整个交互流程。在demo中app和reader都可以充当host的角色。在这里我们以app为例。
首先我们需要在根项目的gradle.properties中增加一个变量mainmodulename,其值就是工程中的主项目,这里是app。设置为mainmodulename的module,其isRunAlone永远是true。
然后在app项目的gradle.properties文件中增加两个变量:
debugComponent=readercomponent,com.mrzhang.share:sharecomponent
compileComponent=readercomponent,sharecomponent
其中debugComponent是运行debug的时候引入的组件,compileComponent是release模式下引入的组件。我们可以看到debugComponent引入的两个组件写法是不同的,这是因为组件引入支持两种语法,module或者modulePackage:module,前者直接引用module工程,后者使用componentrelease中已经发布的aar。
注意在集成调试中,要引入的reader和share组件是不需要把自己的isRunAlone修改为false的。我们知道一个application工程是不能直接引用(compile)另一个application工程的,所以如果app和组件都是isRunAlone=true的话在正常情况下是编译不过的。秘密就在于com.dd.comgradle会自动识别当前要调试的具体是哪个组件,然后把其他组件默默的修改为library工程,这个修改只在当次编译生效。
如何判断当前要运行的是app还是哪个组件呢?这个是通过task来判断的,判断的规则如下:
- assembleRelease → app
- app:assembleRelease或者 :app:assembleRelease → app
- sharecomponent:assembleRelease 或者:sharecomponent:assembleRelease→ sharecomponent
上面的内容要实现的目的就是每个组件可以直接在Androidstudio中run,也可以使用命令进行打包,这期间不需要修改任何配置,却可以自动引入依赖的组件。这在开发中可以极大加快工作效率。
5 代码边界
至于依赖的组件是如何集成到host中的,其本质还是直接使用compile project(...)或者compile modulePackage:module@aar。那么为啥不直接在build.gradle中直接引入呢,而要经过com.dd.comgradle这个插件来进行诸多复杂的操作?原因在第一篇文章中也讲到了,那就是组件之间的完全隔离,也可以称之为代码边界。如果我们直接compile组件,那么组件的所有实现类就完全暴露出来了,使用方就可以直接引入实现类来编程,从而绕过了面向接口编程的约束。这样就完全失去了解耦的效果了,可谓前功尽弃。
那么如何解决这个问题呢?我们的解决方式还是从分析task入手,只有在assemble任务的时候才进行compile引入。这样在代码的开发期间,组件是完全不可见的,因此就杜绝了犯错误的机会。具体的代码如下:
/**
* 自动添加依赖,只在运行assemble任务的才会添加依赖,因此在开发期间组件之间是完全感知不到的,这是做到完全隔离的关键
* 支持两种语法:module或者modulePackage:module,前者之间引用module工程,后者使用componentrelease中已经发布的aar
* @param assembleTask
* @param project
*/
private void compileComponents(AssembleTask assembleTask, Project project) {
String components;
if (assembleTask.isDebug) {
components = (String) project.properties.get("debugComponent")
} else {
components = (String) project.properties.get("compileComponent")
}
if (components == null || components.length() == 0) {
return;
}
String[] compileComponents = components.split(",")
if (compileComponents == null || compileComponents.length == 0) {
return;
}
for (String str : compileComponents) {
if (str.contains(":")) {
File file = project.file("../componentrelease/" + str.split(":")[1] + "-release.aar")
if (file.exists()) {
project.dependencies.add("compile", str + "-release@aar")
} else {
throw new RuntimeException(str + " not found ! maybe you should generate a new one ")
}
} else {
project.dependencies.add("compile", project.project(':' + str))
}
}
}
6 生命周期
在上一篇文章中我们就讲过,组件化和插件化的唯一区别是组件化不能动态的添加和修改组件,但是对于已经参与编译的组件是可以动态的加载和卸载的,甚至是降维的。
首先我们看组件的加载,使用章节5中的集成调试,可以在打包的时候把依赖的组件参与编译,此时你反编译apk的代码会看到各个组件的代码和资源都已经包含在包里面。但是由于每个组件的唯一入口ApplicationLike还没有执行oncreate()方法,所以组件并没有把自己的服务注册到中央路由,因此组件实际上是不可达的。
在什么时机加载组件以及如何加载组件?目前com.dd.comgradle提供了两种方式,字节码插入和反射调用。
- 字节码插入模式是在dex生成之前,扫描所有的ApplicationLike类(其有一个共同的父类),然后通过javassist在主项目的Application.onCreate()中插入调用ApplicationLike.onCreate()的代码。这样就相当于每个组件在application启动的时候就加载起来了。
- 反射调用的方式是手动在Application.onCreate()中或者在其他合适的时机手动通过反射的方式来调用ApplicationLike.onCreate()。之所以提供这种方式原因有两个:对代码进行扫描和插入会增加编译的时间,特别在debug的时候会影响效率,并且这种模式对Instant Run支持不好;另一个原因是可以更灵活的控制加载或者卸载时机。
这两种模式的配置是通过配置com.dd.comgradle的Extension来实现的,下面是字节码插入的模式下的配置格式,添加applicationName的目的是加快定位Application的速度。
combuild {
applicationName = 'com.mrzhang.component.application.AppApplication'
isRegisterCompoAuto = true
}
demo中也给出了通过反射来加载和卸载组件的实例,在APP的首页有两个按钮,一个是加载分享组件,另一个是卸载分享组件,在运行时可以任意的点击按钮从而加载或卸载组件,具体效果大家可以运行demo查看。
加载和卸载示例.png二、组件化拆分的感悟
在最近两个月的组件化拆分中,终于体会到了做到剥丝抽茧是多么艰难的事情。确定一个方案固然重要,更重要的是克服重重困难坚定的实施下去。在拆分中,组件化方案也不断的微调,到现在终于可以欣慰的说,这个方案是经历过考验的,第一它学习成本比较低,组内同事可以快速的入手,第二它效果明显,得到本来run一次需要8到10分钟时间(不过后面换了顶配mac,速度提升了很多),现在单个组件可以做到1分钟左右。最主要的是代码结构清晰了很多,这位后期的并行开发和插件化奠定了坚实的基础。
总之,如果你面前也是一个庞大的工程,建议你使用该方案,以最小的代价尽快开始实施组件化。如果你现在负责的是一个开发初期的项目,代码量还不大,那么也建议尽快进行组件化的规划,不要给未来的自己增加徒劳的工作量。
本文提出的Android组件化方案已经开源,参见Android组件化方案开源项目
JIMU的讨论群,群号693097923,欢迎大家加入:
进群请扫码
网友评论
https://www.jianshu.com/p/df2a6717009d
Error:(1, 1) A problem occurred evaluating project ':app'.
> Failed to apply plugin [id 'com.dd.comgradle']
> Project with path ':chargencomponent' could not be found in project ':app'.
isRunAlone=true
debugComponent=chargecomponent
compileComponent=chargencomponent
运行或者打debug包就没这问题。
if (assembleTask.isDebug) {
components = (String) project.properties.get("debugComponent")
} else {
components = (String) project.properties.get("compileComponent")
}
跟这里有关系吗?
Error:Execution failed for task ':sharecomponentkotlin:transformClassesWithComponentCodeForDebug'.
> you should set applicationName in combuild
这是应该修改哪块?
一般组件复用的前提是,公司的基础库是共用的,一旦做不到这一点,可能就在于前期设计的时候考虑的不周,需要重新考虑一下设计
看到你之前回答中提到这个,这个fragment实例怎么获取?
这个代码不是注释吗吗 read组件的isRegisterCompoAuto又是false,为什么运行app的时候read组件还会加载,始终没有搞明白
1、既然已经通过assemble的时候去加载依赖,那么再通过设置isRunAlone为false去打包aar的意义在哪里,编译全工程都需要加载所有的依赖,直接依赖工程不就好了吗?如果说这样可以省去打包的时间,那单独编译子工程也是需要时间的。而且还需要考虑版本同步的问题。最后这也反倒混淆了一部分人对于这个参数的理解。
2、组件卸载的意义在哪里了。这里的卸载的意思最后是禁止该组件的访问而已,组件还是存在于整个应用之中,只是访问不到而已。因为在打包的时候,整个组件一直都存在于应用中。顶多加载和卸载释放了一个service的引用内存而已,感觉意义不。
不知道个人想法是否没能理解楼主真正的意思
2、卸载在节省内存上效果不是很大,但是在某些情况可以起到补救的作用,例如某个模块出现非技术问题,可以快速下掉这个模块,甚至可以结合降维来使用,把某个模块直接使用web页呈现。这些动作都需要热修复技术的配合。
所以这两个功能可能使用的频率真的很小,但是作为一个整体方案不得不考虑,你可以暂时不用,后面有需要的时候在使用
这个功能最好是做成apt生成的,github上有个anno分支,是一位热心开发者贡献的,我暂时还没时间去优化这块,欢迎你试用并提意见。
https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html
另外Gradle的脚本定制和插件定制不太熟悉啊,楼主是从哪儿系统的学习这些知识的?
问题2:将Componet和Moudle的定义从具体模块中剥离出来是否合适?
目前使用Dagger实现的小Demo:https://github.com/hunter524/AArcDemo
插件这块的知识,也谈不上系统,主要在之前做热修复的时候正好用到过,当时也是用到哪里就google哪里,多搜几篇文章就可以了。
project.android.sourceSets{
main{
}
}
}
project.android.sourceSets{ main{}} 提示错误,没有该方法
一下是build.gradle配置
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies{
compile 'com.android.tools.build:gradle:2.3.2'
compile group:'org.javassist',name:'javassist',version:'3.20.0-GA'
compile gradleApi()
compile localGroovy()
}
repositories{
mavenCentral()
}
group='com.zujian.andcomponent'
version='1.0.0'
uploadArchives{
repositories{
mavenDeployer{
repository(url:uri('../repo'))
}
}
}
问题一:
我在你的项目中新建了一个reviewcomponent.然后也照着你的代码实现了此模块包括ReviewAppLike和ReviewUIRouter,并且这个项目能单独运行。但是在主模块app中通过中央路由加载此模块 Router.registerComponent("com.meiyue.reviewcomponent.applike.ReviewAppLike")
它加载不了这个项目 会报classnotfound异常?
如果我在gradle.properties中配置debugComponent=readercomponent,sharecomponent,reviewcomponent 默认加载也不行,在MainActivity中手动加载模块也不行 为什么会找不到类?
问题二:
我没有通过中央仓库找,因为我新建的reviewcomponent生成不了aar文件,只我手动设置为library的时候才能生成aar,这个也不知道为什么,自定义的gradle不是自动切换的吗?
代码地址:https://github.com/luojilab/DDComponentForAndroid
插件化可能近期不会推出,第一篇文章也讲了,插件化唯一的好处就是可以动态的添加和修改功能模块,这个应用的场景很少,并且带来的成本太大。即使要开始做这块,估计也要比较晚才会动手了。
就是执行过插入字节码的任务后,第二次执行构建会报错,
* What went wrong:
Execution failed for task ':basiclib:clean'.
> Unable to delete file: E:\TDownload\DDComponentForAndroid-master\basiclib\build\intermediates\intermediate-jars\debug\classes.jar
感觉是这个jar在上次任务的进程中没有被释放,必须手动结束java进程,删除build文件夹,第二次构建才能成功
具体的限制是每个组件默认都是application工程,之间不能直接引用
applicatonName = 'com.mrzhang.component.application.AppApplication'
isRegisterCompoAuto = true
}”
------- readercomponent 的 isRegisterCompoAuto = false,也会自动字节码插入,这个开关似乎没发现哪里用到?
------ 应该是 xxxcomponent:assembleRelease 吧?
欢迎运行源码,很多问题分析源码后就能明白了
你可以组件提供的服务(componentservice)中创建一个自定义的抽象类MyFragment,除了继承Fragment之外,还提供刷新数据等抽象方法。组件中具体的MyFragmentImpl继承MyFragment,并完成这些抽象方法的逻辑处理。而在app中是针对MyFragment来编程的,此时除了可以调用Fragment的方法,还可以调用自己定义的方法。
这种模式与装饰器模式十分类似,包括包装来扩展功能。
你可以在地图组件中提供的服务中创建一个自定义的抽象类MapFragment,除了继承Fragment之外,还提供诸如显示覆盖物、删除覆盖物等抽象方法。地图中具体的MyFragment继承MapFragment,并完成这些抽象方法的逻辑处理。然后地图的service将这个MyFragment当做MapFragment的实现类提供出去。
餐饮组件针对MapFragment进行面向抽象编程即可。具体可以参考demo。
在我团队的组件化方案,是多个application来依赖业务module,不会library属性。因为配置越多,越难理解。
----
下面是请教问题:
是否每个业务组件,都发布到maven仓库?还是类似使用compile project()的模式,直接依赖本地代码?(即 compile modulePackage:module@aar到底做了什么)
第二个问题,建议完整运行一下demo,aar是打包之后自动拷贝带componentrelease这个文件夹下面的(这里可以换成maven库),自动拷贝的操作也是由com.dd. comgradle插件完成的。
该方案的核心都在插件的源码中,建议使用插件,很多问题都自动解决了。
1.多个application,入口activity是放在application的,跟module一点关系都没有。一个业务对应一个application,我认为不是累赘,是更清晰的代码边界。
如果你把入口activity和一些初始化代码放在module,那就是module耦合了很多无关代码。但你可以在application做任何事情,多个同事还可以人手一个application,互相不影响。
----
2.你是引用module的 build/outputs/aar/ 里面的文件吗?
第二个问题:目前支持两种方式来管理组件,目前得到更多使用的是直接集成(每次编译本地代码),没有把aar发布到maven仓库,原因在于每个组件还在优化中,这样就可以及时把修改编译打包。方案的创新之处在于虽然是每次都编译本地组件代码,但是没有直接使用compile project()的形式,原因在代码边界章节阐述了。而是通过一个gradle插件来自动引入,判断是否是打包命令,如果是则引入,否则不引入,这样就可以根绝错误调用。
至于使用aar也是同样的道理,不过就是使用aar就不需要每次再编译一次了而已。
(当然 ,可以运行该模块,然后复制build/outputs/aar/*.aar到componentrelease文件夹下的)
//One模块是可以运行的
} else {
//One模块是作为library
RouterService.registerComponent("com.onemodule.OneApplicationLike");
},这个是app里application的,gradle.properties里面有标记One模块是否是library的布尔值,然后后面copy了楼主你的代码,就是没有用到插件,插件代码看得很蒙蔽技术不够还;这样也是可以拿到One模块里的数据,不知道有什么弊端