热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

AndroidRouter从0到1

在Android中启动Activity一般使用startActivity或者startActivityForResult,通过这种方法启动Activity的缺点是写代码时Activ

在Android中启动Activity一般使用startActivity或者startActivityForResult,通过这种方法启动Activity的缺点是写代码时Activity必须已经存在,这不利于多人协同工作,而且这样硬编码启动Activity也不够灵活, 如需要在H5界面中启动本地Activity,或者在server端配置客户端行为时,这样的启动方式显得比较笨重。

如果可以通过类似url的方式打开Activity,即通过解析一个url字符串就可以打开相应的界面,不仅非常酷,而且以上提到问题也可以得到解决。

思路

google Android router 可以发现其实已经有了不少可用的轮子:

《Android Router从0到1》

其中最后一个
ActivityRouter在之前的文章
Android组件化开发实践中提到过,这里我们来分析一下如何来实现一个Android Router,并拆一下
ActivityRouter,分析它的思路。

路由的目的就是把不同的请求交给不同的控制器,路由作为一个中间层,把页面请求和请求处理进行了解耦,而且还可以增加一些自定义功能,在灵活性和扩展性上做一些事情。

Android中我们的目的是建立url到Activity的一个映射,建立的过程要解决几个问题:

  1. url的定义和解析
    一个合理的url结构,不仅要方便理解,而且要方便快速解析查找。

  2. 路由表的建立
    路由表是路由中非常重要的一环,路由表一方面要可以快速查找,一方面要方便建立和维护。

  3. 数据传递
    启动Activity时,经常需要给新的Activity传递一些数据,使用路由后,需要设计一定的策略在Activiy之间传递数据。

下面就从以上几个方面来看一下ActivityRouter的实现。

url 的设计

url定义

url一般主要由Schema、Host、Path以及QueryParameter等构成。

我们在路由中使用自定义的Schema以和普通的http进行区分,Host可以在应用中使用同意的字符串或者可以省略,path用来设置Activity请求路径,QueryParameter可以做他用,完成数据传递的任务。

我们看一下ActivityRouter中url的使用:

mzule://main/0xff878798

上例中mzule是自定义的Schema,main是path,

0xff878798是自定义的Parameter 。

url 解析

url解析就是拿到字符串中的Schema、host、path、queryParameters。


public static Path create(Uri uri) {
Path path = new Path(uri.getScheme().concat("://"));
String urlPath = uri.getPath();
if (urlPath == null) {
urlPath = "";
}
if (urlPath.endsWith("/")) {
urlPath = urlPath.substring(0, urlPath.length() - 1);
}
parse(path, uri.getHost() + urlPath);
return path;
}
private static void parse(Path scheme, String s) {
String[] compOnents= s.split("/");
Path curPath = scheme;
for (String component : components) {
Path temp = new Path(component);
curPath.next = temp;
curPath = temp;
}
}

按照以上url的规范设计我们Android路由中的url,可以非常方便地使用Java的Api,以上代码是ActivityRouter对于url的解析,非常清晰易懂。

路由表的实现

在路由表中增加一条记录都需要那些东西呢?首先肯定需要一个url,其次需要知道跳转的Activity 的名字,最好再有可以传递的一些数据,我们来看一下ActivityRouter的实现:

//Mapping.java
private final String format;
private final Class activity;
private final MethodInvoker method;
private final ExtraTypes extraTypes;
private Path formatPath;

可以看到format其实就是我们需要的url,activity就是跳转的Activity,extraTypes是可以传递的数据,完全符合我们的需求。

如何根据url打开相应的Activity呢:

private static boolean doOpen(Context context, Uri uri, int requestCode) {
initIfNeed();
Path path = Path.create(uri);
for (Mapping mapping : mappings) {
if (mapping.match(path)) {
//activity router 不仅可以打开Activity,还可以执行一些方法
if (mapping.getActivity() == null) {
mapping.getMethod().invoke(context, mapping.parseExtras(uri));
return true;
}
Intent intent = new Intent(context, mapping.getActivity());
intent.putExtras(mapping.parseExtras(uri));
intent.putExtra(KEY_RAW_URL, uri.toString());
//如果context不是activity的实例(如是Application的实例),则需要添加Intent.FLAG_ACTIVITY_NEW_TASK,才可以正确打开Activity
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
if (requestCode >= 0) {
if (context instanceof Activity) {
((Activity) context).startActivityForResult(intent, requestCode);
} else {
throw new RuntimeException("can not startActivityForResult context " + context);
}
} else {
context.startActivity(intent);
}
return true;
}
}
return false;
}

可以参考注释理解代码,没有什么难度。

如何在路由表中插入这样一条条记录呢,如果每次增加一个功能就手动增加一条记录并不是非常明智的做法,扩展性和维护性都不好。ActivityRouter采用了Apt的方式,这是ActivityRouter相比于其他android router的一个亮点,这里重点介绍一下。

annotation

annotation其实在很多常用的第三方库中都会用到,如EventBus3、butterknife、dagger等。
annotation根据其作用可以分为三种:

  • 标记
    仅仅在源码中起作用,用于标示,功能类似于注释,如@Override

  • 编译时annotation
    在编译时起作用,可以在代码进行编译时对注解部分进行处理,比如根据annotation的部分自动生成代码的等,butterknife其实就到了annotation的这个功能

  • 运行时annotation
    可以在运行时根据annotation 通过反射实现一些功能

看一个ActivityRouter中的例子:

@Retention(RetentionPolicy.CLASS)
public @interface Module {
String value();
}

这里定义了一个annotation,在使用时可以@Module("sdk")这样使用。

可以看到定义annotation时也使用了annotation,它们是元注解:

元注解共有四种@Retention, @Target, @Inherited, @Documented

@Retention 保留的范围,默认值为CLASS. 可选值有三种

  • SOURCE, 只在源码中可用
  • CLASS, 在源码和字节码中可用
  • RUNTIME, 在源码,字节码,运行时均可用

@Target 可以用来修饰哪些程序元素,如 TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER等,未标注则表示可修饰所有

@Inherited 是否可以被继承,默认为false

@Documented 是否会保存到 Javadoc 文档中

Apt

Android-apt实际是一个插件,可以处理annotation processors,在编译阶段对annotation进行处理。这里ActivityRouter就是使用annotation通过Apt的方式自动生成我们的路由表的。
关于Apt的更多介绍和使用可以参考android-apt

使用apt之后通过继承AbstractProcessor来对annotation来进行处理:

@Override
public boolean process(Set annotations, RoundEnvironment roundEnv) {
debug("process apt with " + annotations.toString());
if (annotations.isEmpty()) {
return false;
}
boolean hasModule = false;
boolean hasModules = false;
// module
String moduleName = "RouterMapping";
Set 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 modulesList = roundEnv.getElementsAnnotatedWith(Modules.class);
if (modulesList != null && modulesList.size() > 0) {
Element modules = modulesList.iterator().next();
moduleNames = modules.getAnnotation(Modules.class).value();
hasModules = true;
}
// RouterInit
if (hasModules) {
debug("generate modules RouterInit");
generateModulesRouterInit(moduleNames);
} else if (!hasModule) {
debug("generate default RouterInit");
generateDefaultRouterInit();
}
// RouterMapping
return handleRouter(moduleName, roundEnv);
}

ActivityRouter对project中是否有多个module分别进行了处理。

javapoet是非常好用的一个java代码生成库,ActivityRouter使用javapoet处理annotation,在编译时生成路由映射表。

private void generateDefaultRouterInit() {
MethodSpec.Builder initMethod = MethodSpec.methodBuilder("init")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC);
initMethod.addStatement("RouterMapping.map()");
TypeSpec routerInit = TypeSpec.classBuilder("RouterInit")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(initMethod.build())
.build();
try {
JavaFile.builder("com.github.mzule.activityrouter.router", routerInit)
.build()
.writeTo(filer);
} catch (Exception e) {
e.printStackTrace();
}
}

以上代码生成RouterInit.java,并生成init方法。

private boolean handleRouter(String genClassName, RoundEnvironment roundEnv) {
Set elements = roundEnv.getElementsAnnotatedWith(Router.class);
MethodSpec.Builder mapMethod = MethodSpec.methodBuilder("map")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC)
.addStatement("java.util.Map transfer = null")
.addStatement("com.github.mzule.activityrouter.router.ExtraTypes extraTypes")
.addCode("\n");
for (Element element : elements) {
Router router = element.getAnnotation(Router.class);
String[] transfer = router.transfer();
if (transfer.length > 0 && !"".equals(transfer[0])) {
mapMethod.addStatement("transfer = new java.util.HashMap()");
for (String s : transfer) {
String[] compOnents= s.split("=>");
if (components.length != 2) {
error("transfer `" + s + "` not match a=>b format");
break;
}
mapMethod.addStatement("transfer.put($S, $S)", components[0], components[1]);
}
} else {
mapMethod.addStatement("transfer = null");
}
mapMethod.addStatement("extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes()");
mapMethod.addStatement("extraTypes.setTransfer(transfer)");
addStatement(mapMethod, int.class, router.intParams());
addStatement(mapMethod, long.class, router.longParams());
addStatement(mapMethod, boolean.class, router.booleanParams());
addStatement(mapMethod, short.class, router.shortParams());
addStatement(mapMethod, float.class, router.floatParams());
addStatement(mapMethod, double.class, router.doubleParams());
addStatement(mapMethod, byte.class, router.byteParams());
addStatement(mapMethod, char.class, router.charParams());
for (String format : router.value()) {
ClassName className;
Name methodName = null;
if (element.getKind() == ElementKind.CLASS) {
className = ClassName.get((TypeElement) element);
} else if (element.getKind() == ElementKind.METHOD) {
className = ClassName.get((TypeElement) element.getEnclosingElement());
methodName = element.getSimpleName();
} else {
throw new IllegalArgumentException("unknow type");
}
if (format.startsWith("/")) {
error("Router#value can not start with '/'. at [" + className + "]@Router(\"" + format + "\")");
return false;
}
if (format.endsWith("/")) {
error("Router#value can not end with '/'. at [" + className + "]@Router(\"" + format + "\")");
return false;
}
if (element.getKind() == ElementKind.CLASS) {
mapMethod.addStatement("com.github.mzule.activityrouter.router.Routers.map($S, $T.class, null, extraTypes)", format, className);
} else {
mapMethod.addStatement("com.github.mzule.activityrouter.router.Routers.map($S, null, " +
"new MethodInvoker() {\n" +
" public void invoke(android.content.Context context, android.os.Bundle bundle) {\n" +
" $T.$N(context, bundle);\n" +
" }\n" +
"}, " +
"extraTypes)", format, className, methodName);
}
}
mapMethod.addCode("\n");
}
TypeSpec routerMapping = TypeSpec.classBuilder(genClassName)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(mapMethod.build())
.build();
try {
JavaFile.builder("com.github.mzule.activityrouter.router", routerMapping)
.build()
.writeTo(filer);
} catch (Throwable e) {
e.printStackTrace();
}
return true;
}

以上代码用于根据注解生成一条条路由映射。我们可以看一下最终生成的文件:

public final class RouterMapping_app {
public static final void map() {
java.util.Map transfer = null;
com.github.mzule.activityrouter.router.ExtraTypes extraTypes;
transfer = null;
extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
extraTypes.setTransfer(transfer);
com.github.mzule.activityrouter.router.Routers.map("home/:homeName", HomeActivity.class, null, extraTypes);
transfer = null;
extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
extraTypes.setTransfer(transfer);
com.github.mzule.activityrouter.router.Routers.map("with_host", HostActivity.class, null, extraTypes);
transfer = new java.util.HashMap();
transfer.put("web", "fromWeb");
extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
extraTypes.setTransfer(transfer);
extraTypes.setLongExtra("id,updateTime".split(","));
extraTypes.setBooleanExtra("web".split(","));
com.github.mzule.activityrouter.router.Routers.map("http://mzule.com/main", MainActivity.class, null, extraTypes);
com.github.mzule.activityrouter.router.Routers.map("main", MainActivity.class, null, extraTypes);
com.github.mzule.activityrouter.router.Routers.map("home", MainActivity.class, null, extraTypes);
transfer = null;
extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
extraTypes.setTransfer(transfer);
com.github.mzule.activityrouter.router.Routers.map("logout", null, new MethodInvoker() {
public void invoke(android.content.Context context, android.os.Bundle bundle) {
NonUIActions.logout(context, bundle);
}
}, extraTypes);
transfer = null;
extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
extraTypes.setTransfer(transfer);
com.github.mzule.activityrouter.router.Routers.map("upload", null, new MethodInvoker() {
public void invoke(android.content.Context context, android.os.Bundle bundle) {
NonUIActions.uploadLog(context, bundle);
}
}, extraTypes);
transfer = null;
extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
extraTypes.setTransfer(transfer);
com.github.mzule.activityrouter.router.Routers.map("user/:userId", UserActivity.class, null, extraTypes);
com.github.mzule.activityrouter.router.Routers.map("user/:nickname/city/:city/gender/:gender/age/:age", UserActivity.class, null, extraTypes);
transfer = null;
extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
extraTypes.setTransfer(transfer);
com.github.mzule.activityrouter.router.Routers.map("user/collection", UserCollectionActivity.class, null, extraTypes);
}
}

这里有个小问题是,RouterInit 和 RouterMapping两个文件其实是在编译期生成的,而很明显我们在其他地方需要用到这两个文件,也就是在build之前就需要存在着两个文件,怎么处理呢?这里在gradle中的dependencies中使用provided,即我们可以提前写好空壳RouterInit 和 RouterMapping,然后通过provided的方式使得代码通过编译,但是在执行时实际使用的是之后生成的文件:

dependencies {
provided project(':stub')
compile 'com.github.mzule.activityrouter:annotation:1.1.5'
}

数据传递

在web中我们可以通过在url后添加参数访问提交数据,在Android router中我们同样可以把数据保存在url中,只有可以正确解析出数据即可。

我们看下ActivityRouter中实现:

public Bundle parseExtras(Uri uri) {
Bundle bundle = new Bundle();
// path segments // ignore scheme
Path p = formatPath.next();
Path y = Path.create(uri).next();
while (p != null) {
if (p.isArgument()) {
put(bundle, p.argument(), y.value());
}
p = p.next();
y = y.next();
}
// parameter
Set names = UriCompact.getQueryParameterNames(uri);
for (String name : names) {
String value = uri.getQueryParameter(name);
put(bundle, name, value);
}
return bundle;
}

合理解析url,可以把数据打包为bundle,在启动activity时传递过去。

ActivityRouter代码结构

《Android Router从0到1》

  • activityrouter 路由实现的主义逻辑
  • annotation 定义用到的annotation
  • app demo
  • app_module demo
  • compiler 处理annotation,实现apt
  • stub 提供编译期的RouterInit Router Mapping文件

总结

其实ActivityRouter中还实现了在浏览器中启动应用的界面,主要思路是启动一个透明activity,然后在activity中解析url,再启动目标activity。ActivityRouter也支持直接解析http,打开web界面。本文中不再进行分析,感兴趣的同学可以去看看源码。

欢迎关注公众号wutongke,每天推送移动开发前沿技术文章:

《Android Router从0到1》 wutongke

推荐阅读:

寻找卓越的(Android)软件工程师

想在Android中使用java8?你可能不再需要retrolambda了

你不知道一些神奇Android Api

Android增量编译3~5秒的背后


推荐阅读
  • Spring – Bean Life Cycle
    Spring – Bean Life Cycle ... [详细]
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • 字节流(InputStream和OutputStream),字节流读写文件,字节流的缓冲区,字节缓冲流
    字节流抽象类InputStream和OutputStream是字节流的顶级父类所有的字节输入流都继承自InputStream,所有的输出流都继承子OutputStreamInput ... [详细]
  • 在本文中,我们将为 HelloWorld 项目添加视图组件,以确保控制器返回的视图路径能够正确映射到指定页面。这一步骤将为后续的测试和开发奠定基础。首先,我们将介绍如何配置视图解析器,以便 SpringMVC 能够识别并渲染相应的视图文件。 ... [详细]
  • DAO(Data Access Object)模式是一种用于抽象和封装所有对数据库或其他持久化机制访问的方法,它通过提供一个统一的接口来隐藏底层数据访问的复杂性。 ... [详细]
  • 零拷贝技术是提高I/O性能的重要手段,常用于Java NIO、Netty、Kafka等框架中。本文将详细解析零拷贝技术的原理及其应用。 ... [详细]
  • Java高并发与多线程(二):线程的实现方式详解
    本文将深入探讨Java中线程的三种主要实现方式,包括继承Thread类、实现Runnable接口和实现Callable接口,并分析它们之间的异同及其应用场景。 ... [详细]
  • 如何在Java中使用DButils类
    这期内容当中小编将会给大家带来有关如何在Java中使用DButils类,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。D ... [详细]
  • 检查在所有可能的“?”替换中,给定的二进制字符串中是否出现子字符串“10”带 1 或 0 ... [详细]
  • 在 Java 中,`join()` 方法用于使当前线程暂停,直到指定的线程执行完毕后再继续执行。此外,`join(long millis)` 方法允许当前线程在指定的毫秒数后继续执行。 ... [详细]
  • 本题探讨如何编写程序来计算一个数值的整数次方,涉及多种情况的处理。 ... [详细]
  • 本文探讨了资源访问的学习路径与方法,旨在帮助学习者更高效地获取和利用各类资源。通过分析不同资源的特点和应用场景,提出了多种实用的学习策略和技术手段,为学习者提供了系统的指导和建议。 ... [详细]
  • 在Android 4.4系统中,通过使用 `Intent` 对象并设置动作 `ACTION_GET_CONTENT` 或 `ACTION_OPEN_DOCUMENT`,可以从相册中选择图片并获取其路径。具体实现时,需要为 `Intent` 添加相应的类别,并处理返回的 Uri 以提取图片的文件路径。此方法适用于需要从用户相册中选择图片的应用场景,能够确保兼容性和用户体验。 ... [详细]
  • Spring框架中的面向切面编程(AOP)技术详解
    面向切面编程(AOP)是Spring框架中的关键技术之一,它通过将横切关注点从业务逻辑中分离出来,实现了代码的模块化和重用。AOP的核心思想是将程序运行过程中需要多次处理的功能(如日志记录、事务管理等)封装成独立的模块,即切面,并在特定的连接点(如方法调用)动态地应用这些切面。这种方式不仅提高了代码的可维护性和可读性,还简化了业务逻辑的实现。Spring AOP利用代理机制,在不修改原有代码的基础上,实现了对目标对象的增强。 ... [详细]
  • 掌握Android UI设计:利用ZoomControls实现图片缩放功能
    本文介绍了如何在Android应用中通过使用ZoomControls组件来实现图片的缩放功能。ZoomControls提供了一种简单且直观的方式,让用户可以通过点击放大和缩小按钮来调整图片的显示大小。文章详细讲解了ZoomControls的基本用法、布局设置以及与ImageView的结合使用方法,适合初学者快速掌握Android UI设计中的这一重要功能。 ... [详细]
author-avatar
oFoUro_877
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有