为了账号安全,请及时绑定邮箱和手机立即绑定

Android 路由设计最佳实践

原文链接

引子

这篇文章会告诉你

  • 什么是路由,是为了解决什么问题才产生的

  • 业界现状是怎么样的,我们可以做什么来优化当前的问题

  • 路由设计思路是怎么样的,该怎么设计比较好

  • 如何用注解实现路由表

  • URL的参数如何依赖注入到Activity、Fragement

  • 如何HookOnActivityResult,不需要再进行requstCode判断

  • 如何异步拦截路由,实现线程切换,不阻塞页面跳转

  • 如何用Apt实现Retrofit接口式调用

  • 如何找到Activity的调用方

  • 如何实现路由的安全调用

  • 如何避开Apt不能汇总所有Module路由的问题

前言

当前Android的路由库实在太多了,刚开始的时候想为什么要用路由表的库,用Android原生的Scheme码不就好了,又不像iOS只能类依赖,后面越深入就越发现当时想的太简单了,后面看到Retrofit和OKHttp,才想到页面请求本质和网络请求不是一样吗,终于业界最简单高效的路由方案1.0出来了

OkDeepLink

背景

什么是路由

根据路由表页面请求分发到指定页面

使用场景

  1. App接收到一个通知,点击通知打开App的某个页面

  2. 浏览器App中点击某个链接打开App的某个页面

  3. 运营活动需求,动态把原生的页面替换成H5页面

  4. 打开页面需要某些条件,先验证完条件,再去打开那个页面

  5. 不合法的打开App的页面被屏蔽掉

  6. H5打开链接在所有平台都一样,方便统一跳转

  7. App存在就打开页面,不存在就去下载页面下载,只有Google的App Link支持

为什么要有路由

Android原生已经支持AndroidManifest去管理App跳转,为什么要有路由库,这可能是大部分人接触到Android各种Router库不太明白的地方,这里我讲一下我的理解

  • 显示Intent:项目庞大以后,类依赖耦合太大,不适合组件化拆分

  • 隐式Intent:协作困难,调用时候不知道调什么参数

  • 每个注册了Scheme的Activity都可以直接打开,有安全风险

  • AndroidMainfest集中式管理比较臃肿

  • 无法动态修改路由,如果页面出错,无法动态降级

  • 无法动态拦截跳转,譬如未登录的情况下,打开登录页面,登录成功后接着打开刚才想打开的页面

  • H5、Android、iOS地址不一样,不利于统一跳转

怎么样的路由才算好路由

路由说到底还是为了解决开发者遇到的各种奇葩需求,使用简单、侵入性低、维护方便是首要条件,不影响你原来的代码,写入代码也很少,这里就要说说我的OkDeepLink的五大功能了,五大功能瞬间击中你的各种痛点,早点下班不是梦。

  • 编译时注解,实现静态路由表,不再需要在臃肿的AndroidManifest中找到那个Actvity写Scheme和Intent Filter

  • 异步拦截器,实现动态路由,安全拦截、动态降级难不倒你

  • 模仿Retrofit接口式调用,实现方式用apt,不耗性能,参数调用不再是问题

  • HookOnActivityResult,支持RxJava响应式调用,不再需要进行requestCode判断

  • 参数依赖注入,自动保存,不再需要手动写onSaveInstanceonCreate(SaveInstace)onNewIntent(Intent)getQueryParamer

注册路由

具体使用见OkDeepLink

路由结构图

详细比较

大部分路由库都用Apt(编译时注解)生成路由表,然后用路由表转发到指定页面

方案对比OkDeepLinkAirbnb DeepLinkDispatch阿里 ARouter天猫 统跳协议ActivityRouter
路由注册注解式接口注册每个module都要手动注册每个module的路由表都要类查找AndroidManiFest配置每个module都要手动注册
路由查找路由表路由表路由表系统Intent路由表
路由分发Activity转发Activity转发Activity转发Activity转发Activity转发
动态替换Rxjava实现异步拦截器不支持线程等待不支持不支持
动态拦截Rxjava实现异步拦截器不支持线程等待不支持主线程
安全拦截Rxjava实现异步拦截器不支持线程等待不支持主线程
方法调用接口手动拼装手动拼装手动拼装手动拼装
参数获取Apt依赖注入,支持所有类型,不需要在Activity的onCreate中手动调用get方法参数定义在path,不利于多人协作Apt依赖注入,但是要手动调用get方法手动调用手动调用
结果返回Rxjava回调onActivityResultonActivityResultonActivityResultonActivityResult
Module接入不同App支持不支持支持不支持支持

其实说到底,路由的本质就是注册再转发,围绕着转发可以进行各种操作,拦截,替换,参数获取等等,其他Apt、Rxjava说到底都只是为了方便使用出现的,这里你会发现各种路由库反而为了修复各种工具带来的问题,出现了原来没有的问题,譬如DeepLinkDispatch为了解决Apt没法汇总所有Module路由,每个module都要手动注册,ARouter为了解决Apt没法汇总所有Module路由,通过类操作耗时,才出现分组的概念。

原理分析

原理流程图

定义路由

路由定义

我这边是完全按照URL规范了,这里要说一下,现在好多方法是把参数定义在path里面的,虽然这样做,有不需要额外传参数的好处,但是这样路由就没有那么灵活,调试起来就没有那么方便了。
建议有好几款app的公司,host都一样,只有scheme不一样,这样只要替换Scheme就能实现降级,维护也简单。

路由注册

AndroidManifest里面的acitivity声明scheme码是不安全的,所有App都可以打开这个页面,这里就产生有两种方式去注册,

  • 注解产生路由表,通过DispatchActivity转发

  • AndroidManifest注册,将其export=fasle,但是再通过DispatchActivity转发Intent,天猫就是这么做的,比上面的方法的好处是路由查找都是系统调用,省掉了维护路由表的过程,但是AndroidManifest配置还是比较不方便的

我现在还是采用了注解,后面我会结合两种方法,将注解自动修改AndroidManifest,对于接入方是没有变动的,方法已经找到了,用自定义Lint扫描出注解相关的Activity,然后用processManifestTask修改Manifest,有个demo了,后面会接入。

生成路由表

譬如通过Apt把这段代码

public interface SampleService {    @Path("/main")    @Activity(MainActivity.class)    void startMainActivity(@Query("key") String key);
}

生成

@After("execution(* okdeeplink.DeepLinkClient.init(..))")  public void init() {
    DeepLinkClient.addAddress(new Address("/main", MainActivity.class));
  }

初始化路由表

这里就要提一下使用Apt会造成每个module都要手动注册

DeepLinkDispatch是这么做的

@DeepLinkModulepublic class SampleModule {
}
@DeepLinkHandler({ SampleModule.class, LibraryDeepLinkModule.class })public class DeepLinkActivity extends Activity {  @Override protected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);
    DeepLinkDelegate deepLinkDelegate = new DeepLinkDelegate(        new SampleModuleLoader(), new LibraryDeepLinkModuleLoader());
    deepLinkDelegate.dispatchFrom(this);
    finish();
  }
}

ARouter是通过类查找,就比较耗时了,所以他又加入了分组的概念,按需加载

/**
     * 通过指定包名,扫描包下面包含的所有的ClassName
     *
     * @param context     U know
     * @param packageName 包名
     * @return 所有class的集合
     */
    public static List<String> getFileNameByPackageName(Context context, String packageName) throws PackageManager.NameNotFoundException, IOException {        List<String> classNames = new ArrayList<>();        for (String path : getSourcePaths(context)) {
            DexFile dexfile = null;            try {                if (path.endsWith(EXTRACTED_SUFFIX)) {                    //NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
                    dexfile = DexFile.loadDex(path, path + ".tmp", 0);
                } else {
                    dexfile = new DexFile(path);
                }
                Enumeration<String> dexEntries = dexfile.entries();                while (dexEntries.hasMoreElements()) {
                    String className = dexEntries.nextElement();                    if (className.contains(packageName)) {
                        classNames.add(className);
                    }
                }
            } catch (Throwable ignore) {
                Log.e("ARouter", "Scan map file in dex files made error.", ignore);
            } finally {                if (null != dexfile) {                    try {
                        dexfile.close();
                    } catch (Throwable ignore) {
                    }
                }
            }
        }

        Log.d("ARouter", "Filter " + classNames.size() + " classes by packageName <" + packageName + ">");        return classNames;
    }

ActivityRouter就比较巧妙了,通过Stub项目,其他地方都是provide的,只有主工程里面用Apt生成RouterInit类,虽然还是要写module的注解

        // RouterInit
        if (hasModules) {
            debug("generate modules RouterInit");
            generateModulesRouterInit(moduleNames);
        } else if (!hasModule) {
            debug("generate default RouterInit");
            generateDefaultRouterInit();
        }

天猫 统跳协议 是最简单的,转发一下Intent就可以,但是这样就没法享受注解的好处了。

而我用aspectj解决了这个问题,会自动汇总所有module的路由省略了这些多余的代码,或者有谁知道用Apt自生怎么解决,请联系我一下。

@After("execution(* okdeeplink.DeepLinkClient.init(..))")  public void init() {
    DeepLinkClient.addAddress(new Address("/main", MainActivity.class));
  }

路由查找

路由查找就是查找路由表对应的页面,值得提起的就是因为要适应Module接入不同App,Scheme要自动适应,路由表其实是Path—》Activity,这样的话内部跳转的时候ARouterUri是没有的。而我这边是有的,我组装了一个内部的Uri,这样拦截器不会有影响。

public Request buildRequest(Intent sourceIntent) {        if (sourceIntent == null) {            return null;
        }
        Intent newIntent = new Intent(sourceIntent);
        Uri uri = newIntent.getData();

        addNewTaskFlag(newIntent);        if (uri != null) {
            addBundleQuery(newIntent, uri);

            Address entry = new DeepLinkClient(context).matchUrl(uri.toString());            if (entry == null || entry.getActivityClass() == null) {                return new Request(newIntent, this).setDeepLink(false);
            }
            newIntent.setComponent(new ComponentName(context, entry.getActivityClass()));            return new Request(newIntent, this);
        }        return new Request(newIntent, this).setDeepLink(false);

    }

路由分发

现在所有路由方案分发都是用Activity做分发的,这样做会有这几个缺点

  1. 每次都要启动一个Activity,而Activity就算不写任何代码启动都要0.1秒

  2. 如果是异步等待的话,Activiy要在合适时间finish,不然会有一层透明的页面阻挡操作

对于第一个问题,有两个方法

  1. QQ音乐是把DispatchActivity设为SingleInstacne,但是这样的话,动画会奇怪,堆栈也会乱掉,后退会有一层透明的页面阻挡操作

  2. DispatchActivity只在外部打开的时候调用

我选择了第二种

对于第二个问题,有两个方法

  1. DispatchActivity再把Intent转发到Service,再finish,这种方法唯一的缺陷是拦截器里面的context是Servcie的activity,就没发再拦截器里面弹出对话框了。

  2. DispatchActivity在打开和错误的时候finish,如果activity已经finish了,就用application的context去转发路由

我选择了第二种

  public void dispatchFrom(Intent intent) {        new DeepLinkClient(this)
                .buildRequest(intent)
                .dispatch()
                .subscribe(new Subscriber<Request>() {                    @Override
                    public void onCompleted() {
                        finish();
                    }                    @Override
                    public void onError(Throwable e) {
                        finish();
                    }                    @Override
                    public void onNext(Request request) {
                        Intent dispatchIntent = request.getIntent();
                        startActivity(dispatchIntent);
                    }
                });
    }

其实处理透明Activity阻挡操作可以采用取消所有事件变成无感页面的方法,但是还是觉得会影响activity堆栈没有采用这种方案

getwindow().addflags( windowmanager.layoutparams.flag_not_focusable| windowmanager.layoutparams.flag_not_touch_modal 
| windowmanager.layoutparams.flag_not_touchable);

结果返回

这里我封装了一个库RxActivityResult去捕获onActivityResult,这样能保正流式调用

譬如拍照可以这样写,先定义一个接口

    public interface ImageCaptureService {    @Action(MediaStore.ACTION_IMAGE_CAPTURE)    Observable<Response> startImageCapture();
}

然后这样调用

public class MainActivity extends AppCompatActivity {    @Service
    ImageCaptureService imageCaptureService;    public void captureImage(){
        imageCaptureService
                .startImageCapture()
                .subscribe(new Action1<Response>() {                    @Override
                    public void call(Response response) {
                        Intent data = response.getData();                        int resultCode = response.getResultCode();                        if (resultCode == RESULT_OK) {
                            Bitmap imageBitmap = (Bitmap) data.getExtras().get("data");
                        }
                    }
                });
    }
}
}

是不是很简单,原理是这样的,通过封装一个RxResultHoldFragment去处理onActivityResult

private IActivityObservable buildActivityObservable() {

            T target = targetWeak.get();            if (target instanceof FragmentActivity) {
                FragmentActivity activity = (FragmentActivity) target;
                android.support.v4.app.FragmentManager fragmentManager = activity.getSupportFragmentManager();
                IActivityObservable activityObservable = RxResultHoldFragmentV4.getHoldFragment(fragmentManager);                return activityObservable;
            }            if (target instanceof Activity) {
                Activity activity = (Activity) target;
                FragmentManager fragmentManager = activity.getFragmentManager();
                IActivityObservable activityObservable = RxResultHoldFragment.getHoldFragment(fragmentManager);                return activityObservable;
            }            if (target instanceof Context) {                final Context context = (Context) target;
                IActivityObservable activityObservable = new RxResultHoldContext(context);                return activityObservable;
            }            if (target instanceof Fragment) {
                Fragment fragment = (Fragment) target;
                FragmentManager fragmentManager = fragment.getFragmentManager();                if (fragmentManager != null) {
                    IActivityObservable activityObservable = RxResultHoldFragment.getHoldFragment(fragmentManager);                    return activityObservable;
                }
            }            if (target instanceof android.support.v4.app.Fragment) {
                android.support.v4.app.Fragment fragment = (android.support.v4.app.Fragment) target;
                android.support.v4.app.FragmentManager fragmentManager = fragment.getFragmentManager();                if (fragmentManager != null) {
                    IActivityObservable activityObservable = RxResultHoldFragmentV4.getHoldFragment(fragmentManager);                    return activityObservable;
                }
            }            return new RxResultHoldEmpty();
        }

动态拦截

拦截器是重中之重,有了拦截器可以做好多事情,可以说之所以要做页面路由,就是为了要实现拦截器。ARouter是用线程等待实现的,但是现在有Rxjava了,可以实现更优美的方式。
先来看一下我做的拦截器的效果.

@Intercept(path = "/second")public class SecondInterceptor extends Interceptor {    @Override
    public void intercept(final Call call) {

        Request request = call.getRequest();        final Intent intent = request.getIntent();
        Context context = request.getContext();

        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("Intercept\n");
        stringBuffer.append("URL: " + request.getUrl() + "\n");

        AlertDialog.Builder builder = new AlertDialog.Builder(context,R.style.Theme_AppCompat_Dialog_Alert);
        builder.setTitle("Notice");
        builder.setMessage(stringBuffer);
        builder.setNegativeButton("cancel", new DialogInterface.OnClickListener() {            @Override
            public void onClick(DialogInterface dialog, int which) {
                call.cancel();
            }
        });
        builder.setPositiveButton("ok", new DialogInterface.OnClickListener() {            @Override
            public void onClick(DialogInterface dialog, int which) {
                intent.putExtra("key1", "value3");
                call.proceed();
            }
        });
        builder.setOnDismissListener(new DialogInterface.OnDismissListener() {            @Override
            public void onDismiss(DialogInterface dialog) {
                call.cancel();
            }
        });
        builder.show();
    }
}

是不是很简单,参考了部分OkHttp的实现思路,加入Rxjava,实现异步拦截。

首先将请求转换成责任链模式RealCallChain,RealCallChain的call方法实际不会执行路由跳转,只有Interceptor里面调用了call.proceed或者call.cancel才会执行.

    private Observable<Request> buildRequest() {
        RealCallChain chain = new RealCallChain(interceptors, 0, request);
        chain.setTimeout(interceptTimeOut);
        chain.call();        return chain
                .getRequestObservable()
                .map(new Func1<Request, Request>() {                    @Override
                    public Request call(Request request) {                        if (interceptors != null) {                            for (Interceptor interceptor : interceptors) {
                                interceptor.onCall(request);
                            }
                        }                        return request;
                    }
                });
    }

接着处理异步的问题,这里用到了Rxjava的AsyncSubject和BehaviorSubject,

  1. AsyncSubject具有仅释放Observable释放的最后一个数据的特性,作为路由请求的发送器

  2. BehaviorSubject具有一开始就会释放最近释放的数据的特性,作为路由拦截器的发送器

具体实现看核心代码

    @Override
    public void proceed() {        if (index >= interceptors.size()) {
            realCall();            return;
        }        final Interceptor interceptor = interceptors.get(index);
        Observable
                .just(1)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<Integer>() {                    @Override
                    public void call(Integer integer) {
                        interceptor.intercept(RealCallChain.this);
                    }
                });

        interceptorSubject.onNext(interceptor);
        index = index + 1;
    }

方法调用

大部分路由库都是手动拼参数调用路由的,这里模仿了Retrofit接口式调用,受了LiteRouter的启发,不过Retrofit使用了动态代理,我使用的Apt没有性能损耗。

通过Apt生成每个接口的实际方法

譬如把SecondService接口

public interface SecondService {    @Path("/second")    @Activity(SecondActivity.class)    void startSecondActivity();
}

生成

@Aspectpublic final class SecondService$$Provider implements SecondService {  public DeepLinkClient deepLinkClient;  public SecondService$$Provider(DeepLinkClient deepLinkClient) {    this.deepLinkClient= deepLinkClient;
  }  @Override
  public void startSecondActivity() {
    Intent intent = new Intent();
    intent.setData(Uri.parse("app://deeplink/second"));
    Request request = deepLinkClient.buildRequest(intent);    if (request != null) {
      request.start();
    }
  }  @Around("execution(* okdeeplink.DeepLinkClient.build(..))")  public Object aroundBuildMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    DeepLinkClient target = (DeepLinkClient)joinPoint.getTarget();    if (joinPoint.getArgs() == null || joinPoint.getArgs().length != 1) {      return joinPoint.proceed();
    }
    Object arg = joinPoint.getArgs()[0];    if (arg instanceof Class) {
      Class buildClass = (Class) arg;      if (buildClass.isAssignableFrom(getClass())) {        return new SecondService$$Provider(target);
      }
    }    return joinPoint.proceed();
  }
}

然后调用

SecondService secondServicenew = DeepLinkClient(target).build(SecondService.class);

SecondService就生成了。
为了调用方便,直接在Activity或者fragement写这段代码,sampleServive就自动生成了

  @Service
  SampleService sampleService;

但是如果用到MVP模式,不是在Activity里面调用路由,后面会支持在这些类里面自动注入SampleService,现在先用java代码build

参数获取

大部分路由库都是手动获取参数的,这样还要传入参数key比较麻烦,这里模仿了ARouter,不过我支持类型更全一些,支持Bundle支持的所有类型,而且不需要在Acitivty的onCreate调用获取代码。
通过Apt把这段代码

public class MainActivity extends AppCompatActivity {    @Query("key")
    String key;
}

生成

@Aspectpublic class MainActivity$$Injector {  @Around("execution(* okdeeplink.sample.MainActivity.onCreate(..))")  public void onCreate(ProceedingJoinPoint joinPoint) throws Throwable {
    MainActivity target = (MainActivity)joinPoint.getTarget();
    Bundle dataBundle = new Bundle();
    Bundle saveBundle = (Bundle)joinPoint.getArgs()[0];
    Bundle targetBundle = BundleCompact.getSupportBundle(target);    if(targetBundle != null) {
      dataBundle.putAll(targetBundle);
    }    if(saveBundle != null) {
      dataBundle.putAll(saveBundle);
    }    try {
      target.key= BundleCompact.getValue(dataBundle,"key",String.class);
    } catch (Exception e) {
      e.printStackTrace();
    }
    joinPoint.proceed();
  }  @After("execution(* okdeeplink.sample.MainActivity.onSaveInstanceState(..))")  public void onSaveInstanceState(JoinPoint joinPoint) throws Throwable {
    MainActivity target = (MainActivity)joinPoint.getTarget();
    Bundle saveBundle = (Bundle)joinPoint.getArgs()[0];
    Intent intent = new Intent();
    intent.putExtra("key",target.key);
    saveBundle.putAll(intent.getExtras());
  }  @Around("execution(* okdeeplink.sample.MainActivity.onNewIntent(..))")  public void onNewIntent(ProceedingJoinPoint joinPoint) throws Throwable {
    MainActivity target = (MainActivity)joinPoint.getTarget();
    Intent targetIntent = (Intent)joinPoint.getArgs()[0];
    Bundle dataBundle = targetIntent.getExtras();    try {
      target.key= BundleCompact.getValue(dataBundle,"key",String.class);
    } catch (Exception e) {
      e.printStackTrace();
    }
    joinPoint.proceed();
  }
}

Module接入不同App

这里是参考ARouter把path作为key对应activity,这样接入到其他app中,就自动替换了scheme码

DeepLinkClient.addAddress(new Address("/main", MainActivity.class));

安全

现在有好多人用脚本来打开App,然后干坏事,其实时可以用路由来屏蔽掉.

有三种方法供君选择,不同方法适合不同场景

签名屏蔽

就是把所有参数加密成一个数据作为sign参数,然后比对校验,但是这要求加密方法不变,要不然升级了以前的app就打不开了

adb打开屏蔽

在android5.1手机上,用adb打开的app它的mReferrer为空

 public boolean isStartByAdb(android.app.Activity activity){        if (Build.VERSION.SDK_INT >= 22) {
            android.net.Uri uri = ActivityCompat.getReferrer(activity);            return uri == null | TextUtils.isEmpty(uri.toString()) ;
        }        return false;
    }

包名过滤

在Android 4.4手机上, 写了android:ssp的组件,只有特定应用可以打开

<activity
            android:name="okdeeplink.DeepLinkActivity"
            android:noHistory="true"
            android:theme="@android:style/Theme.Translucent.NoTitleBar">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data
                    android:ssp="com.app.test"
                    android:host="app"
                    android:scheme="odl" />
            </intent-filter>
        </activity>

这三种方法,比较适合的还是签名校验为主,adb过滤为副

如何解决路由造成的Activity堆栈错乱的问题

activity的launchMode使用不当会照成闪屏页面打开多次的问题,可以参考我这篇文章

未来展望

路由是一个基础模块,技术难度虽然不是很大,但是如果每个开发都重新踩一遍,性价比就比较低,我希望能把路由相关的所有链路都替你弄好,你可以留着时间去干其他更重要的事情,譬如陪陪家人,逗逗狗什么的。
接下来我会在这几个方面努力,把整条链路补全。

  • 做一个像Swagger的平台,支持一键导出所有路由、二维码打开路由

  • 注解修改AndroidManifest,不再需要路由表

  • 支持路由方法接收器,Url直接打开某个方法,不再局限Activity

如果大家有意见,欢迎联系我kingofzqj@gmail.com


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消