Android Routing Scheme ARouter Super Details

Keywords: Android Fragment network SDK

1. Routing schemes

Disadvantages of native routing schemes:

  • Explicit: Direct class dependency, heavy coupling

  • Implicit: centralized rule management, difficult collaboration

  • Manifest has poor extensibility

  • The jump process is uncontrollable

  • Failure cannot be downgraded

Advantages of ARouter:

  • Automatic registration of mapping relationships and distributed routing management using annotations

  • Processing annotations and generating mapping files during compilation without using reflection does not affect runtime performance

  • Mapping relationships are grouped, multilevel managed, and initialized on demand

  • Flexible downgrade strategy, which calls back the results of each jump to avoid throwing operational level exceptions if StartActivity() fails

  • Custom interceptors, custom intercept order, can intercept routes, such as login judgment and buried point handling

  • Supports Dependent Injection, which can be used independently as a Dependent Injection Framework for cross-module API calls

  • Supports direct parsing of standard URL s for jumps and auto-injection of parameters into target pages

  • Support for getting Fragment s

  • Support multi-module use and component development

…….

With all these benefits, it's time to get to know ARouter.

There are advanced Android development materials and interview materials at the end of the article ~~

2. ARouter Framework

The above diagram is a basic framework diagram organized according to ARouter's basic routing and navigation process, involving the main processes, which are described in detail below.

3. Routing Management

1. Registration

With annotations, annotated classes or variables are collected at compile time and managed uniformly by Android Process Tool processing.

There are three notes @Autowired, @Interceptor, @Route.

@Route

Annotation Definition

String path();//Path URL String
String group() default "";//Group name, default to first-level path name; once set, jump must be assigned
String name() default "undefined";//The name of the path used to generate JavaDoc
int extras() default Integer.MIN_VALUE;//Switch information for additional configuration; e.g. whether some pages require network checking, login checking, etc.
int priority() default -1;//Priority of the path

Implement the @Route annotation

BlankFragment               @Route(path = "/test/fragment") 
Test1Activity               @Route(path = "/test/activity1")

This annotation is mainly used to describe the path URL information in a route. Classes labeled with this annotation are automatically added to the routing table.

@Autowired

Annotation Definition

boolean required() default false;
String desc() default "No desc.";

Implement the @Autowired annotation

@Autowired
int age = 10;
@Autowired
HelloService helloService;

This comment is used to pass parameters when a page is jumped.Variables that use this annotation flag in the target Class automatically assign the passed parameter values after calling inject() when the page is routed open.

@Interceptor

Annotation Definition

int priority();//Priority of the interceptor
String name() default "Default";//The name of the interceptor used to generate JavaDoc

Implement the @Interceptor annotation

The implementation class generally applied to IInterceptor is the interceptor in the routing jump process, regardless of module, and applies globally.

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

2. Collection

Mapping files are generated automatically during compilation, and arouter-compiler implements some annotation processors with the goal of generating mapping files and auxiliary files.

All three types of annotation processors implement AbstractProcessor with the following main functions:

  • First sweep the labeled class file through the annotation processor

  • Classify by different types of source files

  • Generate mapping files in a fixed named format

This allows the mapping file to be loaded with a fixed package name at runtime initialization.

Source code for comment processing Ali Routing Framework--Compiler for ARouter Source Resolution.

Take the official demo for example, by annotating the processor, a mapping file is generated in a fixed named format.

Take ARouter$$Root$$app for example, and look at the content of the class file generated by the annotation processor:

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

Load the group class files it manages into the collection by calling the loadInto() method to facilitate subsequent routing lookups.

3. Loading

The previous collections are all acquired by compiler processing, so the load is at runtime.To avoid memory and performance depletion, ARouter proposed a "group management, load on demand" approach.During the previous compilation process, corresponding mapping files have been generated for different types.

In the case of official demo, an app module has a Root node that manages groups of Groups, with multiple interfaces under each Group, and an Interceptor node and a provider node under the app module.

The Interceptor node corresponds to a custom interceptor, and the provider node corresponds to an IOC for cross-module API calls.

ARouter only loads all root nodes at once during initialization, not any Group nodes, which greatly reduces the number of nodes loaded during initialization.When a page under a Group is visited for the first time, all pages of the entire Group are loaded.

Initial Loading

ARouter is actually a proxy class, and all its function implementations are handed over to _ARouter, both of which are singleton patterns.

public static void init(Application application) {//Initialization of static functions, independent of objects
    if (!hasInit) {
        logger = _ARouter.logger; //Hold a global static scalar for log printing
        _ARouter.logger.info(Consts.TAG, "ARouter init start.");//Print ARouter initialization log
        hasInit = _ARouter.init(application);//Deliver_ARouter to Initialize

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

        _ARouter.logger.info(Consts.TAG, "ARouter init over.");//Print ARouter initialization log
    }
}

Continue to look at the _ARouter initialization method

protected static synchronized boolean init(Application application) {
        mContext = application;// Context of Application
        LogisticsCenter.init(mContext, executor);//Transfer the Logical Center for initialization and pass in the Line City Pool object
        logger.info(Consts.TAG, "ARouter init success!");//Print Log
        hasInit = true;//Indicates whether initialization is complete

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

Move on and see how LogisticsCenter initializes

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
        mContext = context; //Statically hold the context of the Application
        executor = tpe;//Static hold line city pool

        try {
            // These class was generate by arouter-compiler.
            // By specifying the package name com.alibaba.android.arouter.routes, find the class name (without the loading class) in the routes directory that all compile-time generations generate.
            List<String> classFileNames = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);

            for (String className : classFileNames) {//Group List 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)) {//List of interceptors in the module 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 Action Routing List 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() + "]");
        }
    }

With the above code, the way of "group management, load on demand" is implemented, and the nodes managed in the classes generated by the corresponding three annotation processors are loaded into the routing collection.

The memory warehouse Warehouse caches the list of groups for global application, the list of IOC action routes, the list of interceptors in modules, and three map objects.

class Warehouse {
    // Cache route and metas
    static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();//The list of groups contains the mapping of the group name to the routing list Class within the corresponding group
    static Map<String, RouteMeta> routes = new HashMap<>();//The routing list within the group contains the mapping relationship between the routing URL and the target object Class under the corresponding grouping

    // Cache provider
    static Map<Class, IProvider> providers = new HashMap<>(); //Cache IOC target class es and created objects 

    static Map<String, RouteMeta> providersIndex = new HashMap<>();//IOC's Action Routing List contains a mapping relationship between a class's routing URL and class using a dependent injection method

    // Cache interceptor
    //The list of interceptors within a module contains the mapping of interceptors to priorities under a module
    static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("More than one interceptors use same priority [%s]");
    static List<IInterceptor> interceptors = new ArrayList<>();//Sorted interceptor instance object

}

4. Routing lookup

ARouter.getInstance().build("/test/activity2").navigation();</pre>

Take the example above to see the ARouter routing lookup process.First, take a look at the build process

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

It uses build() of the proxy class _ARouter and builds and returns PostCard objects.A Postcard object corresponds to a routing request and acts on the whole process of the routing.

This part of the code mainly contains two parts:

  • Use IOC byType() to find the implementation class for the PathReplaceService.class interface, which is used to implement "Runtime Dynamic Routing Modification".
  • Continue with this routing navigation

First, let's look at the PathReplaceService.class interface:

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

It mainly contains forString() and forUri methods, which preprocess routes to achieve "dynamic modification of routes during runtime".

Next, continue routing navigation through build (path, extractGroup (path), where extractGroup() gets the default grouping information from the path.

The build() method then returns a Postcard object and passes the corresponding path and grouping information to it.

After analyzing the above process, let's take a closer look at the navigation() method in PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class); which actually calls the navigation (Class<? Extends T> service) method of the proxy class_ARouter.

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()) first finds and constructs a PostCard object based on the information stored by Warehouse in providersIndex

  • Then execute LogisticsCenter.completion(postcard), which perfects the postcard object based on the route information of routes saved by Warehouse. This method will also appear below and will be described in detail when

Going back to the previous section, ARouter.getInstance().build("/test/activity2").navigation(), returning the PostCard object, starts calling the corresponding navigation() method.

3.navigation()

Observing this method in 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);
    }

Finally, the navigation() method in ARouter is called, in which the navigation() method in _ARouter is actually called.

This method includes callback lookup, demotion, and interceptor handling specific routing operations.

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);//Triggering route lookup failed
            } 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;
        }
        //Routing meta information found, triggering callback for route lookup
        if (null != callback) {
            callback.onFound(postcard);
        }
        //Green Channel Check Requires Interception Processing
        if (!postcard.isGreenChannel()) {   // It must be run in async thread, maybe interceptor cost too mush time made ANR.
            //Call the interceptor section controller, traverse the custom interceptor in the memory warehouse, and execute the interceptor function in an asynchronous thread
            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;
    }

The two most important methods are LogisticsCenter.completion() and _navigation(), which are described in more detail below.

public synchronized static void completion(Postcard postcard) {
    if (null == postcard) {
        throw new NoRouteFoundException(TAG + "No postcard!");
    }
    //Obtaining path meta-information from path URL
    RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
    if (null == routeMeta) {    // Maybe its does't exist, or didn't load.
        //The list path within the group may not be loaded, getting the corresponding group from the list of groups
        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 {
            //Add the list of in-group lists for this group to the memory warehouse and remove the group
            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);   // Trigger perfect logic again
        }
    } else {
        postcard.setDestination(routeMeta.getDestination());//Target class
        postcard.setType(routeMeta.getType());//Routing Class
        postcard.setPriority(routeMeta.getPriority());//Routing Priority
        postcard.setExtra(routeMeta.getExtra());//Additional configuration switch information

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

This method is to perfect PostCard to achieve a routing navigation.

Next, another method, navigation(), is introduced.

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://Intent jump if Acitvity
                // 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://If IOC, return target object instance
                return postcard.getProvider();
            case BOARDCAST:
            case CONTENT_PROVIDER:
            case FRAGMENT://If it is a Fragment, return the instance and populate the 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;
    }

So far we have completed a route jump.

I am an Android engineer who has been engaged in seven years of development. Many people ask me privately how to learn Android Advancement in 2019. Is there any way?

Yes, I spent more than a month compiling the learning materials at the beginning of the year, hoping to help those friends who want to improve their Android development but don't know how to do so.[Including advanced UI, performance optimization, architect courses, NDK, Kotlin, architecture technical materials such as ReactNative+Weex, Flutter, etc.) I hope to help you review and find a good job before the interview, and also save you time to search for information on the web to learn.

Data acquisition method: Join Android architecture to exchange QQ group chat: 513088520, get data when you join the group!!!

Click the link to join Group Chat [Android Mobile Architecture Group]: Join group chat

Daquan

Posted by WormTongue on Mon, 29 Apr 2019 14:20:36 -0700