前言
组件化开发现在基本上属于基础操作了,大家一般都会使用 ARouter 、LiveDataBus 作为组件化通信的解决方案,那为什么会选择ARouter,ARouter又是怎么实现的呢?这篇文章主要就 搭建组件化开发的准备工作 、组件化跳转分析,如果理解了这篇文章,对于查看ARouter源码应该是会有很大的帮助的。至于ARouter等分析,网上有很多的讲解,这里就不分析ARouter源码了,文章末尾会给出ARouter源码时序图和总结,可忽略。
ps: 为什么写本文,因为笔者最近被问道,为什么要用ARouter,ARouter它到底是解决什么问题,你能就一个点来分析吗?被问到该问题了?笔者是从它的跳转回答的,毕竟跳转简单。刚好记录并回忆一下。
参考资料
目录
一、组件化优势
组件化的优势想必大家都知道,可以总结为四点
-
编译速度 我们可以按需求测试单一业务模块,而不需要整体打包运行,节约了实践,有效的提升了我们的开发速度
-
解耦 极度的降低了模块之间的耦合,便于后期维护与更新,当产品提出一个新业务时,完全可以新建一个业务组件,集成和摒弃都很方便
-
功能重用 某一块的功能在另外的组件化项目中使用只需要单独依赖这一模块即可
-
团队开发效率 组件化架构是团队开发必然会选择的一种开发方式,它能有效的使团队更好的协作
二、组件化开发准备工作
组件化开发,一般可以分为三层,分别为 壳工程、业务组件、基础依赖库,业务组件间互不关联,并且业务组件需要可以单独运行测试,整体都是围绕解耦来开展的,下面开始进行组件化开发前所需要做的准备工作
1、包名和资源文件命名冲突问题
需要制定规范,对包名和项目模块的划分规范化,不同模块内不能有相同名字的类文件,避免打包失败等冲突问题
2、Gradle中的版本好统一管理
接下来的写法是最普遍的,就是有点小瑕疵:不支持 AS 的自动补充功能,也无法使用代码自动跟踪,因此可以考虑使用 buildSrc。buildSrc 是 Android 项目中一个比较特殊的 project,在 buildSrc 中可以编写 Groovy 语言。
在我们创建的模块中,有一些,例如 compileSdkVersion 、buildToolsVersion 或者是集成的第三方依赖库,它们都有对应的版本号,如果不进行统一管理,后续维护很麻烦,总不能对所有模块一个个手动修改版本。所以我们可以在gradle.properties文件中,添加配置,例如
gradle.properties
CompileSdkVersion = 30// 这里不能和compileSdkVersion 一样,会报错
模块的build.gradle
android{
compileSdkVersion CompileSdkVersion.toInteger()
}
所有模块版本号都按照上面的写,每次改版本号都按照gradle.properties里面定义的修改就好。但是,细心的你一定会发现,现在网上的例子,这些写的很少,既然这样写也能做到统一管理,为什么不推荐呢?答案就在 CompileSdkVersion.toInteger()
这里,这里拿到CompileSdkVersion后还需要转换,如果使用下面创建gradle文件的做法,完全可以省去。
在项目根目录下新建一个conffig.gradle 文件,和全局build.gradle同一层级
config.gradle
ext{
android=[ compileSdkVersion:29, buildToolsVersion:'29.0.2', targetSdkVersion:29, ]
dependencies = [ appCompact : 'androidx.appcompat:appcompat:1.0.2' ]
}
根目录的build.gradle中,顶部加入
apply from:"config.gradle"
使用的时候如下
compileSdkVersion rootProject.ext.android.compileSdkVersion
implementation rootProject.ext.dependencies.appCompact
注意,在implementation dependencies 时候是可以这样写的
implementation 'androidx.test.ext:junit:1.1.0','androidx.test.espresso:espresso-core:3.1.1'
但是你在config.gradle中千万不能也类似这样写
dependencies = [
appCompact : '\'androidx.appcompat:appcompat:1.0.2\',\'androidx.test.espresso:espresso-core:3.1.1\''
]
因为在build.gradle中你把所有依赖放到implementation后面,用逗号分隔,这个逗号和字符串的逗号不一样,你在config.gradle中那样写的其实相当于在build.gradle implementation dependencies 时这样写
implementation 'androidx.test.ext:junit:1.1.0,androidx.test.espresso:espresso-core:3.1.1'
那你可能会问,这样写不行的话,那我怎么在config.gradle中实现对所有模块需要的公共依赖库集中管理呢?可以按照下面这样写
ext {
....
dependencies = [ publicImplementation: [ 'androidx.test.ext:junit:1.1.0', 'androidx.test.espresso:espresso-core:3.1.1' ],
appCompact : 'androidx.appcompat:appcompat:1.0.2'
]
}
implementation rootProject.ext.dependencies.publicImplementation //每个模块都写上这句话就好了
这样就完了吗?还有我们自己写的的公共库也要集中管理,一般我们都会在模块的build.gradle中一个个这样写
implementation project(path: ':basic')
现在我们通过gradle来管理,如下
ext {
....
dependencies = [ other:[ ':basic', ]
]
}
rootProject.ext.dependencies.other.each{
implementation project(it)
}
3、组件在Application和Library之间随意切换
Library不能在Gradle文件中有applicationId
AndroidManifest.xml文件区分
在开发过程中,需要独立测试,避免不了经常在Application和Library之间随意切换。在模块,包括壳工程app模块运行时,Application类只能有一个。
首先我们在config.gradle中配置,为什么不在gradle.properties中配置,之前也说了
ext {
android = [
compileSdkVersion: 29,
buildToolsVersion: '29.0.2',
targetSdkVersion : 29,
isApplication:false,
]
....
}
然后在各个模块的build.gradle文件顶部加入以下判断
if(rootProject.ext.android.isApplication) {
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}
-
Library不能在Gradle文件中有applicationId
android { defaultConfig { if(rootProject.ext.android.isApplication){ applicationId "com.cov.moduletest" //作为依赖库,是不能有applicationId的 } .... }
-
在app模块的gradle中也需要有区分
dependencies { ..... if(!rootProject.ext.android.isApplication){ implementation project(path: ':customer') //只有当业务模块是依赖的时候去依赖 ,看业务需求 } }
-
AndroidManifest.xml文件区分
在各个模块的build.gradle中区分
sourceSets { main{ if(rootProject.ext.android.isApplication){ manifest.srcFile '/src/main/AndroidManifest.xml' }else{ manifest.srcFile "src/main/manifest/AndroidManifest.xml" } } }
-
Application配置
因为我们会在Application中做一些初始化操作,如果模块单独运行的话,那么这些操作需要放到模块的Application中,所以这里需要单独配置一下,新建module 文件夹,配置好下面文件时,新建自定义的Application类,然后在manifest文件夹下的清单文件内指定Application。这样作为依赖库运行时,module 文件夹下的文件不会进行编译。
main{
if(rootProject.ext.android.isApplication){
manifest.srcFile '/src/main/AndroidManifest.xml'
}else{
manifest.srcFile "src/main/manifest/AndroidManifest.xml"
java.srcDirs 'src/main/module','src/main/java'
}
}
以上是配置单独模块时,Application可以这样写,但这里还需要考虑Application的初始化问题,壳工程的Application初始化完成后需要分别初始化依赖组件的Application。可以这样写
basic 模块中定义
public interface IApp{
void init(Application app);
}
然后各个模块类似这样写
public AModuleApplication implements IApp{
public void init(Application app){ 初始化操作 }
}
在壳工程的Application里维护一个数组 {"com.cv.AModuleApplication.class","xx"}
但是这样不优雅,建议在basic中建个类专门维护
接下来,作为一个独立AP运行P时,只需要在壳工程Application的onCreate方法中对该数组的类全部进行反射构造,
调用init方法即可。
上面这样写确实可以,唯一不足的是需要维护一个包含各个模块作为Library时需要初始化的类,**有没有更好的方法呢?**答案肯定是有的,使用注解,对每个模块中,需要在Application初始化调用的类,即上述数组中维护的类,加上注解,编译期收集起来,Application的onCreate方法调用,没理解的同学可以看下面的组件化跳转分析,道理类似。
4、在Java代码中判断是否独立运行还是集成运行
在运行时,每个模块都会生成一个对应的BuildConfig类,存放包路径可能不同,那我们怎么做呢?
在basic模块的build.gradle中加入以下代码
buildTypes {
release {
buildConfigField 'boolean', 'isApplication', rootProject.ext.android.isApplication.toString()
}
debug {
buildConfigField 'boolean', 'isApplication', rootProject.ext.android.isApplication.toString()
}
}
为什么要在basic模块下加入呢?就是因为BuildConfig每个模块都会有,总不能在所有模块都加入这句话吧。在basic模块加入后,其它模块依赖这个模块,然后通过在basic模块中定义的BaseActivity中,添加获取该值的方法即可,其他模块继承BaseActivity,就可以拿到父类方法进行判断了,这只是一种,具体要看业务进行分析。
三、组件化跳转分析
1、自定义组件化跳转模块
按照上述配置后,接下啦第一步就需要解决组件化通信问题,其中第一类问题就是跳转相关。因为业务组件之间不能耦合,所以我们只能通过自定义一个新的 router 模块,各个业务组件内通过继承该依赖,然后实现跳转。
我们只需要在router模块中定义一个ARouter容器类,然后各个模块进行注册Activity,就可以使用了,代码如下
public class ARouter {
private static ARouter aRouter = new ARouter();
private HashMap<String, Class<? extends Activity>> map = new HashMap<>();
private Context mContext;
private ARouter(){
}
public static ARouter getInstance(){
return aRouter;
}
public void init(Context context){
this.mContext = context;
}
/**
* 将类对象添加到容器中
* @param key
* @param clazz
*/
public void registerActivity(String key,Class<?extends Activity> clazz){
if(key != null && clazz != null && !map.containsKey(key)){
map.put(key,clazz);
}
}
public void navigation(String key){
navigation(key,null);
}
public void navigation(String key, Bundle bundle){
if(mContext == null){
return;
}
Class<?extends Activity > clazz = map.get(key);
if(clazz != null){
Intent intent = new Intent(mContext,clazz);
if(bundle != null){
intent.putExtras(bundle);
}
mContext.startActivity(intent);
}
}
}
通过ARouter.getInstance().navigation("key") 就能跳转了,但是前提是需要调用registerActivity将每个Activity和对应路径注册进来,那不可能在每个Activity中都调用该方法将类对象加到ARouter路由表吧?我们可能会想到在BasicActivity里面加一个抽象方法,将所有类对象返回,然后你拿到后调用registerActivity方法注册,但是这个前提是 需要你继承BasicActivity的类已经创建了,已经实例化了,所以这不可能在没启动Activity时进行注册。那怎么样才能在Activity没启动时,将所有类对象添加到ARouter容器内呢?有什么方法可以在Application创建时候可以收集到所有未启动的Activity呢?
可能大家还会想到,在每一个模块里面新建一个ActivityUtils类,然后定义一个方法,里面调用ARouter.registerActivity ,注册该模块所有需要注册的类,然后在Application类里触发该方法。模块少还好说,可以一个个手动敲,模块一多,每个模块都得写,维护太麻烦了,可不可以自动生成这样的方法,自动找到需要注册的类,收集起来呢?
这就需要使用APT技术来实现了,通过对需要跳转的Activity进行注解,然后在编译时生成类文件及类方法,该类方法内利用Map收集对应的注解了的类,在Application创建时,执行这些类文件相关方法,收集到ARouter容器内。
2、组件化跳转实现方案升级
不了解如何操作APT的同学可以参考
要实现上述说的方案,需要了解一下APT(Annotation Processing Tool)技术,即注解处理器,它是Javac的一个工具,主要用来在编译时扫描和处理注解。
-
创建注解,对需要注册的Activity类用注解标记 (annotation模块)
@Target 声明注解的作用域
@Retention 生命注解的生命周期
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface ActivityPath { String value(); }
-
创建注解处理器 并生成类 (annotation_compiler模块)
@AutoService(Processor.class) 虚拟机在编译的时候,会通过这个判断AnnotationCompiler是注解处理器, 是固定的写法,加个注解即可,通过auto-service中的@AutoService可以自动生成AutoService注解处理器,用来注册用来生成 META-INF/services/javax.annotation.processing.Processor 文件
@SupportedSourceVersion(SourceVersion.RELEASE_7) 指定JDK编译版本
@SupportedAnnotationTypes({Constant.ACTIVITY_PATH}) 指定注解,这里填写ActivityPath的类的全限定名称 包名.ActivityPath
Filer 对象,用来生成Java文件的工具
Element 官方解释 表示程序元素,如程序包,类或方法,TypeElement表示一个类或接口程序元素,VariableElement表示一个字段、枚举常量或构造函数参数、局部变量,TypeParameterElement表示通用类、接口、方法、或构造函数元素的正式类型参数,这里简单举个例子
package com.example //PackageElement public class A{ //TypeElement private int a;//VariableElement private A mA;//VariableElement public A(){} // ExecuteableElement public void setA(int a){ // ExecuteableElement 参数a是VariableElement } }
还需要注意一点,为了在编译时不出现GBK编码错误等问题,需要在gradle中添加
tasks.withType(JavaCompile) { options.encoding = 'UTF-8' }
接下来就开始真正实现了,现在annotation_compile的依赖中添加
implementation'com.google.auto.service:auto-service:1.0-rc4' annotationProcessor'com.google.auto.service:auto-service:1.0-rc4' implementation 'com.squareup:javapoet:1.11.1'
然后实现注解处理器类
@AutoService(Processor.class) @SupportedAnnotationTypes({Constant.ACTIVITY_PATH}) // 注解处理器接收的参数 @SupportedOptions(Constant.MODULE_NAME) public class AnnotationCompiler extends AbstractProcessor { //生成java文件的工具 private Filer filer; private String moudleName; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); filer = processingEnv.getFiler(); moudleName = processingEnv.getOptions().get(Constant.MODULE_NAME); } /** * 得到最新的Java版本 * * @return */ @Override public SourceVersion getSupportedSourceVersion() { return processingEnv.getSourceVersion(); } /** * 找注解 生成类 * * @param set * @param roundEnvironment * @return */ @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { if (moudleName == null) { return false; } //得到模块中标记了ActivityPath的注解 Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ActivityPath.class); //存放 路径 类文件名称 Map<String, String> map = new HashMap<>(); //TypeElement 类节点 for (Element element : elements) { TypeElement typeElement = (TypeElement) element; ActivityPath activityPath = typeElement.getAnnotation(ActivityPath.class); String key = activityPath.value(); String activityName = typeElement.getQualifiedName().toString();//得到此类型元素的完全限定名称 map.put(key, activityName + ".class"); } //生成文件 if (map.size() > 0) { createClassFile(map); } return false; } private void createClassFile(Map<String, String> map) { //1.创建方法 MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("registerActivity") .addModifiers(Modifier.PUBLIC) .returns(void.class); Iterator<String> iterator = map.keySet().iterator(); while (iterator.hasNext()) { String key = iterator.next(); String className = map.get(key); //2.添加方法体 methodBuilder.addStatement(Constant.AROUTER_NAME + ".getInstance().registerActivity(\"" + key + "\"," + className + ")"); } //3.生成方法 MethodSpec methodSpec = methodBuilder.build(); //4.获取接口类 ClassName iRouter = ClassName.get(Constant.PACKAGE_NAME, Constant.IROUTER); //5.创建工具类 TypeSpec typeSpec = TypeSpec.classBuilder(Constant.CLASS_NAME + "?" + moudleName) .addModifiers(Modifier.PUBLIC) .addSuperinterface(iRouter) //父类 .addMethod(methodSpec) //添加方法 .build(); //6.指定目录构建 JavaFile javaFile = JavaFile.builder(Constant.PACKAGE_NAME, typeSpec).build(); //7.写道文件 try { javaFile.writeTo(filer); } catch (IOException e) { } } }
生成的文件效果如下
public class RouterGroup$$moduletest implements IRouter { public void registerActivity() { com.cv.router.ARouter.getInstance().registerActivity("/main/login",com.cv.moduletest.LoginActivity.class); } }
-
在ARouter中实现init方法,触发类文件的方法
public void init(Context context){ this.mContext = context; //1.得到生成的RouterGroup?.. 相关文件 找到这些类 try { List<String> clazzes = getClassName(); if(clazzes.size() > 0){ for(String className:clazzes){ Class<?> activityClazz = Class.forName(className); if(IRouter.class.isAssignableFrom(activityClazz)){ //2.是否是IRouter 子类 IRouter router = (IRouter) activityClazz.newInstance(); router.registerActivity(); } } } } catch (Exception e) { e.printStackTrace(); } } private List<String> getClassName() throws IOException { List<String> clazzList = new ArrayList<>(); //加载apk存储路径给DexFile DexFile df = new DexFile(mContext.getPackageCodePath()); Enumeration<String> enumeration = df.entries(); while (enumeration.hasMoreElements()){ String className = enumeration.nextElement(); if(className.contains(Constant.CLASS_NAME)){ clazzList.add(className); } } return clazzList; }
到此就实现了自动化收集类信息。
四、ARouter总结
当你了解了上述方法时,你再去看ARouter的源码,会轻松点,跳转实现原理,都差不多。当然ARouter也支持拦截等功能,想要查看ARouter源码,可以自行在掘金上搜索。这里给出以前看ARouter时做的笔记,只针对客户端使用ARouter时的时序图和文字描述,可能总结写得不全不好,不喜勿喷
-
首先在ARouter.getInstance().init()中会调用_ARouter的init()方法,然后回调用after方法,after方法是通过byName形式获取的拦截器Service。
-
这里主要是init()方法,里面会构建一个Handler,主要用来启动应用组件跳转和在debug模式下显示提示,然后还有一个线程池,主要是用于执行后续拦截器拦截逻辑,然后这个init中,最重要的应该就是LogisticsCenter.init()方法,在这里面,他会获取arouter.router包名下的所有类文件名,然后加载到Set集合中,然后遍历这些class,Root相关的类反射调用loadInto方法加载到groupIndex集合中,Interceptors相关的类加载到interceptorsIndex中,Providers相关的类加载都providersIndex中。这些类文件都是arouter-compile根据注解生成的,文件名规则是ARouter Root ? 模块名或者是ARouter ?Provider 模块名,或者是ARouter ? Group ? group名,例如Root相关类的loadInto方法就是把group值和group 相关类匹配放在groupIndex中,然后在需要使用时再去加group相关类的信息。
-
我们使用ARouter.getInstance().build().navigation获取Fragment或者跳转时,它先是_ARouter的build方法, 这个方法里,他会bayType形式调用PathReplaceService,对build()方法传入的路径path做修改,然后如果使用RouterPath注解时没有指定group,会获取path中第一个/后面的字符串作为group并返回一个Poscard,内部有一个bundle用于接收传入的参数,然后调用自身的navigation方法,最后还是回调到了 _ARouter的navigation()方法,这个方法内会按需获取加载指定path对应的类信息,首先是从groupIndex里面需要group组名对应的类信息,然后通过反射调用loadInto方法,将该组名下的所有路径对应关系保存到routes Map中,然后去完善传入的Path对应的RouteMeta信息,最后根据元信息的类型,构建对应的信息,并指定provider和fragment的开启绿色通道。然后接下来,就是如果没有开启绿色通道,将利用CountDownlaunch和线程池将所有拦截器按需进行处理,然后通行后,会根据元信息类型,构造相应参数,启动Activity或者反射构建Fragment返回。
@Path(path)
-
对应于
ARouter$$Root$$模块名.class
,内部生成loadTo
方法,主要用来将 路径group 和对应类的映射关系存入MAP
中,在ARouter
初始化时,会将这些信息存到静态的group Map
结构里。拦截器interceptors
和providers
类似,providers
的话,map
存储关系是接口名和RouteMeta
的映射关系,RouteMeta
中会包含实现类,path
等信息。 -
ARouter$$Group$$组名.class
,内部也有loadTo
方法,主要是将路径group组下的所有路由路径和对应的RouteMeta
信息存入Map
中
navigation跳转或者得到实例过程中
-
会先去看跳转的路径是否是大于等于2级的,然后看
PathReplaceService
路径替换接口有没有实现类,需不需要替换。只有一个 -
然后会检查是否需要预处理,也就是跳转前的处理,例如是否是针对某个路径,自己处理跳转
-
然后就去
group map
里面去找,路径组名group
对应的类,然后调用该类的方法,将该组下的所有路径名和包含跳转目标Activity
或者IProvider
的一些实现类等的信息的RouteMeta
存到新的routes map
里,然后将之前的group map
下该组信息删除,节省内存。 -
如果是
Provider
的话,会将Provider
实现类的Class对象
和反射构造的实例,通过providers map
存起来,然后调用init
方法。 -
Fragment
和Provider
默认开启绿色通道,不会执行拦截器。 -
拦截器是在
ARouter
初始化时,会默认获取到系统设置的拦截器 -
然后在这个拦截器内,会通过线程池 和
CountDownLatch
方式将所有开发者自定义的拦截器,进行执行调用。控制是否放行
五、最长公共子字符串
这周被问到一个问题,android列表上显示的所有数据,如何找出最长公共子标签,我立马想到动态规划,但是总感觉会有更好的实现方式,毕竟LCS问题大多都是给定两个字符串,总不能每两个比较后 (O(n2)),再跟第三个、第四个比较,这样时间复杂度不是很好。最后回过头想想,其实思路应该就是这样的,通过系统API操作,也要这样比较。
/**
* str1的长度为M,str2的长度为N,生成大小为M*N的矩阵dp
* dp[i][j] 的含义是str1[0....i] 与 str2[0......j]的公共子序列的长度
* 如果dp[i][0] dp[0][i] 为1 后面就都为1
* @author xxx
*
*/
public class DemoFive {
//dp[i][j] 的含义是str1[0....i] 与 str2[0......j]的公共子序列的长度
public static String findLCS(String A,int n,String B,int m) {
char[] arrA = A.toCharArray();
char[] arrB = B.toCharArray();
// n * m 矩阵 A * B
int[][] dp = new int[n][m];
int length = 0;
int start = 0;
for(int i = 1;i<n;i++) {
for(int j = 1;j<m;j++) {
if(arrA[i]== arrB[j] ) {
dp[i][j] = dp[i-1][j-1]+1;
if(dp[i][j] > length) {
length = dp[i][j];
start = i - length+1 ; //注意这里 下标是从0开始的
}
}
}
}
String result = A.substring(start,start + length);
return result;
}
}
笔记八