谈谈App的统一跳转和ARouter
作者:菜刀文
简书:http://www.jianshu.com/u/ccabae3e72f2
App中每次页面跳转,都需要调用统一导航, 它用的非常频繁, 有必要对它进行一下梳理. 让他能用起来简单方便, 同时能支持各种常用的跳转业务场景.
一. Android跳转遇到的问题
1.intent-filter跳转不好管理
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("smsto:10086"));
context.startActivity(intent);
如果项目分多个Module, Activity需要在各自Module的AndroidManifest.xml中声明,容易重复,不好统一管理.
2.Activity class跳转耦合性高
//通过设置目标class跳转
Intent intent = new Intent();
intent.setClass(context,TargetActivity.class);
context.startActivity(intent);
A如果要跳转到TargetActivity, A要引用到TargetActivity. 造成:
- 如果项目多个Module开发,底层module不能跳转到高层Activity
- 如果TargetActivity类名变化, 对应调用方都需要改动
3. 混合开发时,H5/Weex跳转新界面不方便
内置H5要跳转 Native页面, 通过JsBidge把目标信息传过来.
两种方式:
方式1: 直接提供目标Activity的 Action 跳过去.
方式2: Native维护一个<描述,Activity信息>的Map, H5传过来Activiy的"描述", Native在Map中查到后,进行跳转.
方式1的问题:
一般H5会同时在"Android/ios"容器中, 所以最好的实践是:H5做跳转时不需要区分平台和版本. 如果利用Action跳转,
1)Action命名要符合两个平台的规范
2)如果Native不支持目标Action,还需要做跳转失败后处理.
方式2的问题:
1)维护<描述,Activity信息>的列表麻烦事,需要单独角色管理.
2)同样存的"Activity信息"也有问题1,2中提到的问题
都有的问题:
处理跳转的Bridge类,可能拿不到context,这需要拿Application的Context,大家都判断略嫌麻烦.
4.跳转到"未知页面"的统一处理
比如2.0版本新加了"消息"功能,App1.0版本没有.
此时1.0版本的App中,"H5/push" 尝试打开"消息"页面, 肯定是不支持的. 这时候有几种策略:
- H5/Push能判断Native支持页面的能力,如果不支持,就不调用
- Native收到调用未知页面, 不做任何动作.
- Native收到调用未知页面, 提示这是新版功能,建议更新版本.
5. 业务降级/重定向
- 比如A/B测试:
Native可以根据配置, 跳转不同的实现页面 - 业务降级:
某个业务本来Native实现, 降级为H5实现, 这时候跳转时切换到H5页面.
6.统一加参
跳转到目标页面前,能统一加参数.
实现比如打点, 添加通用参数操作.
7.外部调用的统一入口
考虑这种业务场景: App有 A,B,C三个页面, 提供给外部调用.
这时候一般两种实现方式:
方式1: A,B,C的Activity 在AndroidManifest.xml中export=true,并且设置 intent-filter
方式2: App设置一个统一的Router-Activity, 外部跳转到A,B,C 都统一先统一到Router-Activity, 他在拉起A,B,C
方式1分析:
除非真的提供通用的功能(拍照/图片处理/..)给外部调用, 否则export一个Activity是不必要也不安全的. 为了安全,App不会export大量的Activity. 这意味着通过这种机制, 外部能调用内部的功能较少.
方式2分析:
优点:
- 只暴露了一个Router-Activity. 安全和好管理.
- Router-Activity里面可以做一些调用者的安全校验, 如果校验通过可以运行跳转App的全部页面. 这样给能外部调用app更多页面的机会, 也兼顾了安全.
缺点:
外部跳转需要一个Activity中转一下,直观上感觉效率低一些. 但是实际感觉基本没有影响.
二. 明确需求
根据问题和业务场景, 我们的"统一跳转"的需求也基本明确:
- "声明/使用" 简单.
- 适用多module开发,避免直接依赖.
- 统一协议, 适用"H5/Weex/Native" 跳转 "Native", 对"Android/ios"两个平台协议应该是一样的.
- 有统一的外部调用入口
- 能对"不支持"的跳转统一处理
- 支持跳转前预处理
- 支持重定向
三.解决方案ARouter
ARouter
ARouter-github 很好的解决了上述问题.
下面是他的对应的方案.
1.使用简单:
- 每个Activity在类中自声明,"代码-路径"对应一目了然
@Route(path = "/test/activity")
public class YourActivity extend Activity {
...
}
- 跳转新页面简单,不需要知道目标ActivityContext,intent-filter,目标的Activity
ARouter.getInstance().build(path).with(bundle).navigation();
2.页面利于统一管理
所有页面可以统一定义. 一目了然
String PAGE_MAIN = "/navigateTo/main";
String PAGE_H5 = "/navigateTo/h5";
String PAGE_WEEX = "/navigateTo/weex";
...
3.便于设置统一Activity承载外部跳转
public class SchameFilterActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//安全/版本校验
....
Uri uri = getIntent().getData();
ARouter.getInstance().build(uri).navigation();
finish();
}
}
4.处理"未知页面"的跳转结果
ARouter.getInstance().build("/test/1").navigation(this, new NavigationCallback() {
@Override
public void onFound(Postcard postcard) {
...
}
@Override
public void onLost(Postcard postcard) {
//可以处理,提示升级版本之类
}
});
5. 自定义全局降级策略
// 实现DegradeService接口,并加上一个Path内容任意的注解即可
@Route(path = "/xxx/xxx")
public class DegradeServiceImpl implements DegradeService {
@Override
public void onLost(Context context, Postcard postcard) {
// do something.
}
@Override
public void init(Context context) {
}
}
6. 重写跳转URL实现重定向
// 实现PathReplaceService接口,并加上一个Path内容任意的注解即可
@Route(path = "/xxx/xxx") // 必须标明注解
public class PathReplaceServiceImpl implements PathReplaceService {
/**
* For normal path.
*
* @param path raw path
*/
String forString(String path) {
return path; // 按照一定的规则处理之后返回处理后的结果
}
/**
* For uri type.
*
* @param uri raw uri
*/
Uri forUri(Uri uri) {
return url; // 按照一定的规则处理之后返回处理后的结果
}
}
技术分析
1.建立 Url-Activity 的对应关系
ARouter最后是通过下面方式跳转的.
//_ARouter.java
Intent intent = new Intent(currentContext,postcard.getDestination());
intent.putExtras(postcard.getExtras());
所以要AROUTER需要维护一个 Path和Activity class的对应关系.
他利用
- javapoet 在编译时候生成类信息
- 初始化时,收集主创Path/Activity信息. 所有信息存在WareHouse中.
2. 跳转流程
screenshot.png其他技术
1. 属性设置在gradle.properties中
BUILDTOOLS_VERSION=25.0.0
使用:
compile "com.android.support:support-v4:${SUPPORT_LIB_VERSION}"
buildToolsVersion BUILDTOOLS_VERSION
2.TreeMap
HashMap通过hashcode对其内容进行快速查找,而 TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)
3.Instrumentation的使用
你可以将Instrumentation理解为一种没有图形界面
的,具有启动能力的,用于监控其他类(用Target
Package声明)的工具类。任何想成为Instrumentation的类必须继承android.app.Instrumentation。
下面是这个类的解释:
“Base class for implementing application instrumentation code. When running with instrumentation turned on, this class will be instantiated for you before any of the application code, allowing you to monitor all of the interaction the system has with the application. An Instrumentation implementation is described to the system through an AndroidManifest.xml's tag.“
4.volatile
- volatile重要工作是避免线程脏读:当线程对volatile变量进行读操作时,会先将2. 自己工作内存中的变量置为无效,之后再通过主内存拷贝新值到工作内存中使用。
- volatile解决的是变量在多个线程之间的可见性,但不能完全保证数据的原子性。
现在JVM经过优化,已不会出现liveness failure 。所以没事别用volatile。
5. CountDownLatch
CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0为止。
6.获取CPU个数
CPU_COUNT = Runtime.getRuntime().availableProcessors()
7.捕捉线程异常
// 捕获多线程处理中的异常
thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable ex) {
ARouter.logger.info(Consts.TAG, "Running task appeared exception! Thread [" + thread.getName() + "], because [" + ex.getMessage() + "]");
}
});
8.javapoet
build classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
使用 annotationProcessor
dependencies {
annotationProcessor project(':arouter-compiler')
}
9.Activity启动
int flags = postcard.getFlags();
if (-1 != flags) {
intent.setFlags(flags);
} else if (!(currentContext instanceof Activity)) {
// Non activity, need less one flag.
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
10. 访问者模式
//设置WareHouse
public interface IRouteGroup {
/**
* Fill the atlas with routes in group.
*/
void loadInto(Map<String, RouteMeta> atlas);
}
iGroupInstance.loadInto(Warehouse.routes);