前言
这一篇文章主要讲解路由知识,上一篇文章主要讲解组件化知识。
我打算开车了... 路由实现确实还是有些难度的,尽量讲仔细点。
实现效果:
这样就可以非常简单的实现组件之间的活动跳转了。
解释下实现的效果:
- 图一是使用自定义的注解来注释我们的某一个活动,注解中的三个属性是为了给这个活动生成对应的URL和路由表。
- 图二则是实现跳转一个活动,最关键的是
Go.readyToJump(this).setUrl("url").launch()
这句代码,就是我们的Go类通过url来找到对应的Activity,并实现跳转。- 图三这个东西可有可无,就是一个txt文件,专门给协同开发的成员查看的,成员可以从中找到他们想要跳转的活动的URL。
实现思路
这个思路图可能并不够清晰,看来下面的代码讲解再回来看可能要好些。
代码讲解
组件化就不讲了,先看下项目目录吧,新增加了三个组件。
- router_annotation:JavaLibrary,这个组件中只有一个注解,其它什么逻辑都没有,也就是上面提及的Route注解。
- router_complier:JavaLibrary,这个组件负责的就是查找被注解了的类、生成对应的代码类。
- router_go:AndroidLibrary,这个组件是暴露给其它开发者使用的组件,就是上面实现跳转Activity的地方,它的作用就是实现“add()”方法,将Activity.class投入Map中去,还有就是通过URL查找到对应的Activity.class,以实现跳转。
router_annotation
这个注解就是有三个变量属性,分别是name->活动别名、module->属于哪一个模块、desc->这个活动的描述。至于为什么注解要单独放在一个组件里面呢?原因我也忘记了...但是这样放置,也许是更加符合组件化吧(勉强...)。
router_go
BaseClassMap主要就两句代码:
public abstract class BaseClassMap {
protected volatile static HashMap mMap = new HashMap<>();
public abstract void add();
}
这个类是给生成类继承的,覆写add()方法,生成的add()就应该是向Map中投入URL和Class对象。类中Map就是放置url和Class的容器。
Go类不用多说,放出关键代码:
/**
* 注册组件 找到生成类,实例,调用add()方法
*/
public static void registerModule(String name) {
String path = "com.module_router." + name + "." + name + "$$Inject";
try {
Class cla = Class.forName(path);
BaseClassMap map = (BaseClassMap) cla.newInstance();
map.add();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
/**
* 通过url在Map中找到对应的Class对象
*/
private Class getTargetClass(String url) {
Class a ;
//判断是否是网址,是网址则去WebActivity
if (isNetworkUrl(url)) {
a = BaseClassMap.mMap.get(WEB_ACTIVITY_URL);
//MatesConfig.TO_WEB_KEY
mAttr.setExtra(WEB_KEY, url);
} else {
a = BaseClassMap.mMap.get(url);
}
//没能找到
if (a == null) {
mAttr.getCallBack().error(parseUrl(url));
return null;
}
//不是Activity对象
if (!Activity.class.isAssignableFrom(a) ) {
mAttr.getCallBack().error(CallBack.CLASS_ERROR);
return null;
}
//频繁操作
long nowTime = System.currentTimeMillis();
mRecordTime = nowTime;
if (mRecordCode == a.hashCode() && nowTime - mRecordTime <= MIN_TIME) {
mAttr.getCallBack().error(CallBack.FREQUENT_OPERATION);
mRecordCode = a.hashCode();
return null;
}
return a;
}
/**
* 通过找到的Class,实现Intent跳转
*/
private void realLaunch(Class a) {
Intent intent = new Intent(mAttr.getContext(), a);
intent.putExtra(mAttr.getKey(), mAttr.getText());
if (mAttr.getBundle() != null) {
mAttr.getContext().startActivity(intent, mAttr.getBundle());
} else {
mAttr.getContext().startActivity(intent);
}
if (mAttr.getEnterAnim() != -1 && mAttr.getExitAnim() != -1) {
AppCompatActivity activity = (AppCompatActivity) mAttr.getContext();
//设置动画
activity.overridePendingTransition(mAttr.getEnterAnim(), mAttr.getExitAnim());
}
}
其实逻辑不难,一句话:使用add()方法将URL和Activity.classy投入Map,通过URL去Map中找Class,找到的Activity.Class用来实现Intent跳转。
router_compiler
先分析AnnotationProcessor类:
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class AnnotationProcessor extends AbstractProcessor {
@Override
public Set getSupportedAnnotationTypes() {
//select type
Set types = new LinkedHashSet<>();
types.add(Route.class.getCanonicalName());
return types;
}
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
return true;
}
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
}
}
该类中不能含有中文字符,因为中文字符在编译的时候不能够被映射。
该类头部注解分别实现注册、确定编译Java版本的功能,而getSupportedAnnatationTypes()
方法则是确定我们要“寻找”的注解,init()
方法很简单,就是可以在里面执行一些初始化的东西。重点是process()
方法,就是在编译器收集注解的元素之后,会执行process()
方法。提供一句关键代码:for (Element e : roundEnvironment.getElementsAnnotatedWith(Route.class))
,这样就可以每一个每一个的拿到被注解了的元素。但是有一个“坑”!,就是在组件化中,编译器是按照组件为单位调用process()
方法,例如第一次调用process()方法,传递的是组件A中的被注解元素,而第二次调用该方法,传递的便是组件B中的被注解元素,要注意处理的逻辑关系。
上面讲解的类的作用就是主动拿到每个组件中被注解的元素,回顾一下,我们的Activity.class是被注解的元素,因此我们可以拿到跟这个Activity.class相关的信息。接下来我们便是要为Activity.class生成对应的Java文件,将我们的Activity.class投递到Map中去。
Java文件生成器ClassBuilder,关键代码:
public void generateJavaFile() {
if (mNodes.size() == 0) return;
ClassName superName = ClassName.get(mElements.getTypeElement(SUPER_CLASS_NAME));
String className = mNodes.get(0).getModule() + "$$Inject";
String packageName = mNodes.get(0).getModulePackageName();
MethodSpec.Builder injectMethod = MethodSpec.methodBuilder("add")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class);
for (Node n : mNodes) {
injectMethod.addCode(
"mMap.put($S, "+n.getPackageName() + "." + n.getClassName()+");\n",
n.getUrl());
}
MethodSpec methodSpec = injectMethod.build();
TypeSpec classSpec =TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC)
.superclass(superName)
.addMethod(methodSpec)
.build();
try {
//write java file
JavaFile.builder(packageName, classSpec).build().writeTo(mFiler);
} catch (IOException e) {
mMessager.printMessage(Diagnostic.Kind.ERROR,
"Router ---> create file error!");
}
}
这里解释下Node类,它就是一个节点类,包含着一个被注解元素的部分信息,例如它的name、module、desc、报名......
上面这段代码就是实现生成一个.java文件,说一下关键之处。这里的SUPER_CLASS_NAME
其实是BaseClassMap的完整包路径名,就是上面讲的那一个BaseClassMap类。在下面,生成类覆写了add()
方法,贴上Demo生成类:
这里的key是一个URL,仔细观察,它其实来源于@Route(name = "home_activity", module = "home", desc = "home组件的主活动")
这一句代码,也就是为当前的Activity.class生成了唯一对应的URL,并且add()
就是要实现将这个URL和Activity.clas投入Map中去,后面我们就根据URL在这个Map中寻找对应得Activity.class,并且用来Intent跳转。
最后再解释RouterFileUtil类,它就是实现生成路由表(table.txt)的一个帮助类而已
接下来再贴出导库的步骤:
记住,路由具有特殊性,要求必须在每一个组件下都导入路由组件,也不要使用api导入。
然后,就可以rebuild一下项目,这样路由就会工作,为你生成对应的代码。
现在还不能直接使用跳转,还记得文章上面说的那个Go类吗?里面有一个registerModule()
,现在再好好解一下它。我们在生成了代码文件之后,代码是没有执行的,因此我们需要在程序运行的时候执行生成的代码,也就是我们所谓的“注册组件”。建议下面这段代码放在程序初始化工作中去:
private void registerModule() {
//app壳组件
Go.registerModule(AppConfig.APP_MODULE);
//home组件 AppConfig.HOME_MODULE = "home"
Go.registerModule(AppConfig.HOME_MODULE);
//聊天组件
Go.registerModule(AppConfig.CHAT_MODULE);
}
这一步就是将URL和Activity.class全部投入Map中去。
最后,我们就可以实现跳转了。
总结
说实话,我觉得自己讲路由确实讲得很差,我是想将主要的思想说出来。将就啦...
还有就是本文中的路由的功能是很少的,但是我觉得路由得主要思想都掌握了的话,其它的实现也不会是多难得事儿,例如实现拦截器、埋点等等,也并不是太难。所以,我还是写了一个Demo,很简单,欢迎clone Demo地址
笔者水平有限,有写得不好的地方,请大胆指出~
转载请注明出处~