ARouter Source Code Adventures (III)

Keywords: Android less Fragment

Connect ARouter Source Code Adventures (II)

Preface

Before analyzing the real runtime source code, I want to know that the next source code analysis ideas and the process of code introduction are all based on my own thinking habits, not in accordance with the strict flow chart step by step.

ARouter.init

Neither flow chart nor specific analysis is drawn at first. Here we follow the code directly and find that the init method of Logistic Center is invoked in the process. Let's analyze the method first.

LogisticsCenter.init

First, we introduce a class, Warehouse, whose main function is to store routing information in memory. The next three fields will be used in a minute.

static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();

Store all the information in the ARouter $Root $XXX file

static Map<String, RouteMeta> providersIndex = new HashMap<>();

Store all the information in the ARouter $Providers $XXX file

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

Store all the information in the ARouter $Interceptors $XXX file

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
        mContext = context;
        executor = tpe;

        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_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)) {
                    // Load interceptorMeta
                    ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_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() + "]");
        }
    }

First, all the class names in the com.alibaba.android.arouter.routes package are obtained through the ClassUtils.getFileNameByPackageName method.

One of the features of ARouter's introduction is that it supports instant run, which is mostly presented in this way. It contains a lot of instant run content and android's knowledge of apk loading. The content claims to be chapters, so we don't go into details here. What's more, even if we don't pursue it, the instrumental methods are generally relatively fixed, which does not hinder the source code reading.

PS: If Instant run mode is used, then the ARouter.openDebug() method must be invoked to open debug mode. However, the openDebug option must be turned off in an online environment.

Traverse through all the acquired classes and call the loadinto method of the class if it is ARouter $Root $XXX to store the data in Warehouse.

Note that we are not going to load the ARouter $Group $XXX file and its contents here. The advantage is that we don't need to initialize any classes annotated by @Route to prevent slow application loading due to using arouter.

ARouter.getInstance().navigation

To interrupt the analysis of init process, we mainly want to analyze the process of navigation and build first, because this is the core of Router routing, and even in the process of init, we will use navigation and build many times.

Then the navigation call is also included in the build method, so in the idea of tracing the origin, we should first analyze the navigation method.

navigation has two overloading methods:


    public <T> T navigation(Class<? extends T> service) {
        return _ARouter.getInstance().navigation(service);
    }

    public Object navigation(Context mContext, Postcard postcard, int requestCode, NavigationCallback callback) {
        return _ARouter.getInstance().navigation(mContext, postcard, requestCode, callback);
    }

The second method we will analyze later.

First, navigation simply calls the method of _ARouter. Looking at the _ARouter class carefully, we find that it is not a public class. Similar to the proxy pattern, ARouter is actually a bit like the proxy class of _ARouter. One of the benefits of this design is also mentioned in ARouter's blog: stability. Because the developer is in contact with the ARouter class, we can modify the method name of _ARouter, etc., but as long as the exposed function remains unchanged, by modifying the code of ARouter, we can realize the advantage that the developer does not need to modify the accessed code.

Look at the navigation code for _ARouter:

protected <T> T navigation(Class<? extends T> service) {
        try {
            Postcard postcard = LogisticsCenter.buildProvider(service.getSimpleName());
            LogisticsCenter.completion(postcard);
            return (T) postcard.getProvider();
        } catch (NoRouteFoundException ex) {
            logger.warning(Consts.TAG, ex.getMessage());
            return null;
        }
    }

Look at the code for the first buildProvider:

public static Postcard buildProvider(String serviceName) {
        RouteMeta meta = Warehouse.providersIndex.get(serviceName);

        if (null == meta) {
            return null;
        } else {
            return new Postcard(meta.getPath(), meta.getGroup());
        }
    }

The content is simple. Get the RouteMeta object of the input class name from the providersIndex stored previously.

Think carefully and find a problem that the parameters of this navigation method can only be the IProvider subinterface and the interface is a direct implementation interface of a class annotated by @Route, otherwise nothing will be gained from providers Index. For what exactly is stored in providers Index, see Forgotten ARouter Source Code Adventures (I) In the analysis.

PS: According to code analysis, IProvider inheritance has "best practices", if you need to implement a Service, you should use the following inheritance line IProvider - > XXXService - > XXXService Impl.

Back in the code, we generated a Postcard in the buildProvider, but only two fields were filled in with values. So we call the LogisticsCenter.completion method to further process.

public synchronized static void completion(Postcard postcard) {
        if (null == postcard) {
            throw new NoRouteFoundException(TAG + "No postcard!");
        }

        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 {
                // Load route and cache it into memory, then delete from metas.
                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);   // Reload
            }
        } else {
            postcard.setDestination(routeMeta.getDestination());
            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 autoinject.
                    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 be implememt 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;
                default:
                    break;
            }
        }
    }

Try to use flow charts to analyze the code (it's exhausting to draw!)

What you need to know is as follows:

Under what circumstances is param not empty in RouteMate? When the type of RouteMate is Activity!

When is the uri not empty? When using uri to jump pages (isn't that nonsense?!)

Green Channel means ignoring all interceptor s!

 

Back to the navigation method, and then directly back to the postcart.provider object, indirectly indicating that the provider is not empty, that is, RouteMate is a Provider type.

ARouter.getInstance().build

First, look at the three overloading methods of build:

/**
     * Build postcard by path and default group
     */
    protected Postcard build(String path) {
        if (StringUtils.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));
        }
    }

    /**
     * Build postcard by uri
     */
    protected Postcard build(Uri uri) {
        if (null == uri || StringUtils.isEmpty(uri.toString())) {
            throw new HandlerException(Consts.TAG + "Parameter invalid!");
        } else {
            PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
            if (null != pService) {
                uri = pService.forUri(uri);
            }
            return new Postcard(uri.getPath(), extractGroup(uri.getPath()), uri, null);
        }
    }

    /**
     * Build postcard by path and group
     */
    protected Postcard build(String path, String group) {
        if (StringUtils.isEmpty(path) || StringUtils.isEmpty(group)) {
            throw new HandlerException(Consts.TAG + "Parameter is invalid!");
        } else {
            PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
            if (null != pService) {
                path = pService.forString(path);
            }
            return new Postcard(path, group);
        }
    }

The first and third of them are using path to build Postcard, the difference is whether the group parameter is passed in, if not, using the default group parameter, remember the default group in RouteProcessor? It's the first word of path.

The second is to use URI to jump, using the default group. So the problem is, if you want a page to be started through uri, then its group must be the same as the default group! Experience it carefully, young man!

In addition, all the build methods contain the same code to get PathReplaceService. What is the use of the Service ReplaceService?

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);
}

In fact, it is an interface that implements IProvider. In ARoute's official words, it is a service (not the four components of android, but the server that provides a service to the outside world). Its function is to transform the path and uri of the input. In build, it is equivalent to a wrapper. The initial incoming uri and path are wrapped and then jumped.

Postcard.navigation

If we use build to generate a Postcard, we will call Postcard's navigation to jump, look at the source code, and find that there are up to five methods overloaded by the navigation. Looking carefully, we find that it is only a parameter processing problem. Finally, the unification will call ARouter's second navigation method, that is _ARout. The navigation (Context context, Postcard postcard, int request Code, Navigation Callback callback) method of Er

protected Object navigation(final Context context, final Postcard postcard, final int requestCode, 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);
        }

        .....
    }

Call completion, which has been analyzed before, is to construct the content of postcard. If the corresponding address of path does not find the corresponding Route for routing, it will report NoRouteFoundException. The key is error handling.

If a Navigation Callback callback is manually passed in, its onLost is called to notify the caller that the call is lost and wait for the caller to process it. If the callback is not set manually, the DegradeService service (degrading process) will be retrieved if the developer implements the DegradeService interface and registers. @Route Then it will automatically call its onLost method (automatic degrading in propaganda, look is such code, the key is thought!).

If the callback is not empty, call onFound's callback, this callback is very strong, give developers the opportunity to handle postcard manually, the idea is very good, fear that developers misoperate, lead to library error, and then shout spam code...

The next code is related to interceptor:

if (!postcard.isGreenChannal()) {   // 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);
                }

                /**
                 * Interrupt process, pipeline will be destory when this method called.
                 *
                 * @param exception Reson of interrupt.
                 */
                @Override
                public void onInterrupt(Throwable exception) {
                    logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.getMessage());
                }
            });
        } else {
            return _navigation(context, postcard, requestCode);
        }

Look at the code, what exactly is interceptor service? When was it initialized? Initialized after init when found! So the question arises. We call the navigation method when init happens. Doesn't this cause null pointer exceptions? After careful reflection, we found that all the navigation instantiation used in the init phase are all the reasons for provider type. The greenChannal of these routes will be set to true, so there is nothing wrong with the old iron.

An example of interceptorService is in the api package, and the file name is InterceptorService Impl. Without analyzing the specific operation process of interceptor, look at callback first.

One way is to continue running, and the other is to call back after interruption. To continue running is to call the method of _navigation further, and then look at:

    

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

        switch (postcard.getType()) {
            case ACTIVITY:

                // Build intent
                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);
                }

                // Judgment activity start type.
                if (requestCode > 0) {  // RequestCode exist, need startActivityForResult, so this context must son of activity.
                    ((Activity) currentContext).startActivityForResult(intent, requestCode);
                } else {
                    currentContext.startActivity(intent);
                }

                break;
            case PROVIDER:
                return postcard.getProvider();
            case BOARDCAST:
            case CONTENT_PROVIDER:
            case FRAGMENT:
            case METHOD:
            case SERVICE:
            default:
                return null;
        }

        return null;
    }

If it's a jump of activity type, it builds Intent to start activity in one go... The key is flag. If we need flag, we need to call the withFlag method manually in Postcard to set flag!

If it's a Provider type, just return the instance of the Provider directly. The question is Service. Mingming has realized the jump of Service (the four components) in Processor. Is there really nothing to do here? And return null... Well, that is to say, service (the four components) can't be jumped for the time being?

Posted by johnsonzhang on Tue, 16 Jul 2019 13:46:29 -0700