1.模块化、组件化、插件化项目开发到一定程度,随着人员的增加,代码越来越臃肿,这时候就需要拆分模块化了。在我看来,模块化是一个指导性的概念,其核心思想是分而治之,降低耦合。至于如何在Android项目中实现,目前有两种方式和两种流派,一种是组件化,一种是插件化。说到组件化和插件化的区别,有一张很形象的图:上图看起来比较清楚,但是容易引起一些误解。有几个小问题,图中可能看不清楚:组件化是一个整体吗?没有头和胳膊它能存在吗?在左图中,似乎组件化是一个有机的整体,只有所有器官都活着才能存在。事实上,组件化的目标之一就是减少整体(app)和器官(components)之间的依赖。没有任何器官,应用程序可以正常存在和运行。头和胳膊能单独存在吗?左图没说清楚,但答案应该是肯定的。每个器官(组成部分)在完成一些基本功能后可以独立生存。这就是组件化的第二个目标:组件可以独立运行。组件化和插件化都可以用右图来表示吗?如果以上两个问题的答案都是YES,那么这道题的答案自然是YES。每个组件都可以看作是一个独立的整体,也可以根据需要与其他组件(包括主工程)集成,形成一个应用程序。右图中的小机器人可以动态添加和修改吗?如果组件化和插件化都用右图来表示,那么这个问题的答案就不一样了。对于组件化,这个问题的答案部分是肯定的,即可以在编译时动态添加和修改,但不能在运行时进行。至于插件,这个问题的答案很简单,就是绝对可以,不管是编译时还是运行时!本文主要关注组件化的实现思路,不讨论插件化的技术细节。我们刚从上面的问答中得出一个结论:组件化和插件化最大的区别(应该是唯一的区别)就是组件化没有运行时动态添加和修改组件的功能,而是插件化-in是可能的。抛开插件的“道德”批评,我认为对于一个Android开发者来说,插件确实是一个福音,它会给我们很大的灵活性。但是由于目前还没有完全适合完全兼容的插件方案(RePlugin的饥饿营销做的很好,但是还没看到效果),尤其是对于一个几十万代码的成熟产品换句话说,应用任何插件解决方案都是非常危险的。所以我们决定先从组件化入手,本着做最彻底的组件化解决方案的思路重构代码。以下是最近思考的结果。欢迎提出建议和意见。2、如何实现组件化要实现组件化,无论采用何种技术路径,需要考虑的问题主要有以下几点:代码解耦。一个庞大的项目如何拆分成一个有机的整体?组件独立运行。上面说到每个组件都是一个完整的整体,如何让它独立运行和调试呢?数据传输。因为每个组件都会为其他组件提供服务,那么如何在主工程(Host)与组件、组件与组件之间传递数据呢?界面跳转。UI跳转可以认为是一种特殊的数据传递,在实现思路上有什么区别?组件生命周期。我们的目标是实现组件的按需动态使用,所以会涉及到组件加载、卸载和降维的生命周期。集成调试。开发阶段如何按需编译组件?一次调试可能只有一两个组件参与集成,这样编译时间会大大减少,开发效率会提高。代码隔离。如果组件之间的交互还是直接引用,那么组件根本就没有解耦。如何从根本上避免组件之间的直接引用?即如何从根本上杜绝耦合的发生?只有这样做,才是完整的组件化。2.1代码解耦对于庞大的代码拆分,Androidstudio可以提供很好的支持。使用IDE中的多模块功能,我们可以很容易地初步拆分代码。这里我们区分两个模块:一个是基础库,这些代码直接被其他组件引用。例如,网络库模块可以被认为是一个库。还有一种我们称之为Component,这种模块就是一个完整的功能模块。比如阅读或者分享一个模块就是一个Component。为了方便,我们统一称库为依赖库,Component为组件。我们所说的组件化,主要是针对Component的类型。它负责组装这些组件以形成一个完成应用程序的模块。一般我们称之为主项目、主模块或者Host。为了方便,我们也称它为主工程。经过简单的思考,我们或许可以将代码拆分成如下结构:组件化简单拆分。这种拆分比较容易做到。从图片上看,阅读、分享等已经拆分成组件。并且共同依赖公共依赖库(为了简单只画了一个),然后这些组件被主工程引用。阅读、分享等组件之间没有直接联系。我们可以认为已经实现了组件之间的解耦。不过这个图中有几个问题需要指出:从上图中,我们似乎认为组件只有集成到主工程中才能使用。其实我们希望每个组件都是一个整体,可以独立运行和调试。那么如何进行单独调试呢?主工程可以直接引用组件吗?也就是说,我们可以直接使用compileproject(:reader)来引用组件吗?如果是这样的话,那么主项目和组件的耦合就没有消除。上面我们说了,组件是可以动态管理的。如果我们删除reader(阅读)组件,那么主工程就无法编译了。动态管理呢?因此不允许直接引用主项目中的组件。但是我们的阅读组件最终还是会被放到apk里面。不仅代码要合并到claases.dex中,资源也会通过meage操作合并到apk的资源中。没有相互参照或相互作用吗?阅读组件也会调用分享模块,图中根本没有体现,那么组件之间是如何交互的呢?我们稍后会一一解决这些问题。首先,我们看一下代码解耦的作用是什么?肯定不能像上面那样直接引用和使用里面的类。因此,我们认为代码解耦的首要目标是组件之间的完全隔离。我们不仅不能直接使用其他组件中的类,而且我们可能根本不了解实现细节。只有这种程度的解耦才是我们所需要的。2.2组件单独调试其实单独调试比较简单,把applyplugin:'com.android.library'切换成applyplugin:'com.android.application'即可,但是我们还需要修改AndroidManifest文件,因为一个单独调试需要一个带有条目的活动。我们可以设置一个变量isRunAlone来标记是否需要单独调试。根据isRunAlone的值,我们可以使用不同的gradle插件和AndroidManifest文件,甚至添加Application等Java文件,这样我们就可以进行初始化操作了。为了避免不同组件之间的资源名称重复,在每个组件的build.gradle中添加resourcePrefix"xxx_",固定每个组件的资源前缀。下面是读取组件的build.gradle示例:if(isRunAlone.toBoolean()){applyplugin:'com.android.application'}else{applyplugin:'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组件的数据传输上面我们提到了主工程和组件,组件不能直接使用类的相互引用进行数据交互。那么如何实现这种隔离呢?这里我们采用接口+实现的结构。每个组件声明它提供的服务。这些服务是一些抽象类或接口。组件负责在一个统一的路由Router中实现和注册这些服务。如果要使用某个组件的功能,只需要向Router请求这个Service的实现即可。我们根本不关心具体的实现细节,只要能返回我们需要的结果即可。这和Binder的C/S架构很相似。因为我们组件之间的数据传递是基于接口编程的,接口和实现是完全分离的,所以组件可以解耦,我们可以对组件进行动态的管理,比如替换,删除。有几个小问题需要搞清楚:组件如何暴露自己提供的服务?在项目中,为了简单起见,我们专门建立了一个组件服务依赖库,它定义了各个组件提供的服务和一些公共模型。整合所有组件的服务是为了让拆分前期的操作更简单,后期需要通过自动化的方式生成。这个依赖库需要严格遵循开闭原则,避免版本兼容等问题。服务的具体实现是由它所属的组件注册到Router中的,那么什么时候注册呢?这涉及到组件加载的生命周期等等,我们后面会介绍。一个容易犯的小错误就是以持久化的方式传输数据,比如file,sharedpreference等,这个需要避免。下面是添加数据传输功能后的架构图:组件间的数据传输2.4组件间的UI跳转可以说,UI跳转也是组件提供的一种特殊服务,可以归结为上面的数据传输go。但是我们会单独处理一般UI的跳转,一般会使用短链来跳转到具体的Activity。每个组件可以注册它可以处理的短链的schme和host,并定义传输数据的格式。然后注册到统一的UIRouter,UIRouter通过schme和host的匹配关系负责分发路由。UI跳转部分的具体实现是为每个Activity添加注解,然后通过apt形成具体的逻辑代码。这也是Android中UI路由的主流实现方式。2.5组件生命周期由于我们要对组件进行动态管理,所以我们为每个组件添加了几种生命周期状态:加载、卸载和降维。为此,我们为每个组件添加了一个ApplicationLike类,它定义了两个生命周期函数onCreate和onStop。Loading:如前所述,每个组件负责在Router中注册自己的服务实现,其具体实现代码写在onCreate方法中。那么主工程调用的onCreate方法称为组件加载,因为onCreate方法一旦执行,组件就在Router中注册了自己的服务,其他组件可以直接使用这个服务。卸载:卸载与加载基本相同,不同的是调用了ApplicationLike的onStop方法,每个组件从Router上注销自己的服务实现。不过这种使用场景可能比较少见,一般适用于一些只使用一次的组件。降维:降维用在极少数场景下,比如某个组件出现问题,我们想把这个组件从本地实现改成wap页面。降维一般需要后台配置才能生效。可以查看onCreate中的在线配置。如果需要降维,则将所有UI跳转到配置好的wap页面。一个小细节是主工程负责加载组件。既然主工程和组件是隔离的,那么主工程如何调用组件ApplicationLike的生命周期方法呢?目前我们使用的是编译时基于字节码插入的方式,扫描所有的ApplicationLike类(有一个共同的父类),然后通过javassisit在主工程的onCreate中插入调用ApplicationLike.onCreate的代码。再优化一下组件化架构图:组件生命周期2.6集成调试每个组件单独调试通过不代表集成没有问题,所以后期开发验证需要将几个组件集成到一个app中.由于我们上面的机制保证了组件之间的隔离,所以我们可以任意选择几个组件参与集成。这种按需加载机制可以保证集成调试的极大灵活性,并且可以大大加快编译速度。我们的做法是这样的,每个组件开发完成后,发布一个relaeseaar到公共仓库,一般是本地的maven库。然后主工程通过参数配置要集成的组件。那么我们稍微改变一下组件和主工程的连接线,最终的组件化架构图如下:最终结构图2.7代码隔离此时我们在回顾一下我们第一次拆分组件化时提出的三个问题。应该说是找到了解决办法,但是还有一个隐患没有解决,就是能否使用compileproject(xxx:reader.aar)引入组件?虽然我们在数据传输章节使用了接口+实现的架构,components是需要针对接口进行编程的,但是一旦我们引入了reader.aar,那么我们就可以直接使用里面的实现类了,所以我们对于接口编程的规范就变成了一封死信千里之堤,毁于一蚁巢。只要有代码(不管是有意还是无意)这样做,我们之前的工作就白费了。我们希望只在assembleDebug或assembleRelease中引入aar,在开发阶段,所有的组件都是不可见的,从根本上杜绝引用实现类的问题。我们把这个问题交给gradle来解决,我们创建一个gradle插件,然后各个组件应用这个插件,插件的配置代码比较简单://根据配置,并自动生成组件加载代码(module))){//添加组件依赖project.dependencies.add("compile","xxx:reader-release@aar")//插入字节码的部分也在这里实现}}privateAssembleTaskgetTaskInfo(List
