路由组件核心原理探究 - 手写一个ARouter
本篇博文基于自己实现的路由组件,主要功能包括Activity路由跳转,支持自定义服务。代码实现比较简单,重在探讨路由组件的核心原理,如需品尝功能更全、代码更屌的框架,可以直接前往ARouter,下载源码,即可。
一、核心原理
还是那句话,没什么是一张图解决不了的问题,如果有,那就是两张。O(∩_∩)O
路由组件的核心原理,如上图所示:
共分为两大部分:编译时 + 运行时。
a)编译时
1、路由信息收集
路由信息收集的前提是有数据源,而这个数据源就是我们自定义的编译时注解Route。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Route {
//配置路由,格式:/xxx/yyy,必须以/开头
String path();
}
当然,只是定义一个编译时注解是没用的,需要与之配合的编译时注解处理器来完成相应的处理工作。
使用:
@Route(path = "/main/mainactivity")
public class MainActivity extends AppCompatActivity {
......
}
我们这个所说的路由信息就是”/main/mainactivity“及相关信息,相关信息是指该路由对应的目标类,比如本例的MainActivity;还有目标类的类型,比如本例的是Activity,这样实现跳转时我们知道下一步的动作是startActivity了;携带的参数等等。
主要原理如下:
Java编译时注解框架会在编译时扫描所有的Java源文件,并将带有指定注解的所有元素Element作为参数,传递给我们的注解处理器的process方法进行相应的处理。
注解处理方法的第一步就是收集路由信息。路由这个词意味着分组,不然如何路由呢?因此,在收集路由信息的同时,我们按照一定的规则进行分组 - 比如/main/mainactivity,则属于main分组,同一分组下的所有路由信息就对应了一张路由表。也就是说有多少个分组,后面我们就会生成多少个路由表文件。示例:
/**
* AUTO GENERATED, MODIFY IS USELESS @WRITE BY JSON */
public class app_Group_main implements IRouteGroup {
public void loadRouteInfo(Map<String, RouteMeta> routes) {
routes.put("/main/mainactivity",RouteMeta.build("/main/mainactivity","main",MainActivity.class,TypeEnum.ACTIVITY));
routes.put("/main/shopactivity",RouteMeta.build("/main/shopactivity","main",ShopActivity.class,TypeEnum.ACTIVITY));
}
}
2、路由信息分组、生成路由表文件、路由入口文件
路由信息分组在第一部分已经说了,这里不再说了。
路由信息完成分组之后,下一步的动作就是生成路由表文件,路由表文件包含了同一分组下的所有路由信息,这是一个java文件,文件名可以按照一定的规则来进行命名,比如
app_Group_main,即 module名 + ”_“ + ”Group “+ ”_“ + 分组名。
一个大型的App,路由表文件的数量是非常多的。如果一开始就把所有的路由信息都加载到内存中,这对于本就内存不宽裕的手机设备来说并不是好的做法。比较优秀的做法是只加载用到的路由信息对应的路由表。这就需要借助路由入口文件实现懒加载了。路由入口文件示例如下:
/**
* AUTO GENERATED, MODIFY IS USELESS @WRITE BY JSON */
public class JRouter_Entry_app implements IEntry {
public void loadRouteEntry(Map<String, Class<? extends IRouteGroup>> entries) {
entries.put("test",app_Group_test.class);
entries.put("main",app_Group_main.class);
}
}
我们会在生成路由表文件的同时,生成路由入口文件,如上述代码所示。原因已经说过了,不再详述了。
b)运行时
3、运行时扫描dex文件,加载路由入口信息至内存
在初始化路由组件的时候,我们只会把路由入口文件中的所有信息加载到内存中。
4、运行时,根据路由信息,加载路由表至内存中
在进行具体的路由跳转时,我们才会去加载相应的路由表。比如MainActivity,我们首先会查找路由入口信息,根据分组main找到了对应的路由表文件是app_Group_main.class,则利用反射将app_Group_main的所有路由信息加载到内存中。
具体涉及到运行时扫描dex文件,在后面的源码分析中会详述。
小结:
a)路由生成:收集路由信息 -> 对路由信息分组 -> 每个路由分组生成一个路由表文件 -> 生成路由入口文件;
b)路由跳转:
- [依据分组名]查找路由入口:在路由入口文件查找对应的路由表文件是哪个;
- 加载路由表文件;
- 根据路由path信息,在路由表文件中查找具体的路由信息;
- 拿到足够的信息,进行路由跳转。
二、手写一个ARouter,探究路由实现核心原理
本部分同样按照博客开始的图示,分四个部分实现。
2.1、路由信息收集 & 分组
a)编译时注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Route {
//配置路由,格式:/xxx/yyy,必须以/开头
String path();
}
注意:Retention必须是CLASS。Target是TYPE。
b)路由信息提取,分组
private Map<String, Set<RouteMeta>> group;//路由分组
我们定义了一个Map用于保存路由分组信息。每一个RouteMeta就是一条路由信息。同一分组的所有路由信息会保存在同一个Set<RouteMeta>中。在生成路由表文件的时候,一个Set<RouteMeta>就是一个路由表文件。
提取 & 分组:
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
.....
Set<? extends Element> routeElements = roundEnvironment.getElementsAnnotatedWith(Route.class);
if (routeElements == null || routeElements.isEmpty()) {
log.printMessage(Diagnostic.Kind.NOTE, "process roundEnvironment is empty");
return false;
}
extractAndGroupRouteInfo(routeElements); //提取&分组路由信息
.....
}
//提取&分组路由信息
private void extractAndGroupRouteInfo(Set<? extends Element> routeElements) {
log.printMessage(Diagnostic.Kind.NOTE, "start to classify route info");
for (Element ele : routeElements) { //遍历&路由分组
TypeMirror annoEle = ele.asType();
if (ele.getKind() == ElementKind.CLASS) {// Route注解只能使用在Class类上,如interface等类型不可使用
Route route = ele.getAnnotation(Route.class);
String path = route.path();//获取路由
String groupName = extractGroupFromPath(path);//提取分组名
RouteMeta routeMeta = new RouteMeta(path, groupName, ele, getElementKind(annoEle));//封装路由信息
Set<RouteMeta> groupRoutes = group.get(groupName);
if (groupRoutes == null) {
groupRoutes = new HashSet<>();
groupRoutes.add(routeMeta);
group.put(groupName, groupRoutes); // 路由分组
} else {
groupRoutes.add(routeMeta);
}
}
}
log.printMessage(Diagnostic.Kind.NOTE, "classify route info -- finish -- group size is " + group.size());
log.printMessage(Diagnostic.Kind.NOTE, "start to generate route group file");
}
前面我们说过:Java编译时注解框架会在编译时扫描所有的Java源文件,并将带有指定注解的所有元素Element作为参数,传递给我们的注解处理器的process方法进行相应的处理。
因此,RoundEnvironment 包含了所有带有Route注解的所有元素。所以,通过遍历roundEnvironment.getElementsAnnotatedWith(Route.class)所返回的集合,我们就可以轻松的完成路由表信息的提取工作。提取的信息包括:路由path,分组groupName,目标类信息,目标类的类型。
- 路由path:通过Element的getAnnotation(Route.class);我们就可以拿到注解信息,从而获取用户填写的路由信息path - route.path();//获取路由
- 分组groupName:拿到path之后,我们就可以根据一定的规则提取分组名。
// 从路由中提取分组名
private String extractGroupFromPath(String path) {
if (TextUtil.isEmpty(path) || !path.startsWith("/")) {
return null;
}
// 截取group
String groupName = path.substring(1, path.indexOf("/", 1));
return TextUtil.isEmpty(groupName) ? "" : groupName;
}
- 目标类信息:由于Element对应的就是Route注解的元素,因此包含了目标类的信息。
- 目标类的类型:
// 获取被注解的元素的类型
private TypeEnum getElementKind(TypeMirror typeMirror) {
if (types.isSubtype(typeMirror, typeMirror_Activity)) {
return TypeEnum.ACTIVITY;
} else if (types.isSubtype(typeMirror, typeMirror_Fragment)) {
return TypeEnum.FRAGMENT;
} else if (types.isSubtype(typeMirror, typeMirror_Fragment_V4)) {
return TypeEnum.FRAGMENTV4;
}
if (types.isSubtype(typeMirror, typeMirror_Service)) {
return TypeEnum.ISERVICE;
}
return TypeEnum.DEFAULT;
}
保存目标类的类型的原因是可以指导我们后续的动作,比如如果是一个Activity类型,那么对应的动作就是startActivity了。
2.2、生成路由表文件
//生成路由表文件
private void generateRouteGroupFile() {
//1、生成参数类型:Map<String, RouteMeta>
ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(
ClassName.get(Map.class),
ClassName.get(String.class),
ClassName.get(RouteMeta.class)
);
//2、生成参数:routes
ParameterSpec parameterSpec = ParameterSpec.builder(parameterizedTypeName,
"routes")
.build();
//3、遍历路由分组(Map<String, Set<RouteMeta>> group),生成路由表文件
// String是分组名,每一个Set<RouteMeta>都是一个路由表文件
//------------------------------------------------------------
//public class app_Group_main implements IRouteGroup {
// public void loadRouteInfo(Map<String, RouteMeta> routes) {
// routes.put("/main/mainactivity",RouteMeta.build("/main/mainactivity","main",MainActivity.class,TypeEnum.ACTIVITY));
// }
//}
for (Map.Entry<String, Set<RouteMeta>> routeEntry : group.entrySet()) {
String groupName = routeEntry.getKey();
MethodSpec.Builder methodSpec = MethodSpec.methodBuilder(Const.LOAD_ROUTE_INFO)
.addModifiers(Modifier.PUBLIC)
.addParameter(parameterSpec);
for (RouteMeta routeMeta : routeEntry.getValue()) {//遍历&添加统一分组下的路由信息
// routes.put("/main/mainactivity", RouteMeta.build(path, group, destination, rawType, type))
methodSpec.addStatement("routes.put($S,$T.build($S,$S,$T.class,$T." + routeMeta.getType() + "))",
routeMeta.getPath(),
ClassName.get(RouteMeta.class),
routeMeta.getPath(),
routeMeta.getGroup(),
ClassName.get(routeMeta.getRawType().asType()),
ClassName.get(TypeEnum.class));
}
//路由文件名
String groupFileName = moduleName + "_Group_" + groupName;
TypeSpec typeSpec = TypeSpec.classBuilder(groupFileName)
.addSuperinterface(ClassName.get(Const.TEMPLATE, Const.IROUTEGROUP))
.addModifiers(Modifier.PUBLIC)
.addJavadoc(Const.WARN)
.addMethod(methodSpec.build())
.build();
JavaFile javaFile = JavaFile.builder(Const.ROUTE_FILE_DIRECTORY_NAME, typeSpec).build();
try {
javaFile.writeTo(filer);
} catch (Exception e) {
log.printMessage(Diagnostic.Kind.ERROR, "generate route group file exception");
}
if (entries == null) {
entries = new HashMap<>();
}
entries.put(groupName, groupFileName);//保存生成的路由文件信息,作为之后的查找入口
}
}
上述代码是使用JavaPoet完成Java源文件的生成的。因此,不熟悉的需要先了解JavaPoet。上述代码的注释很清楚,不再详述了。
生成的路由表如下所示:
/**
* AUTO GENERATED, MODIFY IS USELESS @WRITE BY JSON */
public class app_Group_main implements IRouteGroup {
public void loadRouteInfo(Map<String, RouteMeta> routes) {
routes.put("/main/mainactivity",RouteMeta.build("/main/mainactivity","main",MainActivity.class,TypeEnum.ACTIVITY));
}
}
2.3、生成入口文件
在生成路由表文件的同时,我们会保存对应的分组与路由表文件的关系至Map中。
entries.put(groupName, groupFileName);//保存生成的路由文件信息,作为之后的查找入口
依据这个Map就可以生成路由的入口文件了。
//生成路由入口文件
private void generateRouteEntryFile() {
//4、生成路由入口文件:void loadRouteEntry(Map<String, Class<? extends IRouteGroup>> entries);
//4.1、生成参数类型
ParameterizedTypeName parameterizedTypeName1 = ParameterizedTypeName.get(
ClassName.get(Map.class),
ClassName.get(String.class),
ParameterizedTypeName.get(
ClassName.get(Class.class),
WildcardTypeName.subtypeOf(ClassName.get(Const.TEMPLATE, Const.IROUTEGROUP))
)
);
//4.2、生成参数
ParameterSpec parameterSpec1 = ParameterSpec.builder(parameterizedTypeName1, "entries").build();
//4.3、生成方法
MethodSpec.Builder methodSpec = MethodSpec.methodBuilder(Const.LOADROUTEENTRY)
.addModifiers(Modifier.PUBLIC)
.addParameter(parameterSpec1);
for (Map.Entry<String, String> entry : entries.entrySet()) {
//entries.put("main", JRouterTest_Group_main.class);
methodSpec.addStatement("entries.put($S,$T.class)",
entry.getKey(),
ClassName.get(Const.ROUTE_FILE_DIRECTORY_NAME, entry.getValue()));
}
// 4.5、生成入口文件
TypeSpec typeSpec = TypeSpec.classBuilder(Const.JROUTER + Const.SEPELATOR + Const.ENTRY + Const.SEPELATOR + moduleName)
.addMethod(methodSpec.build())
.addModifiers(Modifier.PUBLIC)
.addJavadoc(Const.WARN)
.addSuperinterface(ClassName.get(Const.TEMPLATE, Const.IENTRY))
.build();
JavaFile javaFile = JavaFile.builder(Const.ROUTE_FILE_DIRECTORY_NAME, typeSpec)
.build();
try {
javaFile.writeTo(filer);
} catch (Exception e) {
log.printMessage(Diagnostic.Kind.ERROR, "generate route entry file exception");
e.printStackTrace();
}
}
入口文件如下所示:
/**
* AUTO GENERATED, MODIFY IS USELESS @WRITE BY JSON */
public class JRouter_Entry_app implements IEntry {
public void loadRouteEntry(Map<String, Class<? extends IRouteGroup>> entries) {
entries.put("test",app_Group_test.class);
entries.put("main",app_Group_main.class);
}
}
2.4、运行时扫描dex,加载路由入口信息至内存中
//加载路由表入口文件至内存
private static void loadIntoEntries(Application context) {
try {
//获取所有dex中指定包名下的所有路由文件信息
Set<String> routeFiles = ClassUtils.getFileNameByPackageName(context, Const.ROUTE_FILE_DIRECTORY_NAME);
if (routeFiles != null && routeFiles.size() > 0) {
for (String fileName : routeFiles) {
if (fileName.startsWith(Const.ROUTE_FILE_DIRECTORY_NAME + "." + Const.JROUTER + Const.SEPELATOR + Const.ENTRY + Const.SEPELATOR)) {
//只加载路由入口文件至内存中
((IEntry) Class.forName(fileName).newInstance()).loadRouteEntry(wareHouse.entries);
}
}
}
} catch (Exception e) {
Log.e("TAG", "loadIntoEntries fail");
}
}
核心代码:运行时扫描dex文件
ClassUtils.getFileNameByPackageName(context, Const.ROUTE_FILE_DIRECTORY_NAME);
可以拿到指定包名下的所有文件的全路径className。比如我们生成的路由表及路由入口文件都在com.jrouter.routes下,因此可以在运行时扫描dex文件时,得到所有的路由入口文件的全路径,然后利用反射将路由入口的信息全部加载到内存中。
详细实现如下:
/**
* 通过指定包名,扫描包下面包含的所有的ClassName
*
* @param context U know
* @param packageName 包名
* @return 所有class的集合
*/
public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException {
final Set<String> classNames = new HashSet<>();
List<String> paths = getSourcePaths(context);//所有的dex文件的路径
final CountDownLatch parserCtl = new CountDownLatch(paths.size());
for (final String path : paths) {
DefaultPoolExecutor.getInstance().execute(new Runnable() {
@Override
public void run() {
DexFile dexfile = null;
try {//加载所有的dex文件
if (path.endsWith(EXTRACTED_SUFFIX)) {
//NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
dexfile = DexFile.loadDex(path, path + ".tmp", 0);
} else {
dexfile = new DexFile(path);
}
Enumeration<String> dexEntries = dexfile.entries();
while (dexEntries.hasMoreElements()) {
String className = dexEntries.nextElement();
if (className.startsWith(packageName)) {
//获取指定包名下的所有文件的className
classNames.add(className);
}
}
} catch (Throwable ignore) {
Log.e("ARouter", "Scan map file in dex files made error.", ignore);
} finally {
if (null != dexfile) {
try {
dexfile.close();
} catch (Throwable ignore) {
}
}
parserCtl.countDown();
}
}
});
}
parserCtl.await();
return classNames;
}
CountDownLatch + DefaultPoolExecutor开启多线程,扫描所有dex指定包名下的所有文件,拿到路径信息。
2.5、加载路由表信息 & 路由跳转
a)加载路由表信息至内存
private PostCard.Builder preparePostCard() {
//wareHouse.entries - 保存入口信息
//wareHouse.routes - 保存路由表信息
Class<? extends IRouteGroup> entryClass = wareHouse.entries.get(routeParamBuilder.getGroup());
if (entryClass != null) {
//加载特定路由的路由表信息至内存中
try {
((IRouteGroup) Class.forName(entryClass.getName()).newInstance()).loadRouteInfo(wareHouse.routes);
wareHouse.entries.remove(routeParamBuilder.getGroup());
} catch (Exception e) {
e.printStackTrace();
return routeParamBuilder;
}
preparePostCard();
} else {
// 构建具体的路由跳转信息
RouteMeta routeMeta = wareHouse.routes.get(routeParamBuilder.getPath());
if (routeMeta != null) {
routeParamBuilder.setDestination(routeMeta.getDestination());
routeParamBuilder.setType(routeMeta.getType());
switch (routeMeta.getType()) {
// 如果是Service,则只需返回Service类即可。只有Activity后面需要跳转。
case ISERVICE:
Class<?> destination = routeMeta.getDestination();
IService service = wareHouse.services.get(destination);
if (null == service) {
try {
service = (IService) destination.getConstructor().newInstance();
wareHouse.services.put(destination, service);
} catch (Exception e) {
e.printStackTrace();
}
}
routeParamBuilder.setService(service);
break;
}
}
return routeParamBuilder;
}
return routeParamBuilder;
}
b)路由跳转
//Service,则只需返回Service类即可。只有Activity后面需要跳转。
public Object navigation(Activity activity) {
if (activity != null) {
switch (type) {
case ACTIVITY:
Intent intent = new Intent(activity, destination);
activity.startActivity(intent);
break;
case ISERVICE:
return service;
}
}
return null;
}