手把手实现一个超简单版本的 Activity 路由框架

最初的时候,在学习路由框架时,发现了 ActivityRouter,但是这个项目只有代码实现和使用方法,缺少实现原理的介绍与实现的过程,对于刚刚接触这个 Router 的初学者来说,不知道怎么下口和学习。本篇文章记录如何使用注解处理器,到最终实现了一个最简单版本的 Activity 路由框架。

为了减少学习成本,本 demo 使用 java 语言。代码已上传到 github

步骤1 - 创建 annotation Module

新建 annotation Java Library 模块,定义 RouterAnnotation 注解,value 则表示页面的 uri 地址。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface RouterAnnotation {
    String value();
}

步骤二 - 创建 processor Module

新建 processors Java Library 模块,继承 AbstractProcessor 创建注解处理器.

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

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        System.out.println("---- process ----")
        return false;
    }
    
     @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(RouterAnnotation.class.getCanonicalName());
        return annotations;
    }

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

配置注解处理器,使编译期该注解处理器能参与进来。这里我们使用 google 提供的框架.在 processor 模块下的 build.gradle 添加如下依赖

implementation 'com.google.auto.service:auto-service:1.0-rc3'
// 该注解处理器是让 autoService 注解生效
annotationProcessor "com.google.auto.service:auto-service:1.0-rc3"
implementation project(':annotation')

步骤三:配置 app moudle

接下来我们在项目 app 模块下增加我们定义好的注解处理器依赖。

implementation project(':annotation')
annotationProcessor project(':processor')

给 MainActivity 配置 RouterAnnotation

@RouterAnnotation("demo://main_activity")
public class MainActivity extends AppCompatActivity 

gradlew build 构建项目,输出 “---- process ----” 则代表注解处理器成功运行。

步骤四:动态生成路由映射文件

通过上面的例子已成功让注解处理器参与编译过程,接下来我们让 RouterProcess 发挥更大的作用,使其生成 Activity 页面路由表,通过 uri 地址的方式启动 Activity。

首先说下大体的实现思路:先获取工程中使用 @RouterAnnotation 的地方,通过该注解获取页面 uri 的地址,以及定义该注解 Activity 的 class 名称。然后利用 javapoet 动态生成 Java 文件,该文件将每个页面的 uri 地址,以及对应的 Activity 的 class 名称插入到路由表里。这样通过 router 打开 uri 页面时,就可以进行路由表的匹配,然后通过 startActivity 的方式启动页面

接下来我们看代码实现流程👇

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 先不管这里,后面我们在看
        generateRouterInit();
        // 首先获取注解元素
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(RouterAnnotation.class);
        
        // 定义一个 public static 类型的 map 方法
        MethodSpec.Builder mapMethod = MethodSpec.methodBuilder("map").addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC);
        
        // 遍历注解元素
        for (Element element: elements) {
            if (element.getKind() == ElementKind.CLASS) {
                RouterAnnotation router = element.getAnnotation(RouterAnnotation.class);
                // 获取 activity 的 class name
                ClassName className = ClassName.get((TypeElement) element);
                // 获取 uri 地址
                String path = router.value();
                // 生成代码 Routers.map(uri, xxx.class); 
                // 这里是将 uri 与 activity记录插入到 Routers 路由表中
                mapMethod.addStatement("com.xiwna.processor.router.Routers.map($S, $T.class, null)", path, className);
            }
        }
        
        mapMethod.addCode("\n");

        // 生成 RouterMapping 文件
        TypeSpec helloWorldClass = TypeSpec.classBuilder("RouterMapping")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(mapMethod.build())
                .build();

        JavaFile javaFile = JavaFile.builder("com.xiwna.processor.router", helloWorldClass)
                .build();

        try {
            javaFile.writeTo(filer);
        } catch (IOException e) {
            // e.printStackTrace();
        }
    }
    
    return false

执行 gradlew build 构建,不出意外,会在 app/build/generated/ap_generated_sources 目录下会生成 RouterMapping.java 文件

image

如上图所以,我们生成一个 RouterMapping 文件。然后调用 Routers.map 方法插入一条路由记录。方法实现如下,后面我们会专门介绍 Routers 路由表的具体实现。

    // 将页面插入到路由表中
    public static void map(String path, Class<? extends Activity> activity, MethodInvoker method) {
        mappings.add(new Mapping(path, activity, method));
    }

map 方法将 uri 和 activity class 构造成 Mapping 对象,放入 mappings 的集合中。

接下来我们看怎么让该文件中的 RouterMapping.map 方法执行,使该路由记录插入到路由表中呢。这就用到我们前面说到的 generateRouterInit 方法了。

    private void generateRouterInit() {
        // 生成 public static init 方法
        MethodSpec.Builder initMethod = MethodSpec.methodBuilder("init")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC);
        
        // 该方法实现为 RouterMapping.map()
        initMethod.addStatement("RouterMapping.map()");

        // 生成 RouterInit 方法
        TypeSpec routerInit = TypeSpec.classBuilder("RouterInit")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(initMethod.build())
                .build();
        try {
            JavaFile.builder("com.xiwna.processor.router", routerInit)
                    .build()
                    .writeTo(filer);
        } catch (Exception e) {
           // e.printStackTrace();
        }
    }
image

看到这里明白了,我们新生成了文件,来进行路由表的初始化。至此,注解处理器的工作就完成了。

步骤五:使用 Router 打开页面 uri

接下来我们 Routers 路由表的实现。

// 路由记录
public class Mapping {
    private final String path;
    private final Class<? extends Activity> activity;
    private final MethodInvoker method;
    private String formatHost;

    public Mapping(String path, Class<? extends  Activity> activity, MethodInvoker method) {
        this.path = path;
        this.activity = activity;
        this.method = method;
        formatHost = Uri.parse(path).getHost();
    }
}

public class Routers {
    // 页面路由表
    private static List<Mapping> mappings = new ArrayList<>();

    // 路由表的初始化
    private static void initIfNeed() {
        if (mappings.isEmpty()) {
            RouterInit.init();
        }
    }
    
    // 将页面插入到路由表中
    public static void map(String path, Class<? extends Activity> activity, MethodInvoker method) {
        mappings.add(new Mapping(path, activity, method));
    }

    /**
     * 通过 router 打开 activity
     *
     * @param context
     * @param url
     * @return
     */
    public static boolean open(Context context, String url) {
        initIfNeed();
        Uri uri = Uri.parse(url);
        // 遍历路由表,进行 uri 的匹配,匹配成功,则启动对面的 Activity 页面
        for (Mapping mapping : mappings) {
            if (mapping.match(uri)) {
                Intent intent = new Intent(context, mapping.getActivity());
                intent.putExtras(mapping.parseExtras(uri));
                context.startActivity(intent);
                return true;
            }
        }
        return false;
    }
}

通过上面的代码我们得知,该路由表的实现内容是提供 map 方法,存储前面注解处理器生成的 RouterMapping 路由映射记录,保存到 mappings 列表中。然后打开 uri 时,进行路由表的初始化,并匹配合适的页面。

匹配规则比较简单,主要是判断 host 地址是否相同

public boolean match(Uri uri) {
    return this.formatHost.equals(uri.getHost());
}

匹配通过后,进行 Activty 的启动,如果 uri 链接中,有使用参数,则进行解析。

public Bundle parseExtras(Uri uri) {
    Bundle bundle = new Bundle();
    Set<String> names = UriCompact.getQueryParameterName(uri);
    for (String name: names) {
    String value = uri.getQueryParameter(name);
    put(bundle, name, value);
    }
    return bundle;
}

启动目标 Activity 页面

{
    intent intent = new Intent(context, mapping.getActivity());
    intent.putExtras(mapping.parseExtras(uri));
    context.startActivity(intent);
}

步骤六:增加 stub 模块

此时,进行 gradlew build 构建,会发生编译错误。那是因为 Routers 类中 initIfNeed 方法中的
RouterInit.init() 方法是在编译过程中才生成的。所以我们需要在编译期间就有 RouterInit 和 RouterMapping 文件。

这里我们增加 stub 模块,并只在编译期间依赖,使用 compileOnly project(':stub') 方式。
通过这种方式,就可以让编译通过,并且在打包运行时,使用的是注解处理器生成的文件。

image

image

至此一个完整的简单版本的 Router 框架就实现了。在组建化项目中,每个单独的模块都只需要插入路由记录到路由表中,这样作为基础模块的 Router,就可以打开各个模块的页面了。希望小伙伴们通过该 demo,也能一步一步实现 ActivityRouter。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352