组件化路由及AnnotationProcessor

1,246 阅读8分钟

组件化开发中涉及最多的就是不同module之间Activity的跳转,跳转可以有多种方式实现:

  • 隐式跳转
  • DeepLink
  • 反射

虽然方式有多种,但是都会有一些使用上或者性能上麻烦的问题。比如隐式跳转需要在清档文件中为每个Activity配置action或者category,DeepLink需要在清档文件中为每个Activity配置scheme和host,而反射则需要拿到每个Activity的全路径去获取Class文件,并且频繁的反射也会造成一定的性能问题。

跳转类型分为两大类:一类是本应用内部的跳转,另一类是第三方应用跳转到本应用。

本应用内部的跳转

应用内部跳转最容易就是拿到对应Activity的Class对象,然后根据Intent做跳转。既然运行时反射比较好性能,就可以通过编译时注解解决该问题。

编译时注解的主要原理是:通过给Activity添加自定义注解,然后通过AnnotationProcessor可以在编译期间拿到这些添加了注解的Activity的Class对象,然后自动生成中间Java类,生成一些静态方法,在该方法中生成一些代码,这些代码的作用是把所有的Activity添加到一个集合中。但是此时只是生成代码,并没有把Activity添加到集合中,因为添加的操作是发生在运行时,所以只需要在程序初始化时调用一下生成的静态方法即可。

主要有以下几个步骤:

1.创建Java library-routeAnnotation

该library的唯一作用就是声明注解,只有一个注解类,代码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Route {

    String value() default "";

}

2.创建Java library-routeCompiler

该library的唯一作用就是拿到所有用Route注解修饰的Activity,并且生成把Activity存入到集合的Java类。

想要在编译期间自动生成Java类,我们需要自定义一个类,继承自系统的AbstractProcessor,然后需要使用谷歌提供的两个依赖帮助我们简化AnnotationProcessor的设置。在生成Java文件时需要使用到Javapoet库。并且拿到的是有Route注解修饰的类,所以也需要依赖我们自己的routeAnnotation。所以该library的gradle依赖如下:

dependencies {
    implementation project(path: ':routeAnnotation')
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
    compileOnly 'com.google.auto.service:auto-service-annotations:1.0-rc7'
    implementation 'com.squareup:javapoet:1.8.0'
}

3.在AbstractProcessor中生成Java文件

如果只是在Processor中生成一个Java文件,配合Javapoet来做是一件非常容易的事情。但是对于生成要保存所有Activity到一个集合中的Java类并非这么简单,主要是因为该集合并不是在routeCompiler库中的,而是在routeApi库中。

先来看下最终生成文件的结果:

RouteInit类

该类是用来调用每个module下自动生成的RouteSaving_moduleName.java类下的add方法的。最终结果如下,例如项目中app为主module,还有另外两个业务逻辑的module:module1和module2.那么在每个module下都会生成一个RouteSaving文件,并且调用每个文件的add方法。

public final class RouterInit {
  public static final void init() {
    RouterSaving_app.add();
    RouterSaving_module1.add();
    RouterSaving_module2.add();
  }
}

RouterSaving_routeName类

该类用来生成存储对应module下每个Activity的代码,例如module1下的文件如下,module1下只有一个Activity使用了Route注解修饰,那么RouterSaving类就会生成把该Activity存入到集合的代码。

public final class RouterSaving_module1 {
  public static final void add() {
    com.example.routeapi.Routers.addRoute("test://module1?key1=123&key2=456", Module1Activity.class);
  }
}

所以RouteInit和RouterSaving两个类是这样配合的:

每个module都会有一个RouterSaving类,每个RouterSaving类会收集对应module下的所有activity,并且生成添加到集合中的代码。

RouterInit类调用所有的RouterSaving类的添加代码,这样整个项目下所有module下的所有带Route注解的Activity都会存入到同一个集合当中。

以上代码都是在编译期间生成的,只是生成了代码,并没有真正的向集合中添加Activity信息。想要达到效果只需要在Application的onCreate中调用RouterInit.init()即可。这样集合就存储了所有的Activity信息,在使用时只需要遍历匹配然后使用Intent跳转即可。

大致原理是这样的,但是仍然会遇到几个小的点:

  • RouterInit和RouterSaving_moduleName都是自动生成的类,只要module需要自动生成类,那么两个类都会生成。但是我们需要保证只在项目主module-也就是app下生成RouterInit,其他module不需要。
  • 生成的RouterSaving_moduleName是以module的名称为后缀的,如何拿到module的名称呢

解决以上两个问题的实质就是:如何在AbstractProcessor中拿到是否是主module以及每个module的名称即可。

其实是可以拿到的,在AbstractProcessor中重写一个方法,并且添加需要的返回值即可

@Override
public Set<String> getSupportedOptions() {
    Set<String> set = new HashSet<>();
    set.add(MODULE_NAME_KEY);
    set.add(MODULE_MAIN_KEY);
    set.add(MODULES_LIST);
    return set;
}

那返回值具体都是什么呢?就是一个键值对中key的集合,这些键值对需要在各自module下的gradle中声明,如在app下的声明:

android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [module_name: project.getName(), module_main: "yes", modules_list: "app&module1&module2"]
            }
        }
    }
}

这样在AbstractProcessor的init方法中就可以得到键值对的Map集合:

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    // 获取gradle中配置的参数
    Map<String, String> options = processingEnvironment.getOptions();
}

例如在app中声明了3个键值对,key分别是module_name、module_main、modules_list。其中module_name就是module的名称,这样在生成RouterSaving_moduleName时就可以有具体的值了。

module_main代表当前module是不是项目的主module,只有在主module的gradle中配置,其他module不需要。modules_list是哪些module使用到了Route,这些module都需要在这里配置一下,是因为RouterInit类只在主项目中生成,他内部需要拿到每个RouterSaving_moduleName的路径和名称,当前module又无法获取都其他module的名称,所以只能在这里配置一下。

上面说了一下原理和注意的点,下面给出最终的AnnotationProcessor的内容:

@AutoService(Processor.class)
public class RouteAnnotationProcessor extends AbstractProcessor {

    private static final String MODULE_NAME_KEY = "module_name";
    private static final String MODULE_MAIN_KEY = "module_main";
    private static final String MODULES_LIST = "modules_list";
    private static final String MODULE_MAIN_VALUE = "yes";

    // 用来生成Java文件
    private Filer filer;
    // 用来在编译期间打印信息
    private Messager messager;
    // 每个module的名称
    private String moduleName;
    // 是不是主项目module
    private String moduleMain;
    // 所有module使用&符号拼接起来的字符串,如:"app&module&module2"
    private String modules;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnvironment.getFiler();
        messager = processingEnvironment.getMessager();
        // 获取gradle中配置的参数
        Map<String, String> options = processingEnvironment.getOptions();
        if (options != null && !options.isEmpty()) {
            moduleName = options.get(MODULE_NAME_KEY);
            moduleMain = options.get(MODULE_MAIN_KEY);
            modules = options.get(MODULES_LIST);
        }
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 如果一个module下有Route注解则会走到process方法,否则不会
        generateInit();
        generateList(roundEnvironment);
        return true;
    }

    /**
     * 生成RouteInit文件
     * 在该文件中生成一个静态方法,有多少个module中使用了Route,则为每个module调用RouterSaving_moduleName.add()。
     * 这样在RouterSaving_moduleName中向集合中存入Activity信息的代码就会生成。
     * 需要注意的是:这里只是编译阶段执行的,真正存入集合是在运行阶段,所以会在Routers类的init方法中动态执行
     */
    private void generateInit() {
        // 如果是主module才去生成RouterInit类
        if (MODULE_MAIN_VALUE.equals(moduleMain)) {
            // 声明RouterInit中的init方法
            MethodSpec.Builder initMethod = MethodSpec.methodBuilder("init")
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC);
            // 在init方法中要调用所有module下RouterSaving_的add方法
            if (modules != null && !"".equals(modules)) {
                String[] moduleList = modules.split("&");
                if (moduleList.length > 0) {
                    for (String module : moduleList) {
                        initMethod.addStatement("RouterSaving_" + module + ".add()");
                    }
                }
            }

            // shengm
            TypeSpec routerInit = TypeSpec.classBuilder("RouterInit")
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(initMethod.build())
                    .build();

            try {
                JavaFile.builder("com.example.route", routerInit)
                        .build()
                        .writeTo(filer);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 生成RouteSaving_moduleName文件
     * 在该文件中生成一个静态方法,遍历当前module中有多少个Activity被Route注解修饰了,然后拿到当前Activity
     * 的path和class对象,存入到集合中。该静态方法的调用是在上面的RouteInit类中,这里仅是向静态方法中写入代码
     */
    private void generateList(RoundEnvironment roundEnvironment) {
        MethodSpec.Builder initMethod = MethodSpec.methodBuilder("add")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC);

        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Route.class);
        for (Element element : elements) {
            Route annotation = element.getAnnotation(Route.class);
            String path = annotation.value();
            ClassName className = ClassName.get((TypeElement) element);
            initMethod.addStatement("com.example.routeapi.Routers.addRoute($S, $T.class)", path, className);
        }

        TypeSpec routerInit = TypeSpec.classBuilder("RouterSaving_" + moduleName)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(initMethod.build())
                .build();

        try {
            JavaFile.builder("com.example.route", routerInit)
                    .build()
                    .writeTo(filer);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> set = new HashSet<>();
        // 指定当前module下只支持Route注解
        set.add(Route.class.getCanonicalName());
        return set;
    }

    @Override
    public Set<String> getSupportedOptions() {
        Set<String> set = new HashSet<>();
        set.add(MODULE_NAME_KEY);
        set.add(MODULE_MAIN_KEY);
        set.add(MODULES_LIST);
        return set;
    }
}

4.创建Android library-routeApi

该library的作用是提供具体的api让外部调用。因为外部调用时是使用的该库,所以存储所有的Activity的集合是在这里面的,不是在routeAnnotation库也不是在routeCompiler库。上面提到过RouterInit和RouterSaving_moduleName两个类只是生成了把Activity添加到集合中的代码,真正添加到集合中需要在运行期间开启。

在该library下提供了一个Routers类,他有两个作用:

  • init

init的作用一个是获取到一个Application,在跳转的时候如果没有传入context,则使用Application跳转。另一个作用就是开启向集合中添加Actiivty的操作。这里我们并不是直接调用RouterInit的init方法,因为无法访问,所以我们通过反射的方式,拿到RouterInit,并且调用它的init方法。这样在Application的onCreate方法中只需要调用Routers.init()即可。

@Override
public void onCreate() {
    super.onCreate();
    Routers.init(this);
}
  • navigation

用来做具体的跳转,跳转很简单,在存储Activity时,我们存储了注解中的path路劲,跳转时也会传入一个路径,此时我们匹配host,如果host相同,则认为是当前要跳转的Activity,此时取出来Activity的Class对象,然后通过Intent传递path信息,最后直接跳转:

public void navigation(Context context, String path) {
    for (RouterInfo routerInfo : list) {
        if (routerInfo.getPath().equals(path)) {
            Intent intent = new Intent(context, routerInfo.getActivity());
            intent.putExtra(RAW_URL, path);
            context.startActivity(intent);
            break;
        }
    }
}

第三方应用跳转到本应用

外部应用打开本应用中的某个页面,需要使用到DeepLink,如果有多个页面,需要给每个页面都设置scheme和host,比较麻烦,可以建立一个统一处理的Activity,只给该Activity设置scheme和host,其他应用统一拉起该页面,该页面根据URI去拉起具体的页面,拉起具体页面就是上面本应用内部的跳转逻辑了。

统一处理DeepLink的Activity:

public class RouterActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Uri uri = getIntent().getData();
        Routers.getInstance().navigation(this, uri.toString());
        finish();
    }
}

由于代码很多,所以在博客中并没有列出所有的代码,下面是Git上的demo Router

参考

阿里巴巴ARouter:github.com/alibaba/ARo…

ActivityRouter:github.com/mzule/Activ…