跟我走,你也可以实现一个Android路由器
什么是路由?
路由器(Router),是连接因特网中各局域网、广域网的设备,它会根据信道的情况自动选择和设定路由,以最佳路径,按前后顺序发送信号。 ——————百度百科
以上这段话是百度百科对于路由器的定义,路由器几乎人人都在用,但是了解其原理的其实并不多,我们作为软件开发人员,也许并不需要知道路由器的硬件知识,但是它的软件实现其实给我们在开发工作中提供了一个很好的思路。其实说白了,路由器就是把一堆网络请求的URL路径统一管理,处理和分发给相应的控制器处理,我们可以在这个分发的过程中对我们的URL请求做一些处理,就好像你为了翻墙挂VPN一样,这些工作其实都是在网络请求前或者网络请求后做一些处理而达到的目的。
Android为什么要用路由器?
Android其实已经给我们提供了一个重要组件来进行跳转了,那就是Intent,至于Intent的一些相关知识如果铺开来说可能这一篇文章都打不住,所以我们只要知道Intent启动Activity有两种方式,一种是显式启动,一种是隐式启动,而这种原生的路由方案都或多或少的存在着一些问题:
1.显式启动:直接的类依赖,耦合严重。
2.隐式启动:规则集中管理,协作困难。
3.Manifest扩展性较差。
4.跳转过程中无法控制。
而如果我们使用自己编写的路由器则会带来一些以下一些优势:
1.通过Uri索引,不存在类的依赖。
2.分布式管理页面配置
3.良好的可扩展性
4.可以加入拦截器自定义路由规则。
从技术层面说了半天自定义路由器的优势,但是你可能还是想不通,为啥要用路由器呢?那我们就从业务层面来简单的说一下吧。
说到业务层面就不得不提组件化开发了,随着项目的发展,业务不断的壮大,业务模块越来越多,原来可能一个小伙伴就可以开发的项目,现在必须要2-3个小伙伴一起协同开发才能良好的运转下去,开发人员一旦多起来,不可避免的耦合问题就会越来越严重,比如各个模块间互相调用就是一个耦合的重灾区。
为了便于开发中的任务划分,也为了各个功能模块之间的功能独立,现在普遍的协作方式是每个开发者负责一个Android Library,然后各个Library都被一个壳Moudle引用,从而达到相互协作又可以独立开发的目的。(如果不明白就把这种方式想象成一个主程序引入了各种jar包,你可能是主程序的开发人员也可能是Jar包的开发人员)
那么在这种开发的架构下,界面跳转就成了一个难题,因为就算有开发规范,但是每次跳转都要保证类名正确且目标界面如果修改了类名,跳转界面也要跟着修改代码,这种强耦合实在是太不极客了。
现在大家明白为什么要引入自定义路由功能了吧?
说白了,如果你的项目小,1-2个人开发就OK,那你引入不引入路由器功能其实效果并不明显,但是如果你的项目属于中大型的,开发人员3个以上,那我的建议还是一定要引入自定义路由器的,它的优点,前面已经说过了。
自定义路由器的原理
为了实现这个路由,首先一点就是弄清楚,我们跳转到底需要什么参数,其实说到底,自定义路由也只不过是封装了Android的原生路由,不会自己另行实现跳转方式的。
所以在这种情况下,我们弄清楚原生路由是如何跳转的就比较重要了:
Intent intent = new Intent();
intent.setClass(FirstActivity.this, SecondActivity.class);
FirstActivity.this.startActivity(intent);
这是一个最最基本的显式跳转的代码了,看上边的代码,我们可以分析出来,我们要让一个界面跳转的到另一个界面,最需要的其实就是两个参数,一个是代表当前页面的上下文FirstActivity.this,它的本质其实就是一个Context,而另一个参数就是目标页面的class了。
好了 知道我们跳转需要的参数了,现在就要考虑如何存放我们注册在路由器的界面了,其实也不用多想,一个Map是最佳的解决方案,这个Map的key我们放置的是自定义界面的跳转路径,而这个Map的value我们存放的就是目标界面的class文件。
现在我们来思考一下软件的最简单的架构:
看图说话:调用Router,然后传入你之前初始化到存储器里的Uri地址,之后从容器(ArrayMap)中获取对应的目标界面的class对象,然后利用传入的Context对象调用跳转方法(StartActivity)跳转到目标界面。
这就是一个最简单的路由器了,但是你可能会问,Intent不止可以跳转Activity还可以启动Service和Recevier啊,是不是也可以封装起来呢?当然可以:
这里利用了Uri的Scheme来对我们要调用的容器进行分类,通过这个方式我们可以延伸出我们自己的自定义跳转类型,但是为了便于扩展,我们可以将这些不同的ArrayMap容器都封装到一个ArrayMap容器中进行统一管理,如果有新的路由类型,我们只要给这个最外部的ArrayMap加一个key(scheme头)和一个value(存放这个路由规则对应内容的ArrayMap就可以了)
OK,到这里我们基本上就弄清楚我们的路由容器的基本框架了,现在我们可以考虑一下,路由器给我们返回什么了,其实你可能会问,为什么需要返回什么呢?只要能直接跳转或者直接启动一个Service就可以啦。但是我们得考虑一个问题,那就是项目组里可能会有新人,这些新人可能一开始上手自定义的路由器会有一定的学习成本和错误概率,为了避免这种因为不熟悉或者粗心而导致的Bug产生,我们可以尽量的让我们的路由器更加偏向于原生化操作一些,至于怎么偏向原生化操作呢?
那当然就是返回一个Intent了,之后你拿已经做好处理的Intent想干嘛就干嘛,和原生操作一毛一样!
自定义路由器的使用方式
在说实现方式前,我们先来看看如何使用这个路由器吧。
首先,在你的壳App的自定义Application中进行Uri的注册,这里考虑到多人协作都修改一个Application不太安全,我的建议是每个开发人员可以写自己的注册类,之后再由Applicaiton中通过引入各自的注册类来实现页面的注册,避免一些小伙伴先提交再更新而出现的文件冲突。
好了现在开始看代码吧:
1.注册:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if(KeyPathConfig.DEBUG){
CoolRouterTool.openLog();
}
setUpPaths();
}
private void setUpPaths(){
CoolRouterTool.addPath(KeyPathConfig.ACTIVITY_SCHEME+"bbs.ac",BBSActivity.class);
}
}
2.跳转:
public class MainActivity extends AppCompatActivity {
TextView hello;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
hello=(TextView) this.findViewById(R.id.hello);
hello.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
/* Intent intent=CoolRouterTool.dispatchPath(MainActivity.this, KeyPathConfig.ACTIVITY_SCHEME+"bbs.ac");
startActivity(intent);*/
CoolRouterTool.build(KeyPathConfig.ACTIVITY_SCHEME+"bbs.ac").go(MainActivity.this);
}
});
}
}
这里可以看到注释的部分就是返回Intent的方式,而后边的跳转方式就是封装好的,各有优势,可以灵活使用。
3.跳转到Web:
public class BBSActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView tv = new TextView(this);
tv.setTextSize(50);
tv.setText("BBS!!!");
setContentView(tv);
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(BBSActivity.this,"点我干啥?", Toast.LENGTH_SHORT).show();
Intent intent=CoolRouterTool.dispatchPath(BBSActivity.this,"http://w121@@1&&……ASD&&sS*@!@(");
startActivity(intent);
}
});
}
}
这里可以注意看一下,就是这个跳转的Url路径其实不是一个网址,由于加入了Web路径检查,如果不符合规范的Url路径是不会跳转的,而是会抛出一个异常,所以你在测试的时候可以换成一个正规的Url就可以了。
使用起来是不是很简单呢?因为本身也没有多难。
自定义路由的实现方式
路由路径接口的定义:
public interface iPath<K,V> {
/**
* 添加路由路径
* @param keyPath 关键路径
* @param tClass 路径对应的Class
*/
void addPath(String keyPath, Class<K> tClass);
/**
* 分发路由路径
* @param context 跳转发起界面的Context
* @param keyPath 路由路径
* @return 返回对应的返回值
*/
V dispatchPath(Context context, String keyPath);
/**
* 清理路由路径
* @param keyPath 路由路径
* @return 返回操作结果 布尔值
*/
boolean clearPath(String keyPath);
/**
* 判断路由路径是否存在
* @param keyPath 路由路径
* @return 返回是否存在的返回值
*/
boolean isExistPath(String keyPath);
/**
* 分发Web路径
* @param keyPath 路由路径
* @return 返回对应的返回值
*/
V dispatchWebPath(Uri keyPath);
}
这里简单讲解一下,首先是iPath接口中的两个泛型,第一个泛型K是我们注册的路由的类型,比如Activity,Service等,第二个泛型V是我们的返回类型,比如我们刚才说到的Intent,其实就是这个V,只不过为了更加灵活便于扩展,我们将返回类型定义成了泛型。
定义好路径接口后,我们需要一个基类来统一这些路径接口的一些通用型操作:
public abstract class CommonIpath<K> implements iPath<K,Intent> {
private ArrayMap<String,Class<K>> mPaths;
public CommonIpath(){
CoolLog.i("add a path to router");
mPaths=new ArrayMap<>();
}
public void addPath(String keyPath, Class<K> tClass){
mPaths.put(keyPath,tClass);
}
public Intent dispatchPath(Context context, String keyPath){
Class<K> tClass=mPaths.get(keyPath);
if(tClass==null){
CoolLog.w("target class is null");
throwExceptions(keyPath);
}
return new Intent(context,tClass);
}
public boolean clearPath(String keyPath){
if(mPaths.get(keyPath)!=null){
mPaths.remove(keyPath);
}
return isExistPath(keyPath);
}
public boolean isExistPath(String keyPath){
return mPaths.get(keyPath) != null;
}
public abstract void throwExceptions(String keyPath);
public Intent dispatchWebPath(Uri keyPath){
if(!URLUtil.checkUrl(keyPath.toString())){
throwExceptions(keyPath.toString());
}
Intent intent=new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(keyPath);
return intent;
}
}
源码非常简单,就是对ArrayMap的一堆操作而已,这里其实是要注意类名定义那行,我们将iPath的V值定义成了Intent,并且给CommonIPath定义了泛型K,这样我们的ArrayMap存放的只能是我们在调用界面定义好的类型了。
我们可以注意到CommonIPath是一个抽象类,那么既然是抽象类,就肯定有他的子类继承它并实现了一些特有的方法将iPath差异化,其实路由框架默认提供的就是Activity,Service和BroadcastReceiver这三种路由类型,那么要实现CommonIpath的子类类型就很清楚了,看代码:
public class ActivityIPath extends CommonIpath<Activity> {
@Override
public void throwExceptions(String keyPath) {
throw new RouterNullWithActivityException(keyPath);
}
}
public class ServiceIPath extends CommonIpath<Activity> {
@Override
public void throwExceptions(String keyPath) {
throw new RouterNullWithServiceException(keyPath);
}
}
public class ReceiverIPath extends CommonIpath<Activity> {
@Override
public void throwExceptions(String keyPath) {
throw new RouterNullWithReciverException(keyPath);
}
}
这三个类都继承了CommonIPath,并且将其泛型设置成了自己对应的类型,既然继承了CommonIPath,那么就要实现其抽象方法 throwExceptions(),其目的也是为了在不规范操作路由的情况下可以快速的定位异常而设计的,现在我们来看看异常设计:
public class BaseAndroidException extends RuntimeException {
public BaseAndroidException(String name, String pattern) {
super(String.format("%s:%s(无法解析这个路由路径,你确定已经加入到公用路由器中了吗?)", name, pattern));
}
}
一个异常基类,起到的作用是规范化异常操作返回的异常日志。
public class RouterNullWithActivityException extends BaseAndroidException {
public RouterNullWithActivityException(String pattern) {
super("Activity", pattern);
}
}
public class RouterNullWithServiceException extends BaseAndroidException {
public RouterNullWithServiceException(String pattern) {
super("Service", pattern);
}
}
public class RouterNullWithReciverException extends BaseAndroidException {
public RouterNullWithReciverException(String pattern) {
super("Reciver", pattern);
}
}
还有一些跳转Web界面时可能发生的异常:
public class BaseWebException extends RuntimeException {
public BaseWebException(String name, String pattern) {
super(String.format("%s:%s (网络路径不规范,请规范路径后再次请求。)", name, pattern));
}
}
public class RouterUrlException extends BaseWebException {
public RouterUrlException( String pattern) {
super("HTTP", pattern);
}
}
异常的设计基本上没什么可说的和iPath的类型设计基本上是对应的,下面我们来看看核心的路由类:
public class CoolRouter {
String keyPath;
/**
* scheme->路由规则内容
*/
private ArrayMap<String,iPath> mPathFilters;
private CoolRouterOptions routerOptions=new CoolRouterOptions();
private CoolRouter() {
mPathFilters = new ArrayMap<>();
initDefault();
}
private void initDefault(){
addPathFilter(KeyPathConfig.ACTIVITY_SCHEME,new ActivityIPath());
addPathFilter(KeyPathConfig.RECEIVER_SCHEME,new ReceiverIPath());
addPathFilter(KeyPathConfig.SERVICE_SCHEME,new ServiceIPath());
addPathFilter(KeyPathConfig.HTTP_SCHEME,new HttpIPath());
}
private static class Holder{
private static final CoolRouter INSTANCE = new CoolRouter();
}
public static CoolRouter getIns(){
return Holder.INSTANCE;
}
private final <K,V>iPath<K,V> getPath(String keyPath){
ArrayMap<String,iPath> pathFilters=mPathFilters;
Set<String> keySet = pathFilters.keySet();
iPath<K, V> rule = null;
for (String scheme : keySet) {
if (keyPath.startsWith(scheme)) {
rule = pathFilters.get(scheme);
break;
}
}
return rule;
}
/**
* 添加跳转动画
* @param enterAnim 进入动画
* @param exitAnim 退出动画
* @return
*/
public CoolRouter setAnim(@AnimRes int enterAnim,@AnimRes int exitAnim){
routerOptions.setAnim(enterAnim,exitAnim);
return this;
}
public CoolRouter setRequestCode(int requestCode){
routerOptions.setRequestCode(requestCode);
return this;
}
public CoolRouter setBundle(Bundle bundle){
routerOptions.setBundle(bundle);
return this;
}
/**
* 添加自定义路径过滤器
* @param key
* @param iPath
*/
CoolRouter addPathFilter(String key,iPath iPath){
mPathFilters.put(key, iPath);
return this;
}
/**
* 添加自定义路径
* @param keyPath
* @param tClass
* @param <K>
* @return
*/
final <K> CoolRouter addPath(String keyPath,Class<K> tClass){
iPath<K,?> path=getPath(keyPath);
if(path ==null){
throw new BaseAndroidException("未知",keyPath);
}
path.addPath(keyPath,tClass);
return this;
}
/**
* 发送路由 跳转
* @param context
* @param keyPath
* @param <V>
* @return
*/
final <V> V dispatchPath(Context context,String keyPath){
iPath<?,V> path=getPath(keyPath);
Uri uri=Uri.parse(keyPath);
if(path ==null){
throw new BaseAndroidException("未知",keyPath);
}
if(uri.getScheme().equals("http")||uri.getScheme().equals("https")){
return path.dispatchWebPath(uri);
}else{
return path.dispatchPath(context,keyPath);
}
}
/**
* 传入路由路径 判断该路由是否存在
* @param keyPath
* @return
*/
final boolean routerIsExist(String keyPath){
iPath<?,?> path= getPath(keyPath);
if(path!=null&&path.isExistPath(keyPath)){
return true;
}else{
return false;
}
}
/**
*
* @param keyPath
* @return
*/
CoolRouter build(String keyPath){
this.keyPath=keyPath;
return this;
}
public void go(Context context){
Intent intent =dispatchPath(context,keyPath);
if(intent==null){
throw new RuntimeException("intent is null");
}
if(routerOptions.getRequestCode()>=0){
if(context instanceof Activity){
if (context instanceof Activity) {
((Activity) context).startActivityForResult(intent, routerOptions.getRequestCode());
} else {
CoolLog.w("Please pass an Activity context to call method 'startActivityForResult'");
context.startActivity(intent);
}
}
}
if (routerOptions.getEnterAnim() != 0 && routerOptions.getExitAnim() != 0
&& context instanceof Activity) {
// Add transition animation.
((Activity) context).overridePendingTransition(
routerOptions.getEnterAnim(), routerOptions.getExitAnim());
}
}
}
CoolRouter就是路由的核心类了,所有的分发规则和存储模式其实都是在这里定义的,而获取CoolRouter的方法是一个单例模式,目的就是在一个项目中我们不用为这个路由器创造多个实例从而影响我们的系统性能,CoolRouter其实是被分成了两个部分,一部分负责承上,比如可以设置跳转参数Bundle,可以设置跳转动画以及设置回调请求码,还有一部分是启下,主要的作用是添加过滤器,添加分发规则,判断当前路由路径是否存在等,这两部分功能共同完成了CoolRouter的操作,从而实现了让我们在使用过程中既可以返回Intent来让我们原生操作也可以直接用路由器提供的代码方便的进行跳转。
之后就是一个静态代理类,CoolRouterTool了, 代码非常简单,基本上就是对CoolRouter代码的调用,以及使用了建造者模式,可以链式调用我们CoolRouter的方法。
public class CoolRouterTool {
public static void openLog() {
CoolLog.openLog();
}
/**
* 添加路由过滤规则
*/
public static CoolRouter addPathFilter(String scheme, iPath iPath) {
CoolRouter coolRouter = CoolRouter.getIns();
coolRouter.addPathFilter(scheme, iPath);
return coolRouter;
}
public static <K> CoolRouter addPath(String keyPath, Class<K> tClass) {
CoolRouter coolRouter = CoolRouter.getIns();
coolRouter.addPath(keyPath, tClass);
return coolRouter;
}
public static <V> V dispatchPath(Context context, String keyPath) {
CoolRouter coolRouter = CoolRouter.getIns();
V v = coolRouter.dispatchPath(context, keyPath);
return v;
}
public static boolean pathIsExist(String keyPath){
return CoolRouter.getIns().routerIsExist(keyPath);
}
public static CoolRouter build(String keyPath){
return CoolRouter.getIns().build(keyPath);
}
}
还有一些属性类,提供基础属性的支持,比如:
CoolRouterOptions主要是提供CoolRouter的一些基础参数的设置及调用:
public class CoolRouterOptions {
private int enterAnim;
private int exitAnim;
private Bundle bundle;
private int requestCode;
public void setRequestCode(int requestCode) {
this.requestCode = requestCode;
}
public void setBundle(Bundle bundle) {
this.bundle = bundle;
}
public void setAnim(int enterAnim,int exitAnim) {
this.enterAnim = enterAnim;
this.exitAnim = exitAnim;
}
public int getEnterAnim() {
return enterAnim;
}
public void setEnterAnim(int enterAnim) {
this.enterAnim = enterAnim;
}
public int getExitAnim() {
return exitAnim;
}
public void setExitAnim(int exitAnim) {
this.exitAnim = exitAnim;
}
public Bundle getBundle() {
return bundle;
}
public int getRequestCode() {
return requestCode;
}
}
KeyPathConfig主要存放的是CoolRouter的跳转组件Scheme和路由器的自定义Log的开关:
public class KeyPathConfig {
public static final String ACTIVITY_SCHEME="activity://";
public static final String SERVICE_SCHEME="service://";
public static final String RECEIVER_SCHEME="receiver://";
public static final String HTTP_SCHEME="http://";
public static boolean DEBUG=true;
}
CoolLog就是路由器的自定义Log工具类了:
public class CoolLog {
private static final String TAG = "CoolRouter";
private static boolean loggable = false;
protected static void openLog() {
loggable = true;
}
public static void i(String msg) {
if (loggable) {
Log.i(TAG, msg);
}
}
public static void i(String tag, String msg) {
if (loggable) {
Log.i(tag, msg);
}
}
public static void w(String msg) {
if (loggable) {
Log.w(TAG, msg);
}
}
public static void w(String msg, Throwable tr) {
if (loggable) {
Log.w(TAG, msg, tr);
}
}
public static void e(String msg) {
if (loggable) {
Log.e(TAG, msg);
}
}
public static void e(String msg, Throwable tr) {
if (loggable) {
Log.e(TAG, msg, tr);
}
}
}
这里还有一个工具类,主要是为了验证Url路径的:
public class URLUtil {
/**
* 判断Web地址是否为正规的请求地址
* @param url 网络请求地址
* @return 返回判断结果
*/
public static boolean checkUrl(String url){
String regEx = "^(http|https|ftp)\\://([a-zA-Z0-9\\.\\-]+(\\:[a-zA-"
+ "Z0-9\\.&%\\$\\-]+)*@)?((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{"
+ "2}|[1-9]{1}[0-9]{1}|[1-9])\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}"
+ "[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|"
+ "[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\\.(25[0-5]|2[0-"
+ "4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])|([a-zA-Z0"
+ "-9\\-]+\\.)*[a-zA-Z0-9\\-]+\\.[a-zA-Z]{2,4})(\\:[0-9]+)?(/"
+ "[^/][a-zA-Z0-9\\.\\,\\?\\'\\\\/\\+&%\\$\\=~_\\-@]*)*$";
return url.matches(regEx);
}
}
差不多CoolRouter的类就是这些了,他的工程架构是这样的:
3.png项目的类图架构是这样的:
4.png好了,基本上CoolRouter的整体实现就是这样了,如果你认真阅读的话会发现我只是实现了Activity的跳转,而Service的启动和BroadcastReceiver的启动都没有实现,其实如果只是返回Intent就无所谓了但是如果想对方法进行封装的话,还是需要在CoolRouter和CoolRouterTool中添加一些方法的,这个就留给大家自己去做了。