Android 开发中,组件化,模块化是一个老生常谈的问题。随着项目复杂性的增长,模块化是一个必然的趋势。除非你能忍受改一下代码,就需要六七分钟的漫长时间。
模块化,组件化随之带来的另外一个问题是页面的跳转问题,由于代码的隔离,代码之间有时候会无法互相访问。于是,路由(Router)框架诞生了。
目前用得比较多的有阿里的 ARouter,美团的 WMRouter,ActivityRouter 等。
今天,就让我们一起来看一下怎样实现一个路由框架。
实现的功能有。
- 基于编译时注解,使用方便
- 结果回调,每次跳转 Activity 都会回调跳转结果
- 除了可以使用注解自定义路由,还支持手动分配路由
- 支持多模块使用,支持组件化使用
使用说明
基本使用
第一步,在要跳转的 activity 上面注明 path,
@Route(path = "activity/main") public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }
在要跳转的地方
Router.getInstance().build("activity/main").navigation(this);
如果想在多 moule 中使用
第一步,使用 @Modules({"app", "sdk"}) 注明总共有多少个 moudle,并分别在 moudle 中注明当前 moudle 的 名字,使用 @Module("") 注解。注意 @Modules({“app”, “sdk”}) 要与 @Module(“”) 一一对应。
在主 moudle 中,
@Modules({"app", "moudle1"}) @Module("app") public class RouterApplication extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); Router.getInstance().init(); } }
在 moudle1 中,
@Route(path = "my/activity/main") @Module("moudle1") public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main_2); } }
这样就可以支持多模块使用了。
自定义注入 router
Router.getInstance().add("activity/three", ThreeActivity.class);
跳转的时候调用
Router.getInstance().build("activity/three").navigation(this);
结果回调
路由跳转结果回调。
Router.getInstance().build("my/activity/main", new RouterCallback() { @Override public boolean beforeOpen(Context context, Uri uri) { // 在打开路由之前 Log.i(TAG, "beforeOpen: uri=" + uri); return false; } // 在打开路由之后(即打开路由成功之后会回调) @Override public void afterOpen(Context context, Uri uri) { Log.i(TAG, "afterOpen: uri=" + uri); } // 没有找到改 uri @Override public void notFind(Context context, Uri uri) { Log.i(TAG, "notFind: uri=" + uri); } // 发生错误 @Override public void error(Context context, Uri uri, Throwable e) { Log.i(TAG, "error: uri=" + uri + ";e=" + e); } }).navigation(this);
startActivityForResult 跳转结果回调
Router.getInstance().build("activity/two").navigation(this, new Callback() { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.i(TAG, "onActivityResult: requestCode=" + requestCode + ";resultCode=" + resultCode + ";data=" + data); } });
原理说明
实现一个 Router 框架,涉及到的主要的知识点如下:
- 注解的处理
- 怎样解决多个 module 之间的依赖问题,以及如何支持多 module 使用
- router 跳转及 activty startActivityForResult 的处理
我们带着这三个问题,一起来探索一下。
总共分为四个部分,router-annotion, router-compiler,router-api,stub
router-annotion 主要是定义注解的,用来存放注解文件
router-compiler 主要是用来处理注解的,自动帮我们生成代码
router-api 是对外的 api,用来处理跳转的。
stub 这个是存放一些空的 java 文件,提前占坑。不会打包进 jar。
router-annotion
主要定义了三个注解
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.CLASS) public @interface Route { String path(); }
@Retention(RetentionPolicy.CLASS) public @interface Modules { String[] value(); }
@Retention(RetentionPolicy.CLASS) public @interface Module { String value(); }
Route 注解主要是用来注明跳转的 path 的。
Modules 注解,注明总共有多少个 moudle。
Module 注解,注明当前 moudle 的名字。
Modules,Module 注解主要是为了解决支持多 module 使用的。
router-compiler
router-compiler 只有一个类 RouterProcessor,他的原理其实也是比较简单的,扫描那些类用到注解,并将这些信息存起来,做相应的处理。这里是会生成相应的 java 文件。
主要包括以下两个步骤
- 根据是否有 @Modules @Module 注解,然后生成相应的 RouterInit 文件
- 扫描 @Route 注解,并根据 moudleName 生成相应的 java 文件
注解基本介绍
在讲解 RouterProcessor 之前,我们先来了解一下注解的基本知识。
如果对于自定义注解还不熟悉的话,可以先看我之前写的这两篇文章。Android 自定义编译时注解1 - 简单的例子,Android 编译时注解 —— 语法详解
public class RouterProcessor extends AbstractProcessor { private static final boolean DEBUG = true; private Messager messager; private Filer mFiler; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); messager = processingEnv.getMessager(); mFiler = processingEnv.getFiler(); UtilManager.getMgr().init(processingEnv); } /** * 定义你的注解处理器注册到哪些注解上 */ @Override public Set<String> getSupportedAnnotationTypes() { Set<String> annotations = new LinkedHashSet<>(); annotations.add(Route.class.getCanonicalName()); annotations.add(Module.class.getCanonicalName()); annotations.add(Modules.class.getCanonicalName()); return annotations; } /** * java版本 */ @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } }
首先我们先来看一下 getSupportedAnnotationTypes 方法,这个方法返回的是我们支持扫描的注解。
注解的处理
接下来我们再一起来看一下 process 方法
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // 注解为 null,直接返回 if (annotations == null || annotations.size() == 0) { return false; } UtilManager.getMgr().getMessager().printMessage(Diagnostic.Kind.NOTE, "process"); boolean hasModule = false; boolean hasModules = false; // module String moduleName = "RouterMapping"; Set<? extends Element> moduleList = roundEnv.getElementsAnnotatedWith(Module.class); if (moduleList != null && moduleList.size() > 0) { Module annotation = moduleList.iterator().next().getAnnotation(Module.class); moduleName = moduleName + "_" + annotation.value(); hasModule = true; } // modules String[] moduleNames = null; Set<? extends Element> modulesList = roundEnv.getElementsAnnotatedWith(Modules.class); if (modulesList != null && modulesList.size() > 0) { Element modules = modulesList.iterator().next(); moduleNames = modules.getAnnotation(Modules.class).value(); hasModules = true; } debug("generate modules RouterInit annotations=" + annotations + " roundEnv=" + roundEnv); debug("generate modules RouterInit hasModules=" + hasModules + " hasModule=" + hasModule); // RouterInit if (hasModules) { // 有使用 @Modules 注解,生成 RouterInit 文件,适用于多个 moudle debug("generate modules RouterInit"); generateModulesRouterInit(moduleNames); } else if (!hasModule) { // 没有使用 @Modules 注解,并且有使用 @Module,生成相应的 RouterInit 文件,使用与单个 moudle debug("generate default RouterInit"); generateDefaultRouterInit(); } // 扫描 Route 注解 Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Route.class); List<TargetInfo> targetInfos = new ArrayList<>(); for (Element element : elements) { System.out.println("elements =" + elements); // 检查类型 if (!Utils.checkTypeValid(element)) continue; TypeElement typeElement = (TypeElement) element; Route route = typeElement.getAnnotation(Route.class); targetInfos.add(new TargetInfo(typeElement, route.path())); } // 根据 module 名字生成相应的 java 文件 if (!targetInfos.isEmpty()) { generateCode(targetInfos, moduleName); } return false; }
,首先判断是否有注解需要处理,没有的话直接返回 annotations == null || annotations.size() == 0 。
接着我们会判断是否有 @Modules 注解(这种情况是多个 moudle 使用),有的话会调用 generateModulesRouterInit(String[] moduleNames) 方法生成 RouterInit java 文件,当没有 @Modules 注解,并且没有 @Module (这种情况是单个 moudle 使用),会生成默认的 RouterInit 文件。
private void generateModulesRouterInit(String[] moduleNames) { MethodSpec.Builder initMethod = MethodSpec.methodBuilder("init") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC); for (String module : moduleNames) { initMethod.addStatement("RouterMapping_" + module + ".map()"); } TypeSpec routerInit = TypeSpec.classBuilder("RouterInit") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(initMethod.build()) .build(); try { JavaFile.builder(Constants.ROUTE_CLASS_PACKAGE, routerInit) .build() .writeTo(mFiler); } catch (Exception e) { e.printStackTrace(); } }
假设说我们有"app",“moudle1” 两个 moudle,那么我们最终生成的代码是这样的。
public final class RouterInit { public static final void init() { RouterMapping_app.map(); RouterMapping_moudle1.map(); } }
如果我们都没有使用 @Moudles 和 @Module 注解,那么生成的 RouterInit 文件大概是这样的。
public final class RouterInit { public static final void init() { RouterMapping.map(); } }
这也就是为什么有 stub module 的原因。因为默认情况下,我们需要借助 RouterInit 去初始化 map。如果没有这两个文件,ide 编辑器 在 compile 的时候就会报错。
compileOnly project(path: ':stub')
我们引入的方式是使用 compileOnly,这样的话再生成 jar 的时候,不会包括这两个文件,但是可以在 ide 编辑器中运行。这也是一个小技巧。