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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,923評論 18 139
  • 今天老師在群里發了照片,是獲得的獎勵貼多的孩子的合影,沒有女兒。當我拿著手機給女兒看時,她滿不在乎的說,我知道啊,...
    仲思涵媽媽閱讀 186評論 0 0
  • 我不知道有多少人在意別人目光中的自己。在幾年以前,我也是一個在意別人看法的人,害怕自己有一點點超出平常的舉動就會引...
    阿淼閱讀 366評論 0 0
  • 姓名:劉小瓊 公司:寧波大發化纖有限公司 寧波盛和塾《六項精進》第235期學員 【日精進打卡第83天】 知~學習 ...
    劉小瓊123閱讀 173評論 0 0
  • 五月份的天空是蔚藍的,也是燥熱的,在這樣令人又驚又喜的天氣里,C姑娘的心思被掩藏的狠狠地寂寞空虛冷。不知道其中緣由...
    阿俊xi閱讀 396評論 0 1