ARouter解析六:拦截器

今天我们接着来拆下ARouter的拦截器,这个是ARouter路由框架的第六篇分享了。在Android系统自带的startActivity的方法调用后,我们是没有办法在跳转的中间过程插入一些其它处理的,ARouter中的拦截器就实现了这种功能,可以在跳转过程中添加自定义的功能,比如添加携带参数,判断是否需要登录等,是针对AOP切面编程思想的实现。

今天拦截器的分享会涉及到多线程,APT等技术,有一些反复的内容在前面的系列文章说过了,比如ARouter单例初始化过程,PostCard等内容,这里就不再赘述了。今天我们从下面几个方面来解析拦截器:

1.拦截器的注册

2.拦截器的初始化

3.拦截器的拦截过程源码分析

开始之前我们先看下官方Demo中拦截器的实现效果:

拦截器.png

点击拦截,会弹出拦截提示框点击加点料就会拦截,在拦截器中添加参数:

拦截.png
拦截添加跳转参数.png

好了,开始我们的拦截器之旅吧~~~

1.拦截器注册

拦截器使用和Activity@Route差不多,只不过拦截器是使用另外一个注解@Interceptor。有两个变量,priority是拦截器的优先级,值越小优先级越高,会优先拦截。name是拦截器的名称,开发也不怎么用。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Interceptor {
    /**
     * The priority of interceptor, ARouter will be excute them follow the priority.
     */
    int priority();

    /**
     * The name of interceptor, may be used to generate javadoc.
     */
    String name() default "Default";
}

拦截器都默认实现IInterceptor接口:

public interface IInterceptor extends IProvider {

    /**
     * The operation of this interceptor.
     *
     * @param postcard meta
     * @param callback cb
     */
    void process(Postcard postcard, InterceptorCallback callback);
}

在拦截器实现类使用注解@Interceptor会自动注册,就是在编译期间会自动生成映射关系类,使用到的就是APT技术,不了解的小伙伴可参考Android模块开发之APT技术.
这里因为是自动注册的,所以可以将不同功能的拦截器放在不同功能的模块中,只有模块被打包到整个项目中,因为自动注册机制所以拦截器就会生效,如果不将这些拦截器放到模块并打包到项目中,那就不会生效,这样就不用去做很多注册与反注册的工作,这也是ARouter适用于模块开发的原因之一。
看看自动生成的映射关系类是怎样,官方Demo提供了两个拦截器,Test1Interceptor拦截器在app模块中,TestInterceptor90拦截器在test-module-1中。下面文件路径ARouter/app/build/generated/source/apt/debug/com/alibaba/android/arouter/routes/ARouter$$Interceptors$$app.java

public class ARouter$$Interceptors$$app implements IInterceptorGroup {
  @Override
  public void loadInto(Map<Integer, Class<? extends IInterceptor>> interceptors) {
    interceptors.put(7, Test1Interceptor.class);
  }
}

下面文件路径:ARouter/test-module-1/build/generated/source/apt/release/com/alibaba/android/arouter/routes/ARouter$$Interceptors$$testmodule1.java

public class ARouter$$Interceptors$$testmodule1 implements IInterceptorGroup {
  @Override
  public void loadInto(Map<Integer, Class<? extends IInterceptor>> interceptors) {
    interceptors.put(90, TestInterceptor90.class);
  }
}

生成的映射关系类就实现一个功能,将拦截器实现类加载缓存。接下来我们看看拦截器在什么时候加载进缓存,也就是初始化。

2.拦截器初始化

因为拦截器是在每次跳转中间都需要判断的,所以在ARouter初始化的时候就会加载进来,初始化过程肯定需要和编译期间生成的映射关系类打交道,所以逻辑自然就放在LogisticsCenter中。我们看看初始化的源码,和拦截器无关的逻辑我做了删减,为了看起来方便点。

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {

        try {
            // These class was generate by arouter-compiler.
            List<String> classFileNames = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);

            for (String className : classFileNames) {
                if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
                    // Load interceptorMeta
                    ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
                } 
            }

            if (Warehouse.groupsIndex.size() == 0) {
                logger.error(TAG, "No mapping files were found, check your configuration please!");
            }

            if (ARouter.debuggable()) {
                logger.debug(TAG, String.format(Locale.getDefault(), "LogisticsCenter has already been loaded, GroupIndex[%d], InterceptorIndex[%d], ProviderIndex[%d]", Warehouse.groupsIndex.size(), Warehouse.interceptorsIndex.size(), Warehouse.providersIndex.size()));
            }
        } catch (Exception e) {
            throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
        }
}

可以看出来,拦截器映射关系会加载到仓库的mapWarehouse.interceptorsIndex 中,要分清楚这个时候只是代码缓存中知道了有框架中有哪些拦截器,但是还没初始化
我在这里下了个断点,看看有几个拦截器。
可以看出来有两个映射文件,这个和前面注册的分析是一样的。

拦截器个数.png

再看看加载到仓库中的是不是两个,可以看出来确实是两个,好厉害:)

仓库拦截器.png

到这里只是找到了拦截器,还没有进行初始化,我们接着看源码,在_ARouter.init就是主要工作就是调用LogisticsCenter进行初始化,这里就是上面的分析工程,然后会调用_ARouter.afterInit()

public static void init(Application application) {
        if (!hasInit) {
            logger = _ARouter.logger;
            _ARouter.logger.info(Consts.TAG, "ARouter init start.");
            hasInit = _ARouter.init(application);

            if (hasInit) {
                _ARouter.afterInit();
            }

            _ARouter.logger.info(Consts.TAG, "ARouter init over.");
        }
}

我们接着跟到_ARouter.afterInit(),获取interceptorService

static void afterInit() {
        // Trigger interceptor init, use byName.
        interceptorService = (InterceptorService) ARouter.getInstance().build("/arouter/service/interceptor").navigation();
    }

在获取interceptorService过程中会在LogisticsCenter中实例化interceptorService实现类InterceptorServiceImpl后调用init(Context)方法。

switch (routeMeta.getType()) {
                case PROVIDER:  
                    Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
                    IProvider instance = Warehouse.providers.get(providerMeta);
                    if (null == instance) { // There's no instance of this provider
                        IProvider provider;
                        try {
                            provider = providerMeta.getConstructor().newInstance();
                            provider.init(mContext);
                            Warehouse.providers.put(providerMeta, provider);
                            instance = provider;
                        } catch (Exception e) {
                            throw new HandlerException("Init provider failed! " + e.getMessage());
                        }
                    }
                    postcard.setProvider(instance);
                    postcard.greenChannel();    // Provider should skip all of interceptors
                    break;
                case FRAGMENT:
                    postcard.greenChannel();    // Fragment needn't interceptors
                default:
                    break;
}

我们接着跟到init方法中,可以看出,就是从仓库Warehouse.interceptorsIndex中取出拦截器然后进行实例化,接着将实例对象添加到Warehouse.interceptors 中。

@Override
public void init(final Context context) {
     LogisticsCenter.executor.execute(new Runnable() {
            @Override
            public void run() {
                if (MapUtils.isNotEmpty(Warehouse.interceptorsIndex)) {
                    for (Map.Entry<Integer, Class<? extends IInterceptor>> entry : Warehouse.interceptorsIndex.entrySet()) {
                        Class<? extends IInterceptor> interceptorClass = entry.getValue();
                        try {
                            IInterceptor iInterceptor = interceptorClass.getConstructor().newInstance();
                            iInterceptor.init(context);
                            Warehouse.interceptors.add(iInterceptor);
                        } catch (Exception ex) {
                            throw new HandlerException(TAG + "ARouter init interceptor error! name = [" + interceptorClass.getName() + "], reason = [" + ex.getMessage() + "]");
                        }
                    }

                    interceptorHasInit = true;

                    logger.info(TAG, "ARouter interceptors init over.");

                    synchronized (interceptorInitLock) {
                        interceptorInitLock.notifyAll();
                    }
                }
            }
        });
}

到这里拦截器的初始化过程就比较清楚了,有一点需要知道,就是拦截器怎么根据优先级进行拦截?原理就在仓库的两个缓存map中,一个就是存储拦截器类的Warehouse.interceptorsIndex,另外一个就是存储拦截器实例的Warehouse.interceptors.我们看下这两个类型:

// Cache interceptor
static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("More than one interceptors use same priority [%s]");
static List<IInterceptor> interceptors = new ArrayList<>();

其中interceptorsIndex的类型是TreeMap,原理是红黑树,可以实现按顺序存储,所以我们拦截器就可以按照优先级存储在这个缓存中。实例化的时候也是按照优先级取出,然后实例化存储到ArrayList中,优先级值越小就在队列中越靠前,这就是拦截器优先级的原理。细节处见功夫啊!

接下来就看看拦截器是怎么拦截的~~~

3.拦截器的拦截过程源码分析

拦截是在跳转之前拦截,所以我们到_ARouter中的navigation去看,如果需要拦截就通过interceptorService进行拦截,不需要拦截就直接跳转。

if (!postcard.isGreenChannel()) {   // It must be run in async thread, maybe interceptor cost too mush time made ANR.
      interceptorService.doInterceptions(postcard, new InterceptorCallback() {
                /**
                 * Continue process
                 *
                 * @param postcard route meta
                 */
                @Override
                public void onContinue(Postcard postcard) {
                    _navigation(context, postcard, requestCode, callback);
                }

                /**
                 * Interrupt process, pipeline will be destory when this method called.
                 *
                 * @param exception Reson of interrupt.
                 */
                @Override
                public void onInterrupt(Throwable exception) {
                    if (null != callback) {
                        callback.onInterrupt(postcard);
                    }

                    logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.getMessage());
                }
            });
        } else {
            return _navigation(context, postcard, requestCode, callback);
}

interceptorServiceInterceptorServiceImpl的实例对象.

@Route(path = "/arouter/service/interceptor")
public class InterceptorServiceImpl implements InterceptorService

我们接着看拦截方法:

1.首先看下Warehouse.interceptors是否有拦截器,如果没有就直接调用回调接口跳转;有拦截器就进行后面步骤。

public void doInterceptions(final Postcard postcard, final InterceptorCallback callback) {
        //1.是否有拦截器
        if (null != Warehouse.interceptors && Warehouse.interceptors.size() > 0) {

            //2.检查拦截器初始化情况
            checkInterceptorsInitStatus();

            if (!interceptorHasInit) {//未初始化就抛异常
                callback.onInterrupt(new HandlerException("Interceptors initialization takes too much time."));
                return;
            }

            //3.拦截
            LogisticsCenter.executor.execute(new Runnable() {
                @Override
                public void run() {
                    CancelableCountDownLatch interceptorCounter = new CancelableCountDownLatch(Warehouse.interceptors.size());
                    try {
                        _excute(0, interceptorCounter, postcard);
                        interceptorCounter.await(postcard.getTimeout(), TimeUnit.SECONDS);
                        if (interceptorCounter.getCount() > 0) {    // Cancel the navigation this time, if it hasn't return anythings.
                            callback.onInterrupt(new HandlerException("The interceptor processing timed out."));
                        } else if (null != postcard.getTag()) {    // Maybe some exception in the tag.
                            callback.onInterrupt(new HandlerException(postcard.getTag().toString()));
                        } else {
                            callback.onContinue(postcard);
                        }
                    } catch (Exception e) {
                        callback.onInterrupt(e);
                    }
                }
            });
        } else {
            callback.onContinue(postcard);
        }
    }

2.首先检查拦截器的初始化情况checkInterceptorsInitStatus,这是个同步方法,拦截器未初始化就会等待一定时间,超时就抛出错误。在前面init 方法是在线程池中异步执行的,中如果初始化成功就会释放锁interceptorInitLock。为什么要异步?因为拦截器可能数目比较多或者初始化比较耗时。

synchronized (interceptorInitLock) {
        interceptorInitLock.notifyAll();
}

接着看看checkInterceptorsInitStatus方法,当拦截器还没初始化完成,这里就在锁上同步等待时间10 * 1000,超时还未获取到锁就会报错。

private static void checkInterceptorsInitStatus() {
        synchronized (interceptorInitLock) {
            while (!interceptorHasInit) {
                try {
                    interceptorInitLock.wait(10 * 1000);
                } catch (InterruptedException e) {
                    throw new HandlerException(TAG + "Interceptor init cost too much time error! reason = [" + e.getMessage() + "]");
                }
            }
        }
    }

拦截器都初始化成功后,来到第三步拦截实现,拦截也是在线程池中异步执行,同上面init的原因一样。

3.拦截过程就在_excute方法中实现,这个我们在第四步中再分析。拦截器不可能无限时长拦截吧,那么怎么实现?就是通过Java提供的异步工具CountDownLatch.在等待interceptorCounter.await(postcard.getTimeout(), TimeUnit.SECONDS);后进行判断interceptorCounter.getCount是否等于0或者postcard.getTag是否是空,如果满足就是拦截出现问题;否则就继续放行。

接着看看拦截的真正实现逻辑:

3.拦截真正过程在_execute中, 逻辑比较简单,就是依次取出拦截器实例,然后调用process方法,传入回调接口InterceptorCallback,当前拦截器处理完毕回调onContinue,在这里面递减counter.countDown,然后取出下一个拦截器循环上面过程。

private static void _excute(final int index, final CancelableCountDownLatch counter, final Postcard postcard) {
        if (index < Warehouse.interceptors.size()) {
            IInterceptor iInterceptor = Warehouse.interceptors.get(index);
            iInterceptor.process(postcard, new InterceptorCallback() {
                @Override
                public void onContinue(Postcard postcard) {
                    // Last interceptor excute over with no exception.
                    counter.countDown();
                    _excute(index + 1, counter, postcard);  // When counter is down, it will be execute continue ,but index bigger than interceptors size, then U know.
                }
                @Override
                public void onInterrupt(Throwable exception) {
                    // Last interceptor excute over with fatal exception.
                    postcard.setTag(null == exception ? new HandlerException("No message.") : exception.getMessage());    // save the exception message for backup.
                    counter.cancel();
                }
            });
        }
}

拦截器的实现流程就是以上。接着我们挑一个拦截器Test1Interceptor的实现看看。这个就是上面Demo演示的弹框拦截器,如果选择选择“加点料”,就会添加参数,然后调用回调接口的callback.onContinue,继续到下一个拦截器。

@Override
public void process(final Postcard postcard, final InterceptorCallback callback) {
        if ("/test/activity4".equals(postcard.getPath())) {
            final AlertDialog.Builder ab = new AlertDialog.Builder(MainActivity.getThis());
            ab.setCancelable(false);
            ab.setTitle("温馨提醒");
            ab.setMessage("想要跳转到Test4Activity么?(触发了\"/inter/test1\"拦截器,拦截了本次跳转)");
            ab.setNegativeButton("继续", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    callback.onContinue(postcard);
                }
            });
            ab.setNeutralButton("算了", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    callback.onInterrupt(null);
                }
            });
            ab.setPositiveButton("加点料", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    postcard.withString("extra", "我是在拦截器中附加的参数");
                    callback.onContinue(postcard);
                }
            });

            MainLooper.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    ab.create().show();
                }
            });
        } else {
            callback.onContinue(postcard);
        }
}

4.总结

今天的拦截器内容会比较多一点,拦截器的注册和其它的activity或者service是差不多的,主要是注解不同。拦截器的初始化过程是在线程池中进行,为了是拦截器可能耗时的问题。拦截器的拦截过程其实就是在线程池中从仓库里依次取出拦截器实例进行拦截,稍微难一点的地方就是在多线程的同步问题上,用的还是Java的多线程技术。

有需要的小伙伴可以看下前面的系列分享:
ARouter解析一:基本使用及页面注册源码解析
ARouter解析二:页面跳转源码分析
ARouter解析三:URL跳转本地页面源码分析
ARouter解析四:发现服务和Fragment
ARouter解析五:IoC与依赖注入

今天的拦截器的内容就是这样,希望对大家有点帮助,谢谢!

欢迎关注公众号:JueCode

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • 今天老师在群里发了照片,是获得的奖励贴多的孩子的合影,没有女儿。当我拿着手机给女儿看时,她满不在乎的说,我知道啊,...
    仲思涵妈妈阅读 173评论 0 0
  • 我不知道有多少人在意别人目光中的自己。在几年以前,我也是一个在意别人看法的人,害怕自己有一点点超出平常的举动就会引...
    阿淼阅读 340评论 0 0
  • 姓名:刘小琼 公司:宁波大发化纤有限公司 宁波盛和塾《六项精进》第235期学员 【日精进打卡第83天】 知~学习 ...
    刘小琼123阅读 165评论 0 0
  • 五月份的天空是蔚蓝的,也是燥热的,在这样令人又惊又喜的天气里,C姑娘的心思被掩藏的狠狠地寂寞空虚冷。不知道其中缘由...
    阿俊xi阅读 347评论 0 1