介绍
在组件化开发的时候,组件之间是相互独立的没有依赖关系,我们不能在使用显示调用来跳转页面了,因为我们组件化的目的之一就是解决模块间的强依赖问题,假如现在要从A业务组件跳转到业务B组件,并且要携带参数跳转,这时候怎么办呢?
上学的时候在书上看到了一句很有意义的话:任何软件工程遇到的问题都可以通过增加一个中间层来解决!
我们从这句话出发去思考:组件之间是平行结构的,它们之间相互没有交集,要实现通信只有添加一个中间层将它们连接到一起,这就是“路由”的概念,由第一篇文章开始的组件化模型下的业务关系图可知路由就是起到一个转发的作用。路由就像一个桥梁一样让平行的河流(组件)之间可以通信和传递数据,这样看起来好像他们之间又是强耦合的关系,维护起来还是代价还有点大,那么还有没有好点的办法呢?那就是路由层使用接口和其他module之间建立弱耦合的关系。
在组件化开发中实现跨组件跳转的库比较有名气的是阿里的ARouter,ARouter的功能很丰富,这一节我们根据ARouter的源码理解自己实现一个简单版的ARouter,ARouter的功能太多了如过滤器,拦截器等,这里只是手写实现一部分核心功能-路由跳转。
准备
在编写之前需要储备的知识点和推荐的文章:
注解以及注解处理器的使用:https://blog.csdn.net/shusheng0007/article/details/90734159
JavaPoet一个生成java源文件的 Java API:https://github.com/square/javapoet
实现
- 定义注解
在router_annotation的module中定义注解Route.java
/**
* Target用于指定被此元注解标注的注解可以标出的程序元素
*
*/
@Target(ElementType.TYPE)
/**
* RetentionPolicy.SOURCE 源码阶段 注解信息只会保留在源码中,编译器在编译源码的时候会将其直接丢弃
* RetentionPolicy.CLASS 编译阶段 javapoet使用 注解信息保留在class文件中,VM不会持有其信息
* RetentionPolicy.RUNTIME 运行阶段,注解信息保留在class文件中,而且VM也会持有此注解信息, 反射获得注解信息
*/
@Retention(RetentionPolicy.CLASS)
public @interface Route {
/**
* 路由的路径,标识一个路由节点
*/
String path();
/**
* 将路由节点进行分组,可以实现按组动态加载
*/
String group() default "";
}
Route注解里有path和group,这是仿照ARouter对路由进行分组。因为当项目变得越来越庞大的时候,为了便于管理和减小首次加载路由表过于耗时的问题,我们对所有的路由进行分组。
- 定义注解处理器生成路由映射文件
定义RouteProcessor实现AbstractProcessor类, 这里使用google的 AutoService注册处理器,使用JavaPoet实现源文件的编写。在router_compiler的build.gradle引入这两个库
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
implementation 'com.squareup:javapoet:1.11.1'
implementation project(':router_annotation')
}
// java控制台输出中文乱码
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
sourceCompatibility = "7"
targetCompatibility = "7"
Processor 一般会重写父类的4个方法:
init:初始化工作,我们可以得到一些有用的工具,例如 Filer,ElementUtils,TypeUtils.
process:最重要的方法,所有的注解处理都是在此完成
getSupportedAnnotationTypes:返回我们所要处理的注解的一个集合
getSupportedSourceVersion:要支持的java版本
@AutoService(Processor.class)
//处理器接受到的参数 代替 {@link AbstractProcessor#getSupportedOptions()} 函数
@SupportedOptions(Constants.ARGUMENTS_NAME)
/**
* 指定使用的Java版本 替代 {@link AbstractProcessor#getSupportedSourceVersion()} 函数
* 声明我们注解支持的JDK的版本
*/
@SupportedSourceVersion(SourceVersion.RELEASE_7)
/**
* 注册给哪些注解的 替代 {@link AbstractProcessor#getSupportedAnnotationTypes()} 函数
* 声明我们要处理哪一些注解 该方法返回字符串的集合表示该处理器用于处理哪些注解
*/
@SupportedAnnotationTypes(Constants.ANN_TYPE_ROUTE)
public class RouteProcessor extends AbstractProcessor {
....
/**
* key是组名,value是注解标记的element的元素数据集合
* 使用Route的注解按照组名分表存储
*/
private Map<String, List<RouteMeta>> groupMap = new HashMap<>();
/**
* key:组名 value:实现IRouteGroup接口的className
*/
private Map<String, String> rootMap = new TreeMap<>();
...
}
init方法中根据ProcessingEnvironment得到一些工具和参数
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
Messager ms = processingEnvironment.getMessager();
Log.init(ms);
elementUtils = processingEnvironment.getElementUtils();
filerUtils = processingEnvironment.getFiler();
typeUtils = processingEnvironment.getTypeUtils();
Map<String, String> options = processingEnvironment.getOptions();
if (!Utils.isEmpty(options)) {
moduleName = options.get(Constants.ARGUMENTS_NAME);
}
if (Utils.isBlank(moduleName)) {
throw new RuntimeException("Not set Processor Parmaters.");
}
}
扫描所有Route修饰的注解文件,根据组名进行分组
/**
* 处理注解
*
* @param set 使用了支持注解的节点集合
* @param roundEnvironment 上下文
* @return true 表示后续处理器不会再处理
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
if (!Utils.isEmpty(set)) {
Set<? extends Element> elementsAnnotateds = roundEnvironment.getElementsAnnotatedWith(Route.class);
if (!Utils.isEmpty(elementsAnnotateds)) {
try {
parseRoute(elementsAnnotateds);
} catch (Exception e) {
Log.i("创建实现类异常---》"+e);
}
}
}
return true;
}
private void parseRoute(Set<? extends Element> routeElements) throws Exception {
// 通过elementUtils获得节点
TypeElement activityTypeElement = elementUtils.getTypeElement(Constants.ACTIVITY);
//mirror 节点自描述
TypeMirror activityTypeMirror = activityTypeElement.asType();
TypeElement IServiceTypeElement = elementUtils.getTypeElement(Constants.ISERVICE);
TypeMirror IServiceTypeMirror = IServiceTypeElement.asType();
Log.i("activityTypeMirror=" + activityTypeMirror + ", IServiceTypeMirror=" + IServiceTypeMirror);
RouteMeta routeMeta;
for (Element routeElement : routeElements) {
Log.i("routeElement="+routeElement);
Route route = routeElement.getAnnotation(Route.class);
TypeMirror typeMirror = routeElement.asType();
if (typeUtils.isSubtype(typeMirror, activityTypeMirror)) {//activity节点
routeMeta = new RouteMeta(RouteMeta.Type.ACTIVITY, route, routeElement);
}/* else if (typeUtils.isSubtype(typeMirror, IServiceTypeMirror)) {//IService节点
routeMeta = new RouteMeta(RouteMeta.Type.ISERVICE, route, routeElement);
} **/else {
throw new RuntimeException("[Just Support Activity/IService Route] :" + routeElement);
}
//分组记录信息, groupMap<Group分组,RouteMeta路由信息>
categories(routeMeta);
}
//生成类 需要实现的接口
generatedGroup();
generatedRoot();
}
每次检索到一个注解元素routeElement都要去groupMap 分类。
private void categories(RouteMeta routeMeta) throws ClassNotFoundException {
if (routeVerify(routeMeta)) {//1
Log.i("group name =" + routeMeta.getGroup() + ", path=" + routeMeta.getPath());
List<RouteMeta> routeMetas = groupMap.get(routeMeta.getGroup());
if (Utils.isEmpty(routeMetas)) {
routeMetas = new ArrayList<>();
groupMap.put(routeMeta.getGroup(), routeMetas);
}
//2
/* Class<?> destination = routeMeta.getDestination();
if (destination == null) {
String className = routeMeta.getElement().asType().toString();
Log.i("name="+className);
destination = Class.forName(className);
routeMeta.setDestination(destination);
}*/
routeMetas.add(routeMeta);
} else Log.e("group info error:" + routeMeta.getPath());
}
routeMeta合法后开始根据groupName进行分组归类。
上边的注释2处,我的本意是通过反射给routeMeta.setDestination设置值,在后边生成文件的时候直接通过routeMeta.getsetDestination使用。但是这样做的时候会报错 ClassNotFoundException。一时半会也不知道原因,晚上在看书的时候突然想起来,这是在编译期完成的,反射是在运行期使用,所以会报ClassNotFoundException。
注释1处的是验证routeMeta是否包含group,包含group条件是,注解的时候声明group,或者path的路径必须是// 大于等于两个“/”
private boolean routeVerify(RouteMeta meta) {
String path = meta.getPath();
String group = meta.getGroup();
//路由地址必须是/ 开头
if (Utils.isBlank(path) || !path.startsWith("/")) {
return false;
}
String[] split = path.split("/");
if (split.length < 3) {
return false;
}
//如果没有分组就以第一个/后的节点为分组
if (Utils.isBlank(group)) {
String defaultGroup = split[1];
if (Utils.isBlank(defaultGroup)) {
return false;
}
meta.setGroup(defaultGroup);
}
return true;
}
对RouteMeta进行分组整理后,在parseRoute方法中根据组名生成IRouteGroup的实现类,实现类中接受map参数,将扫描的路由文件存储到map中。IRouteGroup接口的定义如下:
public interface IRouteGroup {
/**
* 扫描注解 搜集路由信息
* @param atlas key:路由路径 path,value:Route注解修饰的路由信息数据
*/
void loadInto(Map<String, RouteMeta> atlas);
}
该文件是在route_core的module中声明的。
下边是通过Apt实现该接口的方法。
/**
* 生成IRouteGroup的实现类
* @throws Exception
*/
private void generatedGroup() throws Exception {
//参数类型 Map<String.class, RouteMeta.class>
ParameterizedTypeName atlas = ParameterizedTypeName.get(
ClassName.get(Map.class),
ClassName.get(String.class),
ClassName.get(RouteMeta.class)
);
//创建参数 Map<String, RouteMeta> atlas
ParameterSpec groupParamSpec = ParameterSpec.builder(atlas, "atlas").build();
//遍历分组,每一个分组创建一个类
for (Map.Entry<String, List<RouteMeta>> entry : groupMap.entrySet()) {
//声明方法
// @Override
// public void loadTo(Map<String, RouteMeta> atlas)
MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(Constants.METHOD_LOAD_INTO)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(void.class)
.addParameter(groupParamSpec);
//方法体 实现接口的抽象方法,将分组后的路由信息插入atlas中
//key 是path,value---RouteMeta
String groupName = entry.getKey();
List<RouteMeta> routeMetas = entry.getValue();
for (RouteMeta routeMeta : routeMetas) {
//atlas.put(path, RouteMeta.build(class, path, group)
// RouteMeta build(Type type, Class<?> destination, String path, String group)
loadIntoMethodOfGroupBuilder.addStatement("atlas.put($S, $T.build($T.$L,$T.class, $S, $S))",
routeMeta.getPath(),
ClassName.get(RouteMeta.class),
ClassName.get(RouteMeta.Type.class),
routeMeta.getType(),
ClassName.get((TypeElement) routeMeta.getElement()),
routeMeta.getPath().toLowerCase(),
groupName.toLowerCase()
);
}
TypeElement iRouteGroupTypeElement = elementUtils.getTypeElement(Constants.ROUTE_GROUP);
//创建Java文件
String groupClassName = Constants.NAME_OF_GROUP + groupName;
Log.i("groupClassName="+groupClassName);
JavaFile.builder(Constants.PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(groupClassName)
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ClassName.get(iRouteGroupTypeElement))
.addMethod(loadIntoMethodOfGroupBuilder.build()).build()
).build().writeTo(filerUtils);
Log.i("Generated RouteGroup: " + Constants.PACKAGE_OF_GENERATE_FILE + "." +
groupClassName);
rootMap.put(groupName, groupClassName);
}
}
在编写的时候这里需要格外小心,出错了很难排查,因为这是编译期不能进行debug,所以只能借助Log在关键的地方加上日志。另外在编写的时候,可以自己先在module2中创建一个实现该接口的类Router$$Group$$test
,然后对比着进行编码,最后记得把Router$$Group$$test
注释掉,
public class Router$$Group$$test implements IRouteGroup {
private Map<String, List<RouteMeta>> groupMap;
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
for (String groupName : groupMap.keySet()) {
List<RouteMeta> routeMetas = groupMap.get(groupName);
for (RouteMeta routeMeta : routeMetas) {
//RouteMeta build(Type type, Class<?> destination, String path, String group)
Class<?> destination = routeMeta.getDestination();
if (destination == null) {
destination = ClassName.get((Type) routeMeta.getElement());
}//
atlas.put(routeMeta.getPath(),
RouteMeta.build(routeMeta.getType(),
routeMeta.getDestination(),
routeMeta.getPath(),
routeMeta.getGroup()));
}
}
}
}
回到parseRoute中下一步是generatedRoot()
private void generatedRoot() throws Exception {
TypeElement iRouteRootTypeElement = elementUtils.getTypeElement(Constants.ROUTE_ROOT);
TypeElement iRouteGroupTypeElement = elementUtils.getTypeElement(Constants.ROUTE_GROUP);
//Map<String, Class<? extends IRouteGroup>> routes
ParameterizedTypeName routes = ParameterizedTypeName.get(
ClassName.get(Map.class),
ClassName.get(String.class),
ParameterizedTypeName.get(
ClassName.get(Class.class),
WildcardTypeName.subtypeOf(ClassName.get(iRouteGroupTypeElement))
)
);
//参数 Map<String,Class<? extends IRouteGroup> routes
ParameterSpec rootParameterSpec = ParameterSpec.builder(routes, "routes").build();
//函数 public void loadInto(Map<String,Class<? extends IRouteGroup>> routes> routes)
MethodSpec.Builder loadIntoMethodBuilder = MethodSpec.methodBuilder(Constants.METHOD_LOAD_INTO)
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(rootParameterSpec);
//函数体
for (String key : rootMap.keySet()) {
loadIntoMethodBuilder.addStatement("routes.put($S, $T.class)",
key,
ClassName.get(Constants.PACKAGE_OF_GENERATE_FILE, rootMap.get(key)));
}
String rootClassName = Constants.NAME_OF_ROOT + moduleName;
JavaFile.builder(Constants.PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(rootClassName)
.addSuperinterface(ClassName.get(iRouteRootTypeElement))
.addModifiers(Modifier.PUBLIC)
.addMethod(loadIntoMethodBuilder.build()).build())
.build().writeTo(filerUtils);
Log.i("Generated RouteRoot: " + Constants.PACKAGE_OF_GENERATE_FILE + "." + rootClassName);
}
上边的moduleName是在init方法中初始化的
Map<String, String> options = processingEnvironment.getOptions();
if (!Utils.isEmpty(options)) {
//Constants.ARGUMENTS_NAME = "moduleName"
moduleName = options.get(Constants.ARGUMENTS_NAME);
}
processingEnvironment能够接收的参数需要在getSupportedOptions方法中定义,也可以用注解的方式定义,这里在定义RouteProcessor的时候使用@SupportedOptions(Constants.ARGUMENTS_NAME)
我们需要在依赖route_core库的 组件的build.gradle的添加这个参数:
javaCompileOptions{
annotationProcessorOptions {
arguments = [moduleName : project.getName()]
}
}
generatedRoot方法根据rootMap来创建一个IRouteRoot接口的实现类,这个接口主要是记录每个组名对应Group类。同样在编码前也是先写一个实现类Router$$Root$$impl
,然后对比着进行编码。
public class Router$$Root$$impl implements IRouteRoot {
private Map<String, Class<? extends IRouteGroup>> map;
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
for (String groupName : map.keySet()) {
routes.put(groupName, map.get(groupName));
}
}
}
撰写完成后记得把这个类注释掉。注解和注解处理器我们都写完了,我们写一个demo看看效果,这里举一个setting的demo,目录结构如下:
编译后生成的文件如下
public class Router$$Group$$business implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/business/news", RouteMeta.build(RouteMeta.Type.ACTIVITY,BusinessNewsActivity.class, "/business/news", "business"));
atlas.put("/business/other", RouteMeta.build(RouteMeta.Type.ACTIVITY,BusinessOtherActivity.class, "/business/other", "business"));
}
}
public class Router$$Group$$personal implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/personal/wallet", RouteMeta.build(RouteMeta.Type.ACTIVITY,MyWalletActivity.class, "/personal/wallet", "personal"));
atlas.put("/personal/userInfo", RouteMeta.build(RouteMeta.Type.ACTIVITY,UserInfoActivity.class, "/personal/userinfo", "personal"));
}
}
public class Router$$Root$$setting implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("business", Router$$Group$$business.class);
routes.put("personal", Router$$Group$$personal.class);
}
}
这里看到生成的类分别实现了IRouteRoot和IRouteGroup接口,并且实现了loadInto()方法,而loadInto方法通过传入一个特定类型的map就能把分组信息放入map里,只要分组信息存入到特定的map里后,我们就可以随意的从map里取路由地址对应的Activity.class做跳转使用。
路由框架的初始化
我们要实现一个路由框架,就要考虑在合适的时机拿到这些映射文件中的信息,以供上层业务做跳转使用。那么在什么时机去拿到这些映射文件中的信息呢?首先我们需要在上层业务做路由跳转之前把这些路由映射关系拿到手,但我们不能事先预知上层业务会在什么时候做跳转,那么拿到这些路由关系最好的时机就是应用程序初始化的时候。另外如何去拿这些路由信息呢?在上面已经介绍过IRouteRoot接口的所有实现文件里保存着各个module的分组文件(分组文件就是实现了IRouteGroup接口的类文件),那么只要拿到所有实现IRouteGroup接口的类的集合,就可以根据path实现页面跳转了。
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Router.init(this);
}
}
route_core module中的Router.java
public static void init(Application application){
context = application;
try {
loadInfo();
} catch (Exception e) {
Log.e(TAG, "初始化失败!", e);
e.printStackTrace();
}
}
private static void loadInfo() throws PackageManager.NameNotFoundException, InterruptedException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//1.扫描apk中的所有dex文件找出使用注解生成的类
Set<String> routeMap = ClassUtils.getFileNameByPackageName(context, Router.ROUTE_ROOT_PACKAGE);
if (routeMap == null) {
return;
}
for (String className : routeMap) {
if (className.startsWith(ROUTE_ROOT_PACKAGE+"."+SDK_NAME+SEPARATOR+SUFFIX_ROOT)) {
// root中注册的分组信息,将分组信息加入仓库中
((IRouteRoot)(Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
}
}
}
我们首先通过ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE)得到apt生成的所有实现IRouteRoot接口的类文件集合,通过上面的讲解我们知道,拿到这些类文件便可以得到所有的路由地址和Activity映射关系。
路由跳转实现
经过前面的介绍,我们已经能够在app启动的时候获得所有的路由信息,接下来就可以实现跳转了。
在app module的Mainctiviy中如下使用:
public void personalInfoJump(View view) {
Router.getInstance().build("/personal/userInfo").navigation();
}
public void businessNewsJump(View view) {
Router.getInstance().build("/business/news").navigation();
}
build的时候根据path路径得到一个postCard对象,然后调用Postcard的navigation()方法完成跳转。Postcard的内容如下:
public class Postcard extends RouteMeta {
private Bundle extras;
private int flag = -1;
private Bundle optionCompat;
private int enterAnim, exitAnim;
...
public Object navigation() {
return navigation(null, null);
}
public Object navigation(Context context) {
return navigation(context, null);
}
public Object navigation(Context context, NavigationCallback callback) {
return Router.getInstance().navigation(context, this, -1, callback);
}
...
}
下面看一下Router 的navigation核心功能:
public Object navigation(Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback){
prepareCard(postcard);
if (callback != null) {
callback.onFound(postcard);
}
switch (postcard.getType()) {
case ACTIVITY:
final Context currentContext = context==null?Router.context:context;
final Intent intent = new Intent(currentContext, postcard.getDestination());
intent.putExtras(postcard.getExtras());
if (postcard.getFlag()!=-1) {
intent.setFlags(postcard.getFlag());
}else if (!(currentContext instanceof Activity)){
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
if (requestCode>0) {
ActivityCompat.startActivityForResult((Activity) currentContext, intent,
requestCode, postcard.getOptionCompat());
}else {
ActivityCompat.startActivity(currentContext, intent, postcard.getOptionCompat());
}
if ((postcard.getEnterAnim()!=0||postcard.getExitAnim()!=0) && currentContext instanceof Activity) {
((Activity)currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
}
if (callback != null) {
callback.onArrival(postcard);
}
}
});
break;
}
return null;
}
prepareCard的实现如下:
private void prepareCard(Postcard card){
RouteMeta routeMeta = Warehouse.routes.get(card.getPath());
if (routeMeta == null) {
Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(card.getGroup());
if (groupMeta == null) {
throw new NoRouteFoundException("没找到对应路由: " + card.getGroup() + " " +
card.getPath());
}
IRouteGroup iRouteGroup;
try {
iRouteGroup = groupMeta.getConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("路由分组映射表记录失败.", e);
}
iRouteGroup.loadInto(Warehouse.routes);
Warehouse.groupsIndex.remove(card.getGroup());
prepareCard(card);
}else {
card.setDestination(routeMeta.getDestination());
card.setType(routeMeta.getType());
}
}
这段代码Warehouse.routes.get(card.getPath())通过path拿到对应的RouteMeta,这个RouteMeta里面保存了activityClass等信息。继续往下看,如果判断拿到的RouteMeta是空,说明这个路由地址还没有加载到map里面(初始化时为了节省性能,只会加载所有的分组信息,而每个分组下的路由映射关系,会使用懒加载,在首次用到的时候去加载),只有在第一次用到当前路由地址的时候,会去Warehouse.routes里面拿routeMeta,如果拿到的是空,会根据当前路由地址的group拿到对应的分组,通过反射创建实例,然后调用实例的loadInto方法,把它里面保存的映射信息添加到Warehouse.routes里面,并且再次调用prepareCard(card),这时再通过Warehouse.routes.get(card.getPath())就可以顺利拿到RouteMeta了。进入else{}里面,调用了card.setDestination(routeMeta.getDestination()),这个setDestination就是将RouteMeta里面保存的activityClass放入Postcard里面。
prepareCard()方法调用完成后,我们的postcard里面就保存了activityClass,然后switch (postcard.getType()){}会判断postcard的type为ACTIVITY,然后通过ActivityCompat.startActivity启动Activity。到这里,路由跳转的实现已经讲解完毕了。
结束语
关于组件之间传递数据,可以参考ARoute添加Extra 注解,ExtraProcessor处理器以及IExtra接口。
通过手写ARoute我们会学到注解,注解处理器,JavaPoet和组件化思路,编写框架的思路等等。