Android路由方案ARouter分析

Ruheng發表於2018-07-01

一、路由方案

原生的路由方案缺點:

  • 顯式:直接的類依賴,耦合嚴重

  • 隱式:規則集中式管理,協作困難

  • Manifest擴充套件性較差

  • 跳轉過程無法控制

  • 失敗無法降級

ARouter的優勢:

  • 使用註解,實現了對映關係自動註冊 與 分散式路由管理

  • 編譯期間處理註解,並生成對映檔案,沒有使用反射,不影響執行時效能

  • 對映關係按組分類、多級管理,按需初始化

  • 靈活的降級策略,每次跳轉都會回撥跳轉結果,避免StartActivity()一旦失敗將會丟擲運營級異常

  • 自定義攔截器,自定義攔截順序,可以對路由進行攔截,比如登入判斷和埋點處理

  • 支援依賴注入,可單獨作為依賴注入框架使用,從而實現 跨模組API呼叫

  • 支援直接解析標準URL進行跳轉,並自動注入引數到目標頁面中

  • 支援獲取Fragment

  • 支援多模組使用,支援元件化開發

…….

這麼多好處,是時候來了解一下ARouter了。

二、ARouter框架

Android路由方案ARouter分析

上圖是根據ARouter一次基本的路由導航過程,整理的基本框架圖,涉及到主要流程,下面進行詳細介紹。

三、路由管理

1.註冊

通過註解,在編譯時收集使用了註解的類或變數並經過Android Process Tool處理進行統一管理。

包含三種註解@Autowired,@Interceptor,@Route。

@Route

註解定義

String path();//路徑URL字串
String group() default "";//組名,預設為一級路徑名;一旦被設定,跳轉時必須賦值
String name() default "undefined";//該路徑的名稱,用於產生JavaDoc
int extras() default Integer.MIN_VALUE;//額外配置的開關資訊;譬如某些頁面是否需要網路校驗、登入校驗等
int priority() default -1;//該路徑的優先順序
複製程式碼

實現 @Route 註解

BlankFragment               @Route(path = "/test/fragment") 
Test1Activity               @Route(path = "/test/activity1")
複製程式碼

該註解主要用於描述路由中的路徑URL資訊,使用該註解標註的類將被自動新增至路由表中。

@Autowired

註解定義

boolean required() default false;
String desc() default "No desc.";
複製程式碼

實現 @Autowired 註解

@Autowired
int age = 10;
@Autowired
HelloService helloService;
複製程式碼

該註解是在頁面跳轉時引數傳遞用的。目標Class中使用該註解標誌的變數,會在頁面被路由開啟的時候,在呼叫inject()後自動賦予傳遞的引數值。

@Interceptor

註解定義

int priority();//該攔截器的優先順序
String name() default "Default";//該攔截器的名稱,用於產生JavaDoc
複製程式碼

實現 @Interceptor 註解

一般應用於IInterceptor的實現類,是路由跳轉過程中的攔截器,不分module,應用全域性。

@Interceptor(priority = 7)
public class Test1Interceptor implements IInterceptor {
    @Override
    public void process(final Postcard postcard, final InterceptorCallback callback) {
    ............
    }
}
複製程式碼

2.收集

在編譯期間自動生成對映檔案,arouter-compiler實現了一些註解處理器,目標在於生成對映檔案與輔助檔案。

Android路由方案ARouter分析

三種型別的註解處理器,都實現了AbstractProcessor,主要功能如下:

  • 首先通過註解處理器掃出被標註的類檔案

  • 按照不同種類的原始檔進行分類

  • 按照固定的命名格式生成對映檔案

這樣就可以在執行期初始化的時候通過固定的包名來載入對映檔案。

關於註解處理的原始碼詳解見阿里路由框架--ARouter 原始碼解析之Compiler

Android路由方案ARouter分析
以官方demo為例,通過註解處理器,按照固定的命名格式生成對映檔案。

具體以ARouter$$Root$$app為例,看下註解處理器生成的類檔案的內容:

public class ARouter$$Root$$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("service", ARouter$$Group$$service.class);
    routes.put("test", ARouter$$Group$$test.class);
  }
}
複製程式碼

通過呼叫loadInto()方法將其管理的group類檔案載入到集合中,方便後續路由查詢。

3.載入

前面的收集都是在編譯器處理獲得的,那麼載入就是到了執行期。ARouter為了避免記憶體和效能損耗,提出了“分組管理,按需載入”的方式。在前面的編譯處理的過程中,已經按照不同種類生成對應的對映檔案。

以官方demo為示例,一個app模組有一個Root結點,管理各個Group分組,每個Group分組下有著多個介面;此外app模組下還有著Interceptor結點,以及provider結點。

其中Interceptor結點對應於自定義的攔截器,provider結點對應於IOC,以實現跨模組API呼叫。

ARouter在初始化的時候只會一次性地載入所有的root結點,而不會載入任何一個Group結點,這樣就會極大地降低初始化時載入結點的數量。當某一個分組下的某一個頁面第一次被訪問的時候,整個分組的全部頁面都會被載入進去。

初始載入

ARouter 其實是一個代理類,它的所有函式實現都交給_ARouter去實現,兩個都是單例模式。

public static void init(Application application) {//靜態函式進行初始化,不依賴物件
    if (!hasInit) {
        logger = _ARouter.logger; //持有 日誌列印的 全域性靜態標量
        _ARouter.logger.info(Consts.TAG, "ARouter init start.");//列印 ARouter初始化日誌
        hasInit = _ARouter.init(application);//移交 _ARouter去 初始化

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

        _ARouter.logger.info(Consts.TAG, "ARouter init over.");//列印 ARouter初始化日誌
    }
}
複製程式碼

繼續看一下_ARouter的初始化方法

protected static synchronized boolean init(Application application) {
        mContext = application;// Application的上下文
        LogisticsCenter.init(mContext, executor);//移交邏輯中心進行初始化,並傳入線城池物件
        logger.info(Consts.TAG, "ARouter init success!");//列印日誌
        hasInit = true;//標示是否初始化完成

        // It's not a good idea.
        // if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
        //     application.registerActivityLifecycleCallbacks(new AutowiredLifecycleCallback());
        // }
        return true;
    }
複製程式碼

繼續往下走,看LogisticsCenter的初始化方法

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
        mContext = context; //靜態持有Application的上下文
        executor = tpe;//靜態持有 線城池

        try {
            // These class was generate by arouter-compiler.
            // 通過指定包名com.alibaba.android.arouter.routes,找到所有 編譯期產生的routes目錄下的類名(不包含裝載類)
            List<String> classFileNames = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);

            for (String className : classFileNames) {//組別列表com.alibaba.android.arouter.routes.ARouter\$\$Root
                if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
                    // This one of root elements, load root.
                    ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {//模組內的攔截器列表com.alibaba.android.arouter.routes.ARouter\$\$Interceptors
                    // Load interceptorMeta
                    ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {//IOC的動作路由列表com.alibaba.android.arouter.routes.ARouter\$\$Providers
                    // Load providerIndex
                    ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
                }
            }

            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() + "]");
        }
    }
複製程式碼

通過上述程式碼,實現了“分組管理,按需載入”的方式,載入了對應的三個註解處理器生成的類中管理的結點到路由集合中。

Android路由方案ARouter分析

其中記憶體倉庫Warehouse快取了全域性應用的組別的清單列表、IOC的動作路由清單列表、模組內的攔截器清單列表,3個map物件。

class Warehouse {
    // Cache route and metas
    static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();//組別的列表 包含了組名與對應組內的路由清單列表Class的對映關係
    static Map<String, RouteMeta> routes = new HashMap<>();//組內的路由列表 包含了對應分組下的,路由URL與目標物件Class的對映關係

    // Cache provider
    static Map<Class, IProvider> providers = new HashMap<>(); //快取IOC  目標class與已經建立了的物件 
    
    static Map<String, RouteMeta> providersIndex = new HashMap<>();//IOC 的動作路由列表包含了使用依賴注入方式的某class的  路由URL 與class對映關係

    // 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<>();//已排序的攔截器例項物件
 
}
複製程式碼

四、路由查詢

ARouter.getInstance().build("/test/activity2").navigation();</pre>
複製程式碼

以上述例子為例,看一下ARouter路由查詢的過程。首先看一下build過程

1.build()

public Postcard build(String path) {
    return _ARouter.getInstance().build(path);
}

protected Postcard build(String path) {
        if (TextUtils.isEmpty(path)) {
            throw new HandlerException(Consts.TAG + "Parameter is invalid!");
        } else {
            PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
            if (null != pService) {
                path = pService.forString(path);
            }
            return build(path, extractGroup(path));
        }
    }
複製程式碼

其使用了代理類_ARouter的build()並構建和返回PostCard物件。 一個Postcard物件就對應了一次路由請求,作用於本次路由全過程。

這部分程式碼主要包含兩個部分:

  • 使用 IOC byType()方式尋找PathReplaceService.class介面的實現類,該實現類的作用就是實現 “執行期動態修改路由”。
  • 繼續進行本次路由導航

首先來看一下PathReplaceService.class介面:

public interface PathReplaceService extends IProvider {

    /**
     * For normal path.
     *
     * @param path raw path
     */
    String forString(String path);

    /**
     * For uri type.
     *
     * @param uri raw uri
     */
    Uri forUri(Uri uri);
}
複製程式碼

主要包含forString()和forUri兩個方法,針對路徑進行預處理,實現 “執行期動態修改路由”。

接下下,繼續通過build(path, extractGroup(path))進行路由導航,其中extractGroup()是從路徑中獲取預設的分組資訊。

然後build()方法會返回一個Postcard物件,並把對應的路徑和分組資訊傳入該物件。

分析完上面的過程,下面來詳細看下PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);中的navigation()方法,該方法實際呼叫了代理類_ARouter的navigation(Class<? extends T> service)方法。

2.navigation(Class<? extends T> service)

protected <T> T navigation(Class<? extends T> service) {
    try {
        Postcard postcard = LogisticsCenter.buildProvider(service.getName());

        // Compatible 1.0.5 compiler sdk.
        if (null == postcard) { // No service, or this service in old version.
            postcard = LogisticsCenter.buildProvider(service.getSimpleName());
        }

        LogisticsCenter.completion(postcard);
        return (T) postcard.getProvider();
    } catch (NoRouteFoundException ex) {
        logger.warning(Consts.TAG, ex.getMessage());
        return null;
    }
}
複製程式碼
  • 首先LogisticsCenter.buildProvider(service.getName())根據Warehouse儲存的providersIndex的資訊查詢並構建返回一個PostCard物件

  • 然後執行LogisticsCenter.completion(postcard),該方法會根據Warehouse儲存的routes的路由資訊完善postcard物件,該方法在下面還會出現,到時候具體介紹

再回到上文介紹ARouter.getInstance().build("/test/activity2").navigation(),返回PostCard物件後,開始呼叫對應的navigation()方法。

3.navigation()

觀察PostCard中的該方法

public Object navigation() {
        return navigation(null);
    }
    
    public Object navigation(Context context) {
        return navigation(context, null);
    }

    public Object navigation(Context context, NavigationCallback callback) {
        return ARouter.getInstance().navigation(context, this, -1, callback);
    }

    public void navigation(Activity mContext, int requestCode) {
        navigation(mContext, requestCode, null);
    }

    public void navigation(Activity mContext, int requestCode, NavigationCallback callback) {
        ARouter.getInstance().navigation(mContext, this, requestCode, callback);
    }
複製程式碼

最終呼叫了ARouter中的navigation()方法,在其中其實是呼叫了_ARouter中的navigation()方法。

該方法包含查詢回撥的呼叫、降級處理、攔截器處理具體路由操作。

protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        try {
            LogisticsCenter.completion(postcard);
        } catch (NoRouteFoundException ex) {
            logger.warning(Consts.TAG, ex.getMessage());

            if (debuggable()) { // Show friendly tips for user.
                Toast.makeText(mContext, "There's no route matched!\n" +
                        " Path = [" + postcard.getPath() + "]\n" +
                        " Group = [" + postcard.getGroup() + "]", Toast.LENGTH_LONG).show();
            }
    
            if (null != callback) {
                callback.onLost(postcard);//觸發路由查詢失敗
            } else {    // No callback for this invoke, then we use the global degrade service.
                DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class);
                if (null != degradeService) {
                    degradeService.onLost(context, postcard);
                }
            }

            return null;
        }
        //找到了路由元資訊,觸發路由查詢的回撥
        if (null != callback) {
            callback.onFound(postcard);
        }
        //綠色通道校驗 需要攔截處理
        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);
        }

        return null;
    }
複製程式碼

其中最重要的兩個方法就是LogisticsCenter.completion()_navigation(),下面詳細介紹。

public synchronized static void completion(Postcard postcard) {
    if (null == postcard) {
        throw new NoRouteFoundException(TAG + "No postcard!");
    }
    //根據路徑URL獲取到路徑元資訊
    RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
    if (null == routeMeta) {    // Maybe its does't exist, or didn't load.
        //可能沒載入組內清單路徑,從組別的清單列表拿到對應組
        Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());  // Load route meta.
        if (null == groupMeta) {
            throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
        } else {
            //將該組的組內清單列表加入到記憶體倉庫中,並把組別移除
            try {
                if (ARouter.debuggable()) {
                    logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] starts loading, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
                }

                IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
                iGroupInstance.loadInto(Warehouse.routes);
                Warehouse.groupsIndex.remove(postcard.getGroup());

                if (ARouter.debuggable()) {
                    logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] has already been loaded, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
                }
            } catch (Exception e) {
                throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
            }

            completion(postcard);   // 再次觸發完善邏輯
        }
    } else {
        postcard.setDestination(routeMeta.getDestination());//目標 class
        postcard.setType(routeMeta.getType());//路由類
        postcard.setPriority(routeMeta.getPriority());//路由優先順序
        postcard.setExtra(routeMeta.getExtra());//額外的配置開關資訊

        Uri rawUri = postcard.getUri();
        if (null != rawUri) {   // Try to set params into bundle.
            Map<String, String> resultMap = TextUtils.splitQueryParameters(rawUri);
            Map<String, Integer> paramsType = routeMeta.getParamsType();

            if (MapUtils.isNotEmpty(paramsType)) {
                // Set value by its type, just for params which annotation by @Param
                for (Map.Entry<String, Integer> params : paramsType.entrySet()) {
                    setValue(postcard,
                            params.getValue(),
                            params.getKey(),
                            resultMap.get(params.getKey()));
                }

                // Save params name which need auto inject.
                postcard.getExtras().putStringArray(ARouter.AUTO_INJECT, paramsType.keySet().toArray(new String[]{}));
            }

            // Save raw uri
            postcard.withString(ARouter.RAW_URI, rawUri.toString());
        }

        switch (routeMeta.getType()) {
            case PROVIDER:  // if the route is provider, should find its instance
                // Its provider, so it must implement IProvider
                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;
        }
    }
}
複製程式碼

該方法就是完善PostCard,來實現一次路由導航。

接下來介紹另一個方法_navigation()

private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        final Context currentContext = null == context ? mContext : context;

        switch (postcard.getType()) {
            case ACTIVITY://如果是Acitvity,則實現Intent跳轉
                // Build intent
                final Intent intent = new Intent(currentContext, postcard.getDestination());
                intent.putExtras(postcard.getExtras());

                // Set flags.
                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);
                }

                // Navigation in main looper.
                new Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        if (requestCode > 0) {  // Need start for result
                            ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
                        } else {
                            ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle());
                        }

                        if ((-1 != postcard.getEnterAnim() && -1 != postcard.getExitAnim()) && currentContext instanceof Activity) {    // Old version.
                            ((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
                        }

                        if (null != callback) { // Navigation over.
                            callback.onArrival(postcard);
                        }
                    }
                });

                break;
            case PROVIDER://如果是IOC,則返回目標物件例項
                return postcard.getProvider();
            case BOARDCAST:
            case CONTENT_PROVIDER:
            case FRAGMENT://如果是Fragment,則返回例項,並填充bundle
                Class fragmentMeta = postcard.getDestination();
                try {
                    Object instance = fragmentMeta.getConstructor().newInstance();
                    if (instance instanceof Fragment) {
                        ((Fragment) instance).setArguments(postcard.getExtras());
                    } else if (instance instanceof android.support.v4.app.Fragment) {
                        ((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());
                    }

                    return instance;
                } catch (Exception ex) {
                    logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace()));
                }
            case METHOD:
            case SERVICE:
            default:
                return null;
        }

        return null;
    }
複製程式碼

至此我們就完成了一次路由跳轉。

相關文章