组件化开发现在基本上属于基础操作了,大家一般都会使用 ARouter 、LiveDataBus 作为组件化通信的解决方案,那为什么会选择ARouter,ARouter又是怎么实现的呢?这篇文章主要就 搭建组件化开发的准备工作 、组件化跳转分析,如果理解了这篇文章,对于查看ARouter源码应该是会有很大的帮助的。至于ARouter等分析,网上有很多的讲解,这里就不分析ARouter源码了,文章末尾会给出ARouter源码时序图和总结,可忽略。
ps: 为什么写本文,因为笔者最近被问道,为什么要用ARouter,ARouter它到底是解决什么问题,你能就一个点来分析吗?被问到该问题了?笔者是从它的跳转回答的,毕竟跳转简单。刚好记录并回忆一下。
参考资料
Android APT 实践
谈谈APT和JavaPoet的一些使用技巧和要点
组件化的优势想必大家都知道,可以总结为四点
编译速度 我们可以按需求测试单一业务模块,极大的提升了我们的开发速度
解耦 极度的降低了模块之间的耦合,便于后期维护与更新,当产品提出一个新业务时,完全可以新建一个业务组件,集成和摒弃都很方便
功能重用 某一块的功能在另外的组件化项目中使用只需要单独依赖这一模块即可
团队开发效率 组件化架构是团队开发必然会选择的一种开发方式,它能有效的使团队更好的协作
组件化开发,一般可以分为三层,分别为 壳工程、业务组件、基础依赖库,业务组件间互不关联,并且业务组件需要可以单独运行测试,整体都是围绕解耦来开展的,下面开始进行组件化开发前所需要做的准备工作
需要制定规范,对包名和项目模块的划分规范化,不同模块内不能有相同名字的类文件,避免打包失败等冲突问题
在我们创建的模块中,有一些,例如 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) }复制代码
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' } }复制代码
在运行时,每个模块都会生成一个对应的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,就可以拿到父类方法进行判断了,这只是一种,具体要看业务进行分析。
按照上述配置后,接下啦第一步就需要解决组件化通信问题,其中第一类问题就是跳转相关。因为业务组件之间不能耦合,所以我们只能通过自定义一个新的 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容器内。
不了解如何操作APT的同学可以参考
Android APT 实践
谈谈APT和JavaPoet的一些使用技巧和要点
要实现上述说的方案,需要了解一下APT(Annotation Processing Tool)技术,即注解处理器,它是Javac的一个工具,主要用来在编译时扫描和处理注解。
@Target 声明注解的作用域
@Retention 生命注解的生命周期
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface ActivityPath { String value(); }复制代码
@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); } }复制代码
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.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返回。
这周被问到一个问题,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; } }复制代码
笔记八