组件化这种概念也提出来好久了,如今的商用项目基本都是基于组件化来进行开发了,面试时问到它的机率也比较大,这么经典的东东可惜我还木有将其记录下一来,所以接下来会花些时间来对组件化的各个细节进行整理。
模块化、组件化、插件化概念了解:
对于这三者的概念其实已经烂大街了,不过还是先用文字对它们进行一个总结。
模块化:根据不同的关注点,将一个项目的可以共享的部分抽取出来,形成独立的Module,这就是模块化。模块化不只包含公共部分,当然也可以是业务模块。比如:图片加载模块。注意:这里木有module跳module的情况。
组件化:组件化是建立在模块化思想上的一次演进,一个变种。组件化本来就是模块化的概念。核心是模块角色的可转化换性,在打包时,是library;调试时,是application。组件化的单位是组件,这里跟模块化的一个最大的区别就是组件之间是可以相互进行跳转的,其实现就是通过路由,这个在之后会手动来实现这个路由的功能。它是一个编译时的行为。
插件化:严格意义来讲,其实也算是模块化的观念。将一个完整的工程,按业务划分为不同的插件,来化整为零,相互配合。插件化的单位是apk(一个完成的应用)。可以实现apk 的动态加载,动态更新,比组件化更灵活。它是一个运行时的行为,这个在之后也会花个专篇来整理它的。
DEMO实现的效果:
最终的工程的结果如下:
![](https://i-blog.csdnimg.cn/blog_migrate/4810955e9d92ed92b3c579f54886555a.png)
其中对于module1和module2这两个组件可以有两种模式,如上面的概念所说:“在打包时,是library;调试时,是application”,所以在工程中有个配置可以配置模式:
![](https://i-blog.csdnimg.cn/blog_migrate/ad1a83e848fc3a7a6154bcca2fbbe2c1.png)
至于到底怎么来动态通过配置来达到切换模式的效果,这里先忽略,之后会一步步来实现的。
集成模式效果演示:
Activity之间的跳转:
那下面先来看一下集成模式的运行效果:
![](https://i-blog.csdnimg.cn/blog_migrate/db802c1b6411d386220e8768bb2a5aff.png)
![](https://i-blog.csdnimg.cn/blog_migrate/7e2d7409e638f3fd973feb41a56dba35.gif)
是不是看上图跳着晕晕的?下面用静态图稍加说明一下,先明白最终我们要实现的效果,有了目标学习起来有动力:
![](https://i-blog.csdnimg.cn/blog_migrate/dc35a10ec626dc1ed806b4f2232e69bc.png)
![](https://i-blog.csdnimg.cn/blog_migrate/46d7d4fc37d5c10c3108b1728912358c.png)
注意:这里内部的跳转依然是通过路由,而非我们通常的跳转,也就是最终咱们实现的路由既支持多module之间的跳转,也支持同module内部的跳转,好,继续再看其它的场景:
![](https://i-blog.csdnimg.cn/blog_migrate/5defb995e7dd474460466ddde74e36c6.png)
其中要跳转的module1中的Activity的界面长这样:
![](https://i-blog.csdnimg.cn/blog_migrate/11cea0144e3f9c5ecb1a2e7e755593ca.png)
此时它可以跨module跳到主app module中,又可以跳到另一个mududle模块。
同样的:
![](https://i-blog.csdnimg.cn/blog_migrate/df5610f01d47d4ce4a4d1d7f5cfee153.png)
它会跳到module2中的界面:
![](https://i-blog.csdnimg.cn/blog_migrate/aff14546d3c380b21b446dd36702b90b.png)
而它里面的功能跟module1中的一样,其核心就是通过路由来实现各module之间的跳转。
各Module之间方法调用:
除了在不同的module中可以相互调用Activity之外,咱们手写的路由还可以实现各module之间方法的调用,至于效果这个待之后咱们实现自己的路由之后再来观察。
组件模式效果演示:
这个只以module2为例,module1的效果是一模一样的,先切换一个模式:
![](https://i-blog.csdnimg.cn/blog_migrate/729e22c1a6d061c206ab05b82c705c03.png)
也就是对于组件化,每一个module其实是可以单独运行的,这样就能很好的实现模块之间的解耦,而且调试也比较方便,当然啦,目前我所经历的公司在商用时还木有这样使用过,待未来再来体会它的好处。
具体实现:
接下来则开启咱们的撸码,一点点从0开始来实现最终我们想要的组件化的效果。
组件化配置:
工程搭建:
先新建一个工程,然后里面新建2个Module,如开篇所呈现的那样:
![](https://i-blog.csdnimg.cn/blog_migrate/961377e9bd2efab9e875f3a2d2a593a3.png)
这里新建Module注意是要选它:
![](https://i-blog.csdnimg.cn/blog_migrate/4aaab17fa02237e196280fbcf830341f.png)
![](https://i-blog.csdnimg.cn/blog_migrate/0dc2bf280e8aab7efbe55bcbe421548f.png)
gradle组件化配置:
对于app、module1和module2,它们有各自的gradle配置,但是应该将其统一一下好进行版本的维护,比如:
![](https://i-blog.csdnimg.cn/blog_migrate/8b646876bd5243f50abfb3b6cb1f379d.png)
那怎么提取到公共的地方呢?这里新建一个gradle文件专门用来进行这些公共属性的配置的,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/0c6226d7f4e50e1a0e08a80a9bea9a2a.png)
然后里面定义好相关的属性:
ext { //extend
isModule = false // false: 组件模式;true :集成模式
android = [
compileSdkVersion: 28,
buildToolsVersion: "28.0.0",
minSdkVersion : 15,
targetSdkVersion : 28,
versionCode : 1,
versionName : "1.0"
]
appId = ["app" : "com.android.componentarchstudy",
"module1": "com.android.module1",
"module2": "com.android.module2"]
appcompatLibrary = "1.0.2"
constraintlayoutLibrary = "1.1.3"
junitLibrary = "4.12"
androidTestJunitLibrary = "1.1.0"
androidTestEspressoLibrary = "3.1.1"
dependencies = [
"appcompat" : "androidx.appcompat:appcompat:${appcompatLibrary}",
"constraintlayout" : "androidx.constraintlayout:constraintlayout:${constraintlayoutLibrary}",
"junit" : "junit:junit:${junitLibrary}",
"androidtestjunit" : "androidx.test.ext:junit:${androidTestJunitLibrary}",
"androidtestespresso": "androidx.test.espresso:espresso-core:${androidTestEspressoLibrary}",
]
}
其中将appId也提出来了,这样也达到一个统一管理的目的,公共属性在这个配置文件中定义好之后,怎么来应用呢?如下:
![](https://i-blog.csdnimg.cn/blog_migrate/7a57d3723e4a60a2ffcc2bace196334a.png)
引入之后接下来则可以在不同的module中来进行引用替换了,如下:
app的gradle配置:
apply plugin: 'com.android.application'
def cfg = rootProject.ext.android
def appId = rootProject.ext.appId
def dep = rootProject.ext.dependencies
android {
compileSdkVersion cfg.compileSdkVersion
buildToolsVersion cfg.buildToolsVersion
defaultConfig {
applicationId appId["app"]
minSdkVersion cfg.minSdkVersion
targetSdkVersion cfg.targetSdkVersion
versionCode cfg.versionCode
versionName cfg.versionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation dep.appcompat
implementation dep.constraintlayout
testImplementation dep.junit
androidTestImplementation dep.androidtestjunit
androidTestImplementation dep.androidtestespresso
}
module1的gradle配置:它跟app的gradle配置基本上是一样的,只是appid不一样:
apply plugin: 'com.android.application'
def cfg = rootProject.ext.android
def appId = rootProject.ext.appId
def dep = rootProject.ext.dependencies
android {
compileSdkVersion cfg.compileSdkVersion
buildToolsVersion cfg.buildToolsVersion
defaultConfig {
applicationId appId["module1"]
minSdkVersion cfg.minSdkVersion
targetSdkVersion cfg.targetSdkVersion
versionCode cfg.versionCode
versionName cfg.versionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation dep.appcompat
implementation dep.constraintlayout
testImplementation dep.junit
androidTestImplementation dep.androidtestjunit
androidTestImplementation dep.androidtestespresso
}
module2的gradle配置:同样相比之下只是appid这块不同
apply plugin: 'com.android.application'
def cfg = rootProject.ext.android
def appId = rootProject.ext.appId
def dep = rootProject.ext.dependencies
android {
compileSdkVersion cfg.compileSdkVersion
buildToolsVersion cfg.buildToolsVersion
defaultConfig {
applicationId appId["module2"]
minSdkVersion cfg.minSdkVersion
targetSdkVersion cfg.targetSdkVersion
versionCode cfg.versionCode
versionName cfg.versionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation dep.appcompat
implementation dep.constraintlayout
testImplementation dep.junit
androidTestImplementation dep.androidtestjunit
androidTestImplementation dep.androidtestespresso
}
以上整体都统一之后,接下来则需要对module1和module2进行单独处理了,因为它需要支持组件和集成两种模式,这里再来解释下这两种模式的特点:组件模式就是module可以单独生成apk运行的;而集成模式是该module是一个library供app来使用,无法单独运行,最终整个工程只会生成一个apk的。那如何来设置呢?其实很简单,根据配置来动态控制一下这两处既可:
![](https://i-blog.csdnimg.cn/blog_migrate/ffa5beea2736ffb232059c1a0f627621.png)
而控制的配置属性也已经在config.gradle中定义了,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/3eed6009eb35b02bb8d6e1bd2cb88fef.png)
所以,咱们来修改一下:
![](https://i-blog.csdnimg.cn/blog_migrate/78f0627012f568ba458542dbf365a19b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/b888b6c2cf753ca5ad46de5aad3f2150.png)
是不是已经完美可以实现了模式的切换了?其实不然,这里还有一个关于清单文件的区别,先试想一下:
如果是集成模式的话,是不是清单文件中没有自己的Application,只是有一些清单的注册如Activity,也没有带启动intent的Activity?相反如果是组件模式的话这些都应该有的,所以不同模式下其manifest文件的内容也不一样,那怎么来达到灵活切换的目的呢?其实很简单,定义两个manifest文件,然后在gradle中根据条件来动态指定用哪一个manifest既可,先来定义两个manifest:
![](https://i-blog.csdnimg.cn/blog_migrate/95234e953e52fa00b770066098db9217.png)
那接下来对于组件模式的manifest那怎么定义呢?直接在某个目录中新建既可,不过这里新建目录需要这样来建,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/445e48bc98de4dd6f6481d59569ea80b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/316d390414d4404661114d85ab7fff39.png)
直接点finish既可:
![](https://i-blog.csdnimg.cn/blog_migrate/5dbac811603084536b0bee50f46e0a31.png)
然后咱们在里面新添一个manifest文件:
![](https://i-blog.csdnimg.cn/blog_migrate/3e28bbfc45bac8e3d9acae297d323a84.png)
另外,对于组件模式下的Application通常是需要自定义的,而这个自定义的类在集成模式下是不需要的,那。。又如何搞呢?这里将组件模式下需要的类都放到module文件下的java目录下既可,比如咱们自定义一个Application,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/70d71f6228eda40474ed6a4dfaa648f9.png)
然后在manifest中定义一下:
![](https://i-blog.csdnimg.cn/blog_migrate/0f7c756a5ca22d84c267df5ea3d80e71.png)
好,文件定义好了,接下来就是怎么来动态配置的问题了,这里回到module1的gradle配置中,具体配置如下:
![](https://i-blog.csdnimg.cn/blog_migrate/b89b98cf00e81e0f349e6839a1ce29a8.png)
module2也一样:
![](https://i-blog.csdnimg.cn/blog_migrate/3b8fd5bc4c60811b6fc35b8f7eebd60b.png)
好,此时看一下集成模式下编译后的样子:
![](https://i-blog.csdnimg.cn/blog_migrate/4cf6a12825885cb8880dd82e194c8344.png)
那如果改为组件模式编译后呢?
![](https://i-blog.csdnimg.cn/blog_migrate/c1bc46fa94351e10a139bb5dd51bd8bf.png)
![](https://i-blog.csdnimg.cn/blog_migrate/b91d56bf37c2058ecccd03fdc53b3bf1.png)
最后还有一个主模块那块的依赖需要做下判断:
![](https://i-blog.csdnimg.cn/blog_migrate/6280a0b359d2915d6d915fe7a0d058c2.png)
手动实现路由之注解处理器(AnnotationProcessor)学习:
如开篇所示的工程结构中出现了如下几个东东:
![](https://i-blog.csdnimg.cn/blog_migrate/d87814c9de5b7dca37660bdfe17870de.png)
其实它都是路由实现的核心,如开篇所描述的概念,对于组件化有一个很重要的特点就是各Module之间是可以相互通讯的,而Module与Module之间是相互独立的,那怎么能实现通讯的效果呢?此时路由功能就出现了,市面上关于组件化路由框架其实有很多,对于我自己公司就用过两个,一个是自己内部公司实现的,还有一个是cc(https://github.com/luckybilly/CC)框架。而市面上还有一款大名鼎鼎阿里巴巴出品的Arouter也是被商用APP大量使用的,而这里呢,咱们是手动来实现Arouter这样的一个路由框架的功能,说实话我实际项目中并未用过它,但是这么好我东东有必要对其原理进行剖析一下,这里的研究改变一下策略,先不去看Arouter的使用,待自己实现了咱们预期DEMO的效果之后,再来对着官方的框架进行对比,这样学习也会比较深刻。
对于咱们要实现的路由功能来说,很重要的一个技术点就是使用了注解处理器(AnnotationProcessor),其实关于它的使用,在当时https://www.cnblogs.com/webor2006/p/10582178.html手写ButterKnife框架时已经有所耳闻,不过细节已经忘得差不多了,所以接下来对于这块的知识再来巩固一下,为之后的路由框架实现打下良好的基础,一些基础知识就不多说了。下面开始:
首先先新建一个自定义注解的java library,注意不是module哈:
![](https://i-blog.csdnimg.cn/blog_migrate/62cc83fbbb6a63cf669247f9170abc0a.png)
![](https://i-blog.csdnimg.cn/blog_migrate/e19fc0cc244bb3053fc12f9f07eff332.png)
然后还要新建一个注解处理器的模块,也是java library:
![](https://i-blog.csdnimg.cn/blog_migrate/d5c7df442a65493b1e3239a222021a7e.png)
其中router_compiler需要依赖于router_annotation:
![](https://i-blog.csdnimg.cn/blog_migrate/2968f4ad183f9cc9555156e24c427783.png)
接下来则需要定义Annotation,这里需要定义两个注解,最终是用到这上面的:
![](https://i-blog.csdnimg.cn/blog_migrate/5931f1aff6c29cfa2ad91020761ad609.png)
这些注解肯定都是只存在于编译期的,跟之前学习的Butterknife差不多,如何定义注解就不多说了,所以注解定义如下:
package com.android.router_annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//添加元注解
// @Target(ElementType.TYPE) //接口、类、枚举、注解
// @Target(ElementType.FIELD) //字段、枚举的常量
// @Target(ElementType.METHOD) //方法
// @Target(ElementType.PARAMETER) //方法参数
// @Target(ElementType.CONSTRUCTOR) //构造函数
// @Target(ElementType.LOCAL_VARIABLE)//局部变量
// @Target(ElementType.ANNOTATION_TYPE)//注解
// @Target(ElementType.PACKAGE) ///包
@Target(ElementType.TYPE)
//注解的生命周期
//RetentionPolicy.SOURCE 源码阶段
//RetentionPolicy.CLASS 编译阶段
//RetentionPolicy.RUNTIME 运行阶段
@Retention(RetentionPolicy.CLASS)
public @interface Route {
/**
* 路由的路径,标识一个路由节点
*/
String path();
/**
* 将路由节点进行分组,可以实现按组动态加载
*/
String group() default "";
}
package com.android.router_annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.CLASS)
public @interface Extra {
String name() default "";
}
接下来则需要定义一个注解处理器了:
![](https://i-blog.csdnimg.cn/blog_migrate/fb0c876adb49aa3656bbeafa2bd692aa.png)
接下来则需要将这个注解处理器进行注册,这样在gradle编译时才会用这个注解处理器来为我们办事,先来回顾一下之前在学习ButterKnife时是怎么注册的?
![](https://i-blog.csdnimg.cn/blog_migrate/7660065b4d5f4f6589e1b1f17e9c4f96.png)
需要构造一个这个固定的目录才行,而这个文件中就写入咱们的这个注解处理类的全类名既可,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/d78b0d8be577cb243cdfc1d093d26b5d.png)
以上则为整个注册处理器的注册,当时咱们是全手工的方式来搞的,这次咱们升华一下,其实是可以引用一个三方的注解处理器自动的进行注册,先来引用个注解处理器:
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
![](https://i-blog.csdnimg.cn/blog_migrate/07dae576604fe89fad0a6b399d07071f.png)
此时就可以使用如下注解了:
![](https://i-blog.csdnimg.cn/blog_migrate/a1a201eeb5b3868f7b7573ef6ee57b2d.png)
该注解的作用为:在这个类上添加了@AutoService注解,它的作用是用来生成META-INF/services/javax.annotation.processing.Processor文件的,也就是我们在使用注解处理器的时候需要手动添加META-INF/services/javax.annotation.processing.Processor,而有了@AutoService后它会自动帮我们生成。AutoService是Google开发的一个库,使用时需要在factory-compiler中添加依赖。
好,对于这个注解处理器有几个方法需要我们重写的:
![](https://i-blog.csdnimg.cn/blog_migrate/242f3e8cb05fd97677c29cd2e6cf6c7a.png)
其中对于上面的getSupportedSourceVersion()和getSupportedAnnotationTypes()也可以用注解的方式来指定,咱们这次全用新式的方式来编写,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/d60905fa22e1afd361c47361c60a3027.png)
其中将常量提取到Constants中了,注意提供的注解类型是类的全路径,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/e44c20504b2aa64e2066af5ca0440101.png)
好,那咱们注册的这个处理器有木有生效呢?下面咱们来在代码中使用一下,并在处理器中增加一些日志,然后编译看一下我们打印的日志是否能正常输出,能输出那肯定咱们注解处理器是已经注册好了。
因为要在我们的Activity中使用注解,首先app中依赖一下这个注解模块:
![](https://i-blog.csdnimg.cn/blog_migrate/465ae4c1b510c4dfc9dafd07d574c244.png)
然后在咱们的注解处理器中增加日志便于观察:
![](https://i-blog.csdnimg.cn/blog_migrate/82d8653f9d2bf8afda190f826d353b27.png)
然后日志需要初始化一下,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/07a68a2e7bed7d10ed4e2da3989a1ca4.png)
另外需要将模式改为集成模式:
![](https://i-blog.csdnimg.cn/blog_migrate/145738c2431068e48dbbe27bff67a0dc.png)
最后app模块还需要将我们的处理器组件给依赖一下:
![](https://i-blog.csdnimg.cn/blog_migrate/16004c4ec929434de37808e717afe9d6.png)
好,一切就绪,咱们来编译一下,看能否看到我们的日志:
![](https://i-blog.csdnimg.cn/blog_migrate/905b7e2ecae69ea9fd37c78f34e8209a.png)
![](https://i-blog.csdnimg.cn/blog_migrate/9cc70b56b5f48ad128a9d9d9da6b9122.png)
原因是有个依赖还少了一句代码:
![](https://i-blog.csdnimg.cn/blog_migrate/06dfb4cf717f5afb950524b31f273c27.png)
此时再编译看一下:
![](https://i-blog.csdnimg.cn/blog_migrate/7429eebdf73a3ca6cbd9fb39c5b797b0.png)
嗯,确实是输出了,另外看一下注解处理器是否自动生成了注册目发了?
![](https://i-blog.csdnimg.cn/blog_migrate/a83e981a96f96a76554d3079a8946079.png)
说明咱们目前的注解处理器已经成功注册了,接下来则只需要关注我们的业务逻辑既可。接下来的核心则是编写这个注解处理器的逻辑了,放下次继续了。