(本文提出的组件化方案已经开源,参见Android完全组件化开源项目)java
今年6月份开始,我开始负责对“获得app”的android代码进行组件化拆分,在动手以前我查阅了不少组件化或者模块化的文章,虽然有一些收获,可是不多有文章可以给出一个总体且有效的方案,大部分文章都只停留在组件单独调试的层面上,涉及组件之间的交互就不多了,更不用说组件生命周期、集成调试和代码边界这些最棘手的问题了。有感于此,我以为颇有必要设计一套完整的组件化方案,通过几周的思考,反复的推倒重建,终于造成了一个完整的思路,整理在个人第一篇文章中Android完全组件化方案实践。这两个月以来,获得的Android团队按照这个方案开始了组件化的拆分,通过两期的努力,目前已经拆分两个大的业务组件以及数个底层lib库,并对以前的方案进行了一些完善。从使用效果上来看,这套方案彻底能够达到了咱们以前对组件化的预期,而且架构简单,学习成本低,对于一个急需快速组件化拆分的项目是很适合的。如今将这套方案开源出来,欢迎你们共同完善。代码地址:https://github.com/luojilab/DDComponentForAndroidandroid
虽然说开源的是一个总体的方案,代码量其实不多,简单起见demo中作了一些简化,请你们在实际应用中注意一下几点: (1)目前组件化的编译脚本是经过一个gradle plugin提供的,如今这个插件发布在本地的repo文件夹中,真正使用的使用请发布到本身公司的maven库 (2)组件开发完成后发布aar到公共仓库,在demo中这个仓库用componentrelease的文件夹代替,这里一样须要换成本地的maven库 (3)方案更侧重的是单独调试、集成编译、生命周期和代码边界等方面,我认为这几部分是已发表的组件化方案所缺少的或者比较模糊的。组件之间的交互采用接口+实现的方式,UI之间的跳转用的是一个中央路由的方式,在这两方面目前已有一些更完善的方案,例如经过注解来暴露服务以及自动生成UI跳转代码等,这也是该方案后面须要着力优化的地方。若是你已经有更好的方案,能够替换,更欢迎推荐给我。git
首先咱们看一下demo的代码结构,而后根据这个结构图再次从单独调试(发布)、组件交互、UI跳转、集成调试、代码边界和生命周期等六个方面深刻分析,之因此说“再次”,是由于上一篇文章咱们已经讲了这六个方面的原理,这篇文章更侧重其具体实现。github
代码中的各个module基本和图中对应,从上到下依次是:编程
图中没有体现的module有两个,一个是componentlib,这个是咱们组件化的基础库,像Router/UIRouter等都定义在这里;另外一个是build-gradle,这个是咱们组件化编译的gradle插件,也是整个组件化方案的核心。bash
咱们在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。实际上这个插件还能作更“智能”的事情,这个在集成调试章节中会详细阐述。app
单独调试所必须的AndroidManifest.xml、application、入口activity等类定义在src/main/runalone下面,这个比较简单就不赘述了。maven
若是组件开发并测试完成,须要发布一个release版本的aar文件到中央仓库,只须要把isRunAlone修改成false,而后运行assembleRelease命令就能够了。这里简单起见没有进行版本管理,你们若是须要本身加上就行了。值得注意的是,发布组件是惟一须要修改isRunAlone=false的状况,即便后面将组件集成到app中,也不须要修改isRunAlone的值,既保持isRunAlone=true便可。因此实际上在Androidstudio中,是能够看到三个application工程的,随便点击一个都是能够独立运行的,而且能够根据配置引入其余须要依赖的组件。这背后的工做都由com.dd.comgradle插件来默默完成。ide
在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来实现,不一样的是这里增长了一个优先级的概念。具体的实现就不在这里赘述了,代码仍是很清晰的。
页面的跳转经过短链的方式,例如咱们要跳转到share页面,只须要调用
UIRouter.getInstance().openUri(getActivity(), "componentdemo://share", null);
复制代码
具体是哪一个组件响应componentdemo://share这个短链呢?这就要看是哪一个组件处理了这个schme和host,在demo中share组件在本身实现的ShareUIRouter中声明了本身处理这个短链,具体代码以下:
private static final String SCHME = "componentdemo";
private static final String SHAREHOST = "share";
public boolean openUri(Context context, Uri uri, Bundle bundle) {
if (uri == null || context == null) {
return true;
}
String host = uri.getHost();
if (SHAREHOST.equals(host)) {
Intent intent = new Intent(context, ShareActivity.class);
intent.putExtras(bundle == null ? new Bundle() : bundle);
context.startActivity(intent);
return true;
}
return false;
}
复制代码
在这里若是已经组件已经响应了这个短链,就返回true,这样更低优先级的组件就不会接收到这个短链。
目前根据schme和host跳转的逻辑是开发人员本身编写的,这块后面要修改为根据注解生成。这部分已经有一些优秀的开源项目能够参考,如ARouter等。 ###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来判断的,判断的规则以下:
上面的内容要实现的目的就是每一个组件能够直接在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提供了两种方式,字节码插入和反射调用。
这两种模式的配置是经过配置com.dd.comgradle的Extension来实现的,下面是字节码插入的模式下的配置格式,添加applicatonName的目的是加快定位Application的速度。
combuild {
applicatonName = 'com.mrzhang.component.application.AppApplication'
isRegisterCompoAuto = true
}
复制代码
demo中也给出了经过反射来加载和卸载组件的实例,在APP的首页有两个按钮,一个是加载分享组件,另外一个是卸载分享组件,在运行时能够任意的点击按钮从而加载或卸载组件,具体效果你们能够运行demo查看。
在最近两个月的组件化拆分中,终于体会到了作到剥丝抽茧是多么艰难的事情。肯定一个方案当然重要,更重要的是克服重重困难坚决的实施下去。在拆分中,组件化方案也不断的微调,到如今终于能够欣慰的说,这个方案是经历过考验的,第一它学习成本比较低,组内同事能够快速的入手,第二它效果明显,获得原本run一次须要8到10分钟时间(不事后面换了顶配mac,速度提高了不少),如今单个组件能够作到1分钟左右。最主要的是代码结构清晰了不少,这位后期的并行开发和插件化奠基了坚实的基础。
总之,若是你面前也是一个庞大的工程,建议你使用该方案,以最小的代价尽快开始实施组件化。若是你如今负责的是一个开发初期的项目,代码量还不大,那么也建议尽快进行组件化的规划,不要给将来的本身增长徒劳的工做量。