Android 彻底组件化 demo 发布

作者 | 格竹子

地址 | http://www.jianshu.com/p/59822a7b2fad

声明 | 本文是 格竹子 原创,已获授权发布,未经原作者允许请勿转载



前言

今年 6 月份开始,我开始负责对“得到app”的 android 代码进行组件化拆分,在动手之前我查阅了很多组件化或者模块化的文章,虽然有一些收获,但是很少有文章能够给出一个整体且有效的方案,大部分文章都只停留在组件单独调试的层面上,涉及组件之间的交互就很少了,更不用说组件生命周期、集成调试和代码边界这些最棘手的问题了。有感于此,我觉得很有必要设计一套完整的组件化方案,经过几周的思考,反复的推倒重建,终于形成了一个完整的思路,整理在我的第一篇文章中 Android 彻底组件化方案实践。这两个月以来,得到的 Android 团队按照这个方案开始了组件化的拆分,经过两期的努力,目前已经拆分两个大的业务组件以及数个底层 lib 库,并对之前的方案进行了一些完善。从使用效果上来看,这套方案完全可以达到了我们之前对组件化的预期,并且架构简单,学习成本低,对于一个急需快速组件化拆分的项目是很适合的。现在将这套方案开源出来,欢迎大家共同完善。代码地址:https://github.com/mqzhangw/AndroidComponent


虽说开源的是一个整体的方案,代码量其实很少,简单起见 demo 中做了一些简化,请大家在实际应用中注意一下几点:
(1)目前组件化的编译脚本是通过一个 gradle plugin 提供的,现在这个插件发布在本地的 repo 文件夹中,真正使用的使用请发布到自己公司的maven库
(2)组件开发完成后发布aar到公共仓库,在 demo 中这个仓库用componentrelease 的文件夹代替,这里同样需要换成本地的 maven 库
(3)方案更侧重的是单独调试、集成编译、生命周期和代码边界等方面,我认为这几部分是已发表的组件化方案所缺乏的或者比较模糊的。组件之间的交互采用接口+实现的方式,UI 之间的跳转用的是一个中央路由的方式,在这两方面目前已有一些更完善的方案,例如通过注解来暴露服务以及自动生成 UI 跳转代码等,这也是该方案后面需要着力优化的地方。如果你已经有更好的方案,可以替换,更欢迎推荐给我。


一、AndroidComponent 使用指南

首先我们看一下 demo 的代码结构,然后根据这个结构图再次从单独调试(发布)、组件交互、UI跳转、集成调试、代码边界和生命周期等六个方面深入分析,之所以说“再次”,是因为上一篇文章我们已经讲了这六个方面的原理,这篇文章更侧重其具体实现。



代码中的各个 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,然后运行assembleRelease命令就可以了。这里简单起见没有进行版本管理,大家如果需要自己加上就好了。值得注意的是,发布组件是唯一需要修改isRunAlone=false的情况,即使后面将组件集成到app中,也不需要修改isRunAlone的值,既保持isRunAlone=true即可。所以实际上在Androidstudio中,是可以看到三个application工程的,随便点击一个都是可以独立运行的,并且可以根据配置引入其他需要依赖的组件。这背后的工作都由com.dd.comgradle插件来默默完成。



2 组件交互

在这里组件的交互专指组件之间的数据传输,在我们的方案中使用的是接口+实现的方式,组件之间完全面向接口编程。


在 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 来判断的,判断的规则如下:


  • 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 类(其有一个共同的父类),然后通过 javassisit 在主项目的Application.onCreate() 中插入调用 ApplicationLike.onCreate() 的代码。这样就相当于每个组件在 application 启动的时候就加载起来了。

  • 反射调用的方式是手动在 Application.onCreate() 中或者在其他合适的时机手动通过反射的方式来调用 ApplicationLike.onCreate()。之所以提供这种方式原因有两个:对代码进行扫描和插入会增加编译的时间,特别在 debug 的时候会影响效率,并且这种模式对 Instant Run 支持不好;另一个原因是可以更灵活的控制加载或者卸载时机。


这两种模式的配置是通过配置 com.dd.comgradle的Extension 来实现的,下面是字节码插入的模式下的配置格式,添加 applicatonName 的目的是加快定位 Application 的速度。

combuild {
   applicatonName = 'com.mrzhang.component.application.AppApplication'
   isRegisterCompoAuto = true
}

demo 中也给出了通过反射来加载和卸载组件的实例,在 APP 的首页有两个按钮,一个是加载分享组件,另一个是卸载分享组件,在运行时可以任意的点击按钮从而加载或卸载组件,具体效果大家可以运行 demo 查看。



二、组件化拆分的感悟

在最近两个月的组件化拆分中,终于体会到了做到剥丝抽茧是多么艰难的事情。确定一个方案固然重要,更重要的是克服重重困难坚定的实施下去。在拆分中,组件化方案也不断的微调,到现在终于可以欣慰的说,这个方案是经历过考验的,第一它学习成本比较低,组内同事可以快速的入手,第二它效果明显,得到本来 run 一次需要 8 到 10 分钟时间(不过后面换了顶配 mac,速度提升了很多),现在单个组件可以做到 1 分钟左右。最主要的是代码结构清晰了很多,这位后期的并行开发和插件化奠定了坚实的基础。


总之,如果你面前也是一个庞大的工程,建议你使用该方案,以最小的代价尽快开始实施组件化。如果你现在负责的是一个开发初期的项目,代码量还不大,那么也建议尽快进行组件化的规划,不要给未来的自己增加徒劳的工作量。



与之相关

你好,芒果!使用 RxKotlin 开发的 Dribbble App.

非常有用的资源合集-开发设计再也不愁啦



微信号:code-xiaosheng





公众号

「code小生」


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值