With the interview and work many times encountered the use of ARouter problem, I decided to ARouter source from beginning to end. Let’s see what you’re worth and why everyone uses you as a routing framework for project componentization.

preface

When developing a project, we always want to build code that can be freely reused, freely assembled, implemented in a single responsibility, and separated from maintaining a variety of reusable components.

In the componentization process, routing is an insurmountable hurdle.

When modules are freely assembled and dismantled, strong references to classes become undesirable. Some classes may not be found during compilation. So you need a way to pull up the corresponding function or page directly through the serialized string. This is the usual routing function. ARouter is a highly accepted open source routing scheme.

The purpose of this article is to provide a comprehensive analysis of the source principles of ARouter.

After reading this article, you will learn how to use Arouter, how to develop annotation processor, gradle plug-in for jar and class files to dex process.

The noun is introduced

  • apt:APTThat is (the Annotation Processing Tool)Annotation handlerJavac is a tool for handling annotations. Specifically, it is a javAC tool for handling annotationsCompile timeScan and process annotations. The annotation handler usesJava code(or compiled bytecode) as inputJava fileAs output.ARouterGenerates related routing information classes by processing annotations
  • asm:The ASM library is a code analysis and modification tool based on the Java bytecode level. ASM can produce binary class files directly or dynamically modify class behavior before the class is loaded into the JVM. In the ARouterarouter_registerPlug-in inserts initialization code.The official link

directory

  1. Project module structure
  2. Analysis of ARouter route usage
  3. ARouter initialization analysis
  4. ARouter annotation processing code generation: ARouter -compiler
  5. ARouter automatic register plugin: ARouter -register
  6. ARouter Helper = ARouter Helper
  7. Automatic code generation
  8. Gradle plug-in

I. Project module structure

We clone github ARouter source code, open the project is the following project structure diagram.

The module instructions
app Sample APP module
module-java Java sample component module
module-java-export The Java instance module’s service data module, which defines a sample IProvider service interface and some entity classes
module-kotlin Kotlin sample component module
arouter-annotation Annotation classes and related data entity classes
arouter-api Main API module, providing ARouter class routing methods
arouter-compiler Process annotations and generate related code
arouter-gradle-plugin Gradle plugins add automatic registration code to jar packages to reduce the overhead of scanning dex files
arouter-idea-plugin Idea plugin, add arouter.build (Path) line tag, and click to jump to the code definition
Introduction to Key Categories
  • ARouterClass: API entry
  • LogisticsCenter: The routing logic center maintains all route diagrams
  • Warehouse: Saves all route mappings and uses them to find routing information corresponding to all strings. This information is filled in when the ARouter is initialized.
  • RouteType: Route type, now has:ACTIVITY, SERVICE, PROVIDER, CONTENT_PROVIDER, BOARDCAST, METHOD, FRAGMENT, UNKNOWN*
  • RouteMeta: Indicates the routing information class, including the route type, route target class, and route group name.

2. Analysis of ARouter route usage

For access and use of ARouter, just refer to the official instructions.

Next, look at routing navigation processing by starting with common Activity jumps

Starting with the most commonly used API, we can understand the main workings of ARouter and how it supports the most commonly used function of jump. The jump Activity code is as follows:

ARouter.getInstance().build("/test/activity").navigation();
Copy the code

This code completes the activity jump

Key steps are as follows:

  1. throughPathReplaceServicePreprocess the path and frompath:"/test/activity"The extract group: "test"
  2. willpathgroupCreate as a parameterPostcardThe instance
  3. Call postcard#navigation, and finally navigate to _ARouter#navigation
  4. By group and pathWarehouse.routes*Get the specific path information RouteMeta and perfect postcard.

Detailed instructions

Step 1: Extract the group

Whether it’s jumping an Activity, getting a Fragment instance, or getting a Provider instance. GetInstance ().build(“”), which is the core of ARouter’s routing API. The result of this build is the Postcard class.

Let’s first look at the build code execution path:

protected Postcard build(String path) {
    if (TextUtils.isEmpty(path)) {
        throw new HandlerException(Consts.TAG + "Parameter is invalid!");
    } else {
        /// User defined path processing class. The default value is empty. Arouter.getinstance ().navigation
        PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
        if (null! = pService) { path = pService.forString(path); }/// Obtain the group contained in the path as parameter two
        return build(path, extractGroup(path), true); }}Copy the code

In the above code, the PathReplaceService instance is preprocessed by ARouter (there is no custom implementation class by default). Then, the group information is obtained from the path through the extractGroup method.

private String extractGroup(String path) {
    if(TextUtils.isEmpty(path) || ! path.startsWith("/")) {
        throw new HandlerException(Consts.TAG + "Extract the default group failed, the path must be start with '/' and contain more than 2 '/'!");
    }

    try {
        String defaultGroup = path.substring(1, path.indexOf("/".1));
        if (TextUtils.isEmpty(defaultGroup)) {
            throw new HandlerException(Consts.TAG + "Extract the default group failed! There's nothing between 2 '/'!");
        } else {
            returndefaultGroup; }}catch (Exception e) {
        logger.warning(Consts.TAG, "Failed to extract default group! " + e.getMessage());
        return null; }}Copy the code

= = = = = = = = = = = = = = = = = = = =

  1. Make sure it starts with a “/”
  2. There must be at least two “/”
  3. After the first backslash is group

So the path must have a similar format, or more “/”.

Step 2: Create the Postcard instance

That’s easy. New comes out

return new Postcard(path, group);
Copy the code

Step 3: Call_ARouter#navigation

This piece of code is the core android route jump code is a long long string:

protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    ///1. Customize the preprocessor code
    PretreatmentService pretreatmentService = ARouter.getInstance().navigation(PretreatmentService.class);
    if (null! = pretreatmentService && ! pretreatmentService.onPretreatment(context, postcard)) {// Preprocessing intercepts returns
        return null;
    }

    / / set the context
    postcard.setContext(null == context ? mContext : context);

    try {
        ///2. Find the corresponding route RouteMeta based on the RouteType RouteType
        / / / perfect posrcard
        LogisticsCenter.completion(postcard);
    } catch (NoRouteFoundException ex) {
        / / /... Omit exception logs and pop-up displays. And related callback methods
        / / / it is worth mentioning go DegradeService custom callback is lost
    }

    if (null! = callback) { callback.onFound(postcard); }///3. If it is not the green channel, you need to use interceptor: InterceptorServiceImpl
    if(! postcard.isGreenChannel()) {// It must be run in async thread, maybe interceptor cost too mush time made ANR.
        interceptorService.doInterceptions(postcard, new InterceptorCallback() {
            @Override
            public void onContinue(Postcard postcard) {
                ///4. Continue the navigation method
                _navigation(postcard, requestCode, callback);
            }
            @Override
            public void onInterrupt(Throwable exception) {
               // Omit some of the intercepted code}}); }else {
        ///4. Continue the navigation method
        return _navigation(postcard, requestCode, callback);
    }

    return null;
}
Copy the code

A quick summary of the main code steps:

  1. Perform and check intercepts if there is custom preprocessing navigation logic

  2. Through path path to find corresponding routemeta routing information, use this information to perfect it object (LogisticsCenter.com pletion methods, later analysis for details)

  3. If not, you need to use the interceptor: InterceptorServiceImpl. The interceptor service class completes the interceptor execution. PROVIDER and FRAGMENT types are green channels.

  4. To continue the navigation method, call _navigation.

Look at the code:

private Object _navigation(final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    final Context currentContext = postcard.getContext();

    switch (postcard.getType()) {
        case ACTIVITY:
            // Build intent
            final Intent intent = new Intent(currentContext, postcard.getDestination());
            / /... Omit the completion intent code
            // Navigation in main looper.
            runInMainThread(new Runnable() {
                @Override
                public void run(a) { startActivity(requestCode, currentContext, intent, postcard, callback); }});break;
        case PROVIDER:
            return postcard.getProvider();
        case BOARDCAST:
        case CONTENT_PROVIDER:
        caseFRAGMENT: 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;
}
Copy the code

Obviously, the code pays attention to various types of routing.

  • *ACTIVITY: * NewIntentThrough thepostcardInformation, perfectionintentgocontext.startActivityorcontext.startActivityForResult.
  • The PROVIDER:postcard.getProvider()To obtainproviderInstance (instantiate code inLogisticsCenter.completion)
  • FRAGMENT, BOARDCAST, CONTENT_PROVIDER:routeMeta.getConstructor().newInstance()The route information is used to instantiate the Fragment. If the Fragment is a Fragment, set this parameter separatelyextrasInformation.
  • *METHOD, SERVICE: * returns null and does nothing. Note This type of route invocationnavigationIt doesn’t make any sense.

We call the startActivity or startActivityForResult method. Other provider fragments and other instances are also clearly obtained. Let’s move on to the completion postcard key code mentioned above.

The key code: LogisticsCenter.com pletion analysis

Perfecting the information code it was done by LogisticsCenter.com pletion methods. Now let’s comb through the code:

/** * Perfect postcard * with RouteMate@param postcard Incomplete postcard, should complete by this method.
 */
public synchronized static void completion(Postcard postcard) {
    // Omit the null judgment
    RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
    if (null == routeMeta) {
        // If the routing group is not found, throw an exception
        if(! Warehouse.groupsIndex.containsKey(postcard.getGroup())) {throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
        } else {
            / /... Omit some logging code
            // 1. Dynamically add group elements (find the generation class corresponding to IRouteGroup from groupsIndex, and then load group elements)
            addRouteGroupDynamic(postcard.getGroup(), null);
            completion(postcard);   // Reload}}else {
        postcard.setDestination(routeMeta.getDestination());
        postcard.setType(routeMeta.getType());
        postcard.setPriority(routeMeta.getPriority());
        postcard.setExtra(routeMeta.getExtra());

        Uri rawUri = postcard.getUri();
        ///2. If there is URI information, resolve the PARAMETERS related to THE URI. Parse the values of the parameters of the AutoWired
        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());
        }
        ///3. Get the Provider instance, and if it is originally obtained, initialize the Provider, and then assign the postcard
        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) {
                        logger.error(TAG, "Init provider failed!", e);
                        throw new HandlerException("Init provider failed!");
                    }
                }
                postcard.setProvider(instance);
                postcard.greenChannel();    // Provider should skip all of interceptors
                break;
            case FRAGMENT:
                postcard.greenChannel();    // Fragment needn't interceptors
            default:
                break; }}}Copy the code

Taking a look at the code in this section, which refines the Postcard message, it’s broken down into three main points

  1. ** Obtain routing information: ** If the routing information is not found, dynamically add all routes in the group using the group information, and then calladdRouteGroupDynamic
  2. ** Get the parameters in the URI: ** If the POSTCARD was created with a passing URI. Resolve all parameters in the URI that require AutoInject. Place into the Postcard.
  3. ** Get the Provider instance and configure whether to leave the interceptor’s green channel or not: ** If the Provider does not exist, create and initialize the instance using the getDestination reflection of the routing information. If the Provider does exist, get the instance directly.

So that’s the analysis. The various RouteType jumps and instance retrieves are clear. Now the remaining question is, where does the routing information data in the WareHouse come from? The addRouteGroupDynamic method for dynamically adding intra-group routes was mentioned earlier.

Let’s take a look:

public synchronized static void addRouteGroupDynamic(String groupName, IRouteGroup group) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    if (Warehouse.groupsIndex.containsKey(groupName)){
        // If this group is included, but it has not been loaded
        // load this group first, because dynamic route has high priority.
        Warehouse.groupsIndex.get(groupName).getConstructor().newInstance().loadInto(Warehouse.routes);
        Warehouse.groupsIndex.remove(groupName);
    }

    // cover old group. 
    if (null != group) {
        group.loadInto(Warehouse.routes);
    }
}
Copy the code

You can see that all the routing information in Warehouse. Routes is loaded from irouteGroup.loadInto. IRouteGroup is stored in Warehouse. GroupsIndex. Then a new problem arises, where does the Warehouse. GroupsIndex data come from? The answer is in the next section, ARouter initialization analysis.

Tips: External customizable configuration mentioned above:

A simple list of source mentioned in the customization of the configuration of the IProvider. Easy to use when customizing.

  • PathReplaceService /// Route custom processing replacement
  • DegradeService // Fails to find the generic callback for a route
  • PretreatmentService /// Navigation preintercepts

Obtain the IProvider instance using the Class

User defined classes such as PathReplaceService mentioned earlier are obtained by means of arouter.getInstance ().navigation(clazz). How does this piece of code get the instance from the routing information? Look at the specific navigation code:

protected <T> T navigation(Class<? extends T> service) {
    try {
        //1. Obtain routing information from the Provider routing information index based on the class name to build a PostCart
        Postcard postcard = LogisticsCenter.buildProvider(service.getName());

        // Compatible 1.0.5 Compiler SDK
        // Earlier versions did not use the fully qualified name to get the service
        if (null == postcard) {
            // No service, or this service in old version.
             //1. Obtain routing information from the Provider routing information index based on the class name to build a PostCart
                postcard = LogisticsCenter.buildProvider(service.getSimpleName());
        }

        if (null == postcard) {
            return null;
        }

        // Set application to postcard.
        postcard.setContext(mContext);
        //2. Perfect the Postcard, which creates a provider in it
        LogisticsCenter.completion(postcard);
        return (T) postcard.getProvider();
    } catch (NoRouteFoundException ex) {
        logger.warning(Consts.TAG, ex.getMessage());
        return null; }}Copy the code

Obviously, the main code is the LogisticsCenter. BuildProvider (service. The getName ()), get to it. The code to improve the Postcard and obtain provider instances has already been described above. So let’s look at the buildProvider method:

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

    if (null == meta) {
        return null;
    } else {
        return newPostcard(meta.getPath(), meta.getGroup()); }}Copy the code

Similar to obtaining routing group information, the routing information of the Provider is obtained from the mapping table maintained by Warehouse. ProvidersIndex. ProvidersIndex is used to create instances of providers that do not have @route routing information. This is what maintaining providersIndex is for. The question then becomes where the data in providersIndex comes from.

summary

The principles of routing hops and obtaining Provider instances can be summarized as follows:

  1. First getpostcard, which may be directly through the routing path anduriBuild, such as"/test/activity1"Or it could be throughProviderThe class name is obtained from the index, such asPathReplaceService.class.getName()
  2. Then throughRouteMateperfectpostcard. Get information such as class name information, route type, provider instance, etc.
  3. Finally, the navigation, depending on the route type, makes a jump or returns the corresponding instance.

The key is the routing map that WareHouse maintains.

ARouter initialization analysis

Let’s look at the ARouter#init method provided to the user:

public static void init(Application application) {
    if(! hasInit) { logger = _ARouter.logger; _ARouter.logger.info(Consts.TAG,"ARouter init start.");
        /// Call the initialization code
        hasInit = _ARouter.init(application);
        /// After initialization, the interceptor service is loaded and all interceptors are initialized
        if (hasInit) {
            _ARouter.afterInit();
        }
        _ARouter.logger.info(Consts.TAG, "ARouter init over."); }}Copy the code

There are two key steps to the code,

  1. Initialize ARouter
  2. Getting an interceptor service instance initializes all interceptors

Initialize ARouter

The init code finally calls LogisticsCenter#init

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

    try {
        long startInit = System.currentTimeMillis();
        //load by plugin first
        //1. Load the route mapping table (registered with the ARouter plug-in)
        loadRouterMap();
        //2. Whether to initialize through plug-in registration
        if (registerByPlugin) {
            logger.info(TAG, "Load router map by ARouter-auto-register plugin.");
        } else {
            Set<String> routerMap;

            // It will rebuild router map every times when debuggable.
            // Rebuild the routing table for debugging and new versions
            if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
                logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
                // These class was generated by ARouter-compiler.
               //3. In the thread pool, scan all dex files and obtain the class name of the route mapping table from the package name
               / / package name ROUTE_ROOT_PAKCAGE: com. Alibaba. Android. ARouter. Routes
                routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
                /// Save the route mapping table class names to the cache
                if(! routerMap.isEmpty()) { context.getSharedPreferences(ARouter_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(ARouter_SP_KEY_MAP, routerMap).apply(); } PackageUtils.updateVersion(context);// Save new version name when router map update finishes.
            } else {
                /// Get all routing class files from the SharedPreferences cache
                logger.info(TAG, "Load router map from cache.");
                routerMap = new HashSet<>(context.getSharedPreferences(ARouter_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(ARouter_SP_KEY_MAP, new HashSet<String>()));
            }

            logger.info(TAG, "Find router map finished, map size = " + routerMap.size() + ", cost " + (System.currentTimeMillis() - startInit) + " ms.");
            startInit = System.currentTimeMillis();
            ///4. Load the IRouteRoot, IInterceptorGroup, and IProviderGroup classes, and populate the routing information group index for the Warehouse
            for (String className : routerMap) {
                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); }}}/ /.. Omit the log code for the route class initialization result
    } catch (Exception e) {
        throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]"); }}Copy the code

Through the code, we know the following processes:

  1. A:ARouter-auto-registerThe plug-in loads the routing table (if there is a plug-in). See section 5 for a detailed analysis of this method.
  2. Method 2:
  3. Scan all dex files as needed and find all package namescom.alibaba.android.ARouter.routesClass, class name inrouterMap Set inside.
  4. Instantiate all classes found above and load the corresponding set map index into WareHouse from these set classes.

Obviously,com.alibaba.android.ARouter.routesThe classes in the package name are automatically generated routing table classes. By searching, we can find the package name objects generated in the sample code:module_java The generatedIRouteRoot The code is shown below

public class ARouter$$Root$$modulejava implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("m2", ARouter$$Group$$m2.class);
    routes.put("module", ARouter$$Group$$module.class);
    routes.put("test", ARouter$$Group$$test.class);
    routes.put("yourservicegroupname", ARouter$$Group$$yourservicegroupname.class); }}Copy the code

This makes it completely clear where all the routing information maintained in the WareHouse comes from. Go back to the source. We just need to know when and how classes like ARouter$$Root$$ModuleJava were generated. We analyze this in the next section. The process of initializing ARouter is to fill the Warehouse providersIndex, groupsIndex, interceptorsIndex

Initialization subsequent: Initializes all interceptors

After initialization, look at the afterInit operation after initialization

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

This piece of code says that navigation got the InterceptorService. As mentioned above, when navigation is performed, the IProvider init method is called. So we need to find the InterceptorService implementation class and see what its init does. In the project, the implementation class is InterceptorServiceImpl. Find the init code as follows:

@Override
public void init(final Context context) {
    LogisticsCenter.executor.execute(new Runnable() {
        @Override
        public void run(a) {
            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(); }}}}); }Copy the code

The code explicitly tells us that the initialization code loads and instantiates all interceptors from the interceptor routing information index. The waiting interceptor is then notified to start intercepting.

summary

After watching the initialization code, understand the source of data for WareHouse, now the problem into a com. Alibaba. Android. ARouter. Routes when package name code generated. Let’s do the decomposition next time.

ARouter annotation processor: ARouter -compiler

ARouter generates routing information code that takes advantage of the attributes of annotation handlers. Aroutter-compiler is the annotation processing code module. Let’s look at the module’s dependency libraries

// Define the annotation class, and the related data entity class
implementation 'com. Alibaba: arouter - the annotation: 1.0.6'

annotationProcessor 'com. Google. Auto. Services: auto - service: 1.0 rc7'
compileOnly 'com. Google. Auto. Services: auto - service - annotations: 1.0 rc7'

implementation 'com. Squareup: javapoet: 1.8.0 comes with'

implementation 'org.apache.com mons: the Commons - lang3:3.5'
implementation 'org.apache.com mons: the Commons - collections4:4.1'

implementation 'com. Alibaba: fastjson: 1.2.69'
Copy the code

Annotation processing related dependency libraries in dependency libraries:

  • Auto-service: The official documentationTo be@AutoServiceThe annotated classes generate the corresponding metadata, which is automatically loaded when javAC is compiled and placed in the annotation processing environment.
  • javapoetSquare’s open source Java code generation framework makes it easy to generate code based on annotations, database schemas, protocol formats, etc.
  • arouter-annotation:arouter annotation classes and routing information entity classes
  • Other, utility class library

RouteProcessorNote processor processing process description

Let’s first look at the routing processor RouteProcessor

@AutoService(Processor.class)
@SupportedAnnotationTypes({ANNOTATION_TYPE_ROUTE, ANNOTATION_TYPE_AUTOWIRED})
public class RouteProcessor extends BaseProcessor {
    @Override
    // In this method you get the processingEnvironment object,
    // Using this object, you can obtain the generated code file object, debug output object, and some related utility classes
    public synchronized void init(ProcessingEnvironment processingEnv) {
         / /...
        super.init(processingEnv);
    }
    @Override
    // Return the supported Java version. Generally, return the latest Supported Java version
    public SourceVersion getSupportedSourceVersion(a) {
          / /...
        return super.getSupportedSourceVersion();
    }

    @Override
    // All annotated elements must be scanned and processed to generate a file. This method returns a value of type Boolean. If true is returned,
    // The next annotation handler is not expected to continue processing.
    // Otherwise the next annotation handler will proceed.
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        / /...
        return false; }}Copy the code

You can see that the processing of annotations is mainly in the Process method. RouteProcessor inherits BaseProcessor indirectly from AbstractProcessor, and in the BaseProcessor#init method, obtains the various utilities in processingEnv for processing annotations. It is worth mentioning that init gets moduleName and generateDoc parameters as follows:

if (MapUtils.isNotEmpty(options)) {
    ///AROUTER_MODULE_NAME
    moduleName = options.get(KEY_MODULE_NAME);
    ///AROUTER_GENERATE_DOC
    generateDoc = VALUE_ENABLE.equals(options.get(KEY_GENERATE_DOC_NAME));
}
Copy the code

This is where arguments often need to be configured in Gradle:

android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName(), AROUTER_GENERATE_DOC: "enable"]}}}}/ / or kotlin
kapt {
    arguments {
        arg("AROUTER_MODULE_NAME", project.getName())
    }
}
Copy the code

Let’s look at the implementation of the RouteProcess #process method:

Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
this.parseRoutes(routeElements);
Copy the code

The code gets all the relevant class elements annotated with the @route annotation. Then we do it in the parseRoutes method: After omitting a lot of code, the code is still very long, so we can go straight to the conclusion below:

private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
    if (CollectionUtils.isNotEmpty(routeElements)) {
        // prepare the type an so on.
        logger.info(">>> Found routes, size is " + routeElements.size() + "< < <");

        rootMap.clear();
        // omit the type fetch code
        
        / *... Builder void loadInto(Map
      
       > atlas); * /
      ,>
        MethodSpec.Builder loadIntoMethodOfRootBuilder;

        // Follow a sequence, find out metas of group first, generate java file, then statistics them as root.
        for (Element element : routeElements) {
             / /.. Skip the code and create a RouteMate instance based on the Element type.
             // And injectParamCollector methods all @AutoWired within the Activity and Fragmentr
             // Put the annotation information in the MetaData paramsType and injectConfig
v           
            // Classify the routeMate and fill the groupMap with the corresponding data
            categories(routeMeta);
        }

        / *... Omit the 'loadInto' method description, define the variable name, define the type and finally get the MethodSpec.Builder, which is used to build the providers index. void loadInto(Map
      
        providers); * /
      ,>
        MethodSpec.Builder loadIntoMethodOfProviderBuilder;

        Map<String, List<RouteDoc>> docSource = new HashMap<>();

        // Start generate java source, structure is divided into upper and lower levels, used for demand initialization.
        for (Map.Entry<String, Set<RouteMeta>> entry : groupMap.entrySet()) {
            String groupName = entry.getKey();

            Void loadInto(Map
      
        Atlas)*/
      ,>
            MethodSpec.Builder loadIntoMethodOfGroupBuilder;
            
            /// The document list is stored in the aroutert-mapof -{module_name}. Json file
            List<RouteDoc> routeDocList = new ArrayList<>();
            // Add the body of the loadInto method,
            // Step 1: Walk through all the RouteMate under the group and, if it is an IProvider subclass, add it to the providers' loadInto method
            // Step 2: Add the parameter list
            Set<RouteMeta> groupData = entry.getValue();
            for (RouteMeta routeMeta : groupData) {
                RouteDoc routeDoc = extractDocInfo(routeMeta);

                ClassName className = ClassName.get((TypeElement) routeMeta.getRawType());
                 // Omit the first step to add the provider code
                
                // Make map body for paramsType
                StringBuilder mapBodyBuilder = new StringBuilder();
                Map<String, Integer> paramsType = routeMeta.getParamsType();
                Map<String, Autowired> injectConfigs = routeMeta.getInjectConfig();
                if (MapUtils.isNotEmpty(paramsType)) {
                   / /.. Omit the build parameter method body string and add the parameters to the DOC data
                }
                String mapBody = mapBodyBuilder.toString();

                // add the body of the IRouteGroup#loadInto method
                loadIntoMethodOfGroupBuilder.addStatement(
                        "atlas.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, " + (StringUtils.isEmpty(mapBody) ? null : ("new java.util.HashMap<String, Integer>(){{" + mapBodyBuilder.toString() + "}}")) + "," + routeMeta.getPriority() + "," + routeMeta.getExtra() + ")"./ *... * /);

                routeDoc.setClassName(className.toString());
                routeDocList.add(routeDoc);
            }

            // Generate groups
            ARouter$$Group$$[GroupName] ARouter$$Group$$[GroupName]
            String groupFileName = NAME_OF_GROUP + groupName;
            JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                    TypeSpec.classBuilder(groupFileName)
                            .addJavadoc(WARNING_TIPS)
                            .addSuperinterface(ClassName.get(type_IRouteGroup))
                            .addModifiers(PUBLIC)
                            .addMethod(loadIntoMethodOfGroupBuilder.build())
                            .build()
            ).build().writeTo(mFiler);

            logger.info(">>> Generated group: " + groupName + "< < <");
            rootMap.put(groupName, groupFileName);
            docSource.put(groupName, routeDocList);
        }

        if (MapUtils.isNotEmpty(rootMap)) {
            // Generate root meta by group name, it must be generated before root, then I can find out the class of group.
            for (Map.Entry<String, String> entry : rootMap.entrySet()) {
                loadIntoMethodOfRootBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(PACKAGE_OF_GENERATE_FILE, entry.getValue())); }}// 2.Output route doc write JSON to doc document
        if (generateDoc) {
            docWriter.append(JSON.toJSONString(docSource, SerializerFeature.PrettyFormat));
            docWriter.flush();
            docWriter.close();
        }

        // Write provider into disk
        ARouter$$Providers$$[moduleName]
        String providerMapFileName = NAME_OF_PROVIDER + SEPARATOR + moduleName;
        JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                TypeSpec.classBuilder(providerMapFileName)
                        .addJavadoc(WARNING_TIPS)
                        .addSuperinterface(ClassName.get(type_IProviderGroup))
                        .addModifiers(PUBLIC)
                        .addMethod(loadIntoMethodOfProviderBuilder.build())
                        .build()
        ).build().writeTo(mFiler);

        logger.info(">>> Generated provider map, name is " + providerMapFileName + "< < <");

        // Write root meta into disk.
        ARouter$$Root$$[moduleName]
        String rootFileName = NAME_OF_ROOT + SEPARATOR + moduleName;
        JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                TypeSpec.classBuilder(rootFileName)
                        .addJavadoc(WARNING_TIPS)
                        .addSuperinterface(ClassName.get(elementUtils.getTypeElement(ITROUTE_ROOT)))
                        .addModifiers(PUBLIC)
                        .addMethod(loadIntoMethodOfRootBuilder.build())
                        .build()
        ).build().writeTo(mFiler);

        logger.info(">>> Generated root, name is " + rootFileName + "< < <"); }}Copy the code

It’s a long code, and the key result is three points,

  1. incom.alibaba.android.arouter.routes Package name generationARouter$$Group$$[GroupName] Class that contains all routing information for the routing group.
  2. Generated under the package nameARouter$$Root$$[moduleName]Class that contains information about all groups.
  3. Generated under the package nameARouter$$Providers$$[moduleName] Class that contains allProvidersThe index
  4. Under docs, the generated file is named"arouter-map-of-" + moduleName + ".json"The document.

Other annotations processor instructions

The remaining two annotation processors are InterceptorProcessor and AutowiredProcessor. The generated code logic is much the same except for the complexity of the logic,

  • AutowiredProcessor: handling@AutowiredAnnotated parameters are generated according to the corresponding class classification of the parameters[classSimpleName]$$ARouter$$Autowired Code file to automatically jump from when an Activity or Fragment jumpsintentAnd assign values to the Activity and fragment objects.
  • InterceptorProcessor: handling@InterceptorAnnotation. Generate correspondingARouter$$Interceptors$$[modulename]Code file that provides interceptor functionality.

It is worth mentioning that ARouter provides SerializationService for custom type @Autowired, and the user only needs to implement the parsing class.

summary

This module generates all the code needed to initialize ARouter. ARouter source code and source analysis to here, has successfully gone to the closed loop, the main functions have been clear. No code generation libraries related to AnotationProcessor have been written before. This time, you have learned how to use the entire annotation processing code generation framework. Also understand the principle and method of ARouter code generation. Try writing a simple code generation function in your spare time. In the next section, take a look at the source code for aroutter-register, an alternative to ARouter initializing registration.

Aroutter-register: aroutter-register

The code is in the aroutter-gradle-plugin folder,

When I first checked the source code of this module, some of the code was always red and some classes could not be found, so I modified the gradle dependency version number in build.gradle of this module. Changed from 2.1.3 to 4.1.3. The code worked. For a better understanding of the code, see the blog on the Web to start the gradle plugin debugging.

Registration converter

The aroutter-register plugin uses the registerTransform API. Added a custom Transform to customize the dex. Look directly at the entry code PluginLaunch#apply in the source code

def isApp = project.plugins.hasPlugin(AppPlugin)
//only application module needs this plugin to generate register code
if (isApp) {
    def android = project.extensions.getByType(AppExtension)
    def transformImpl = new RegisterTransform(project)
    android.registerTransform(transformImpl)
}
Copy the code

Code calls the AppExtension. RegisterTransform registerTransform method for. According to the API documentation, this method allows third-party plug-ins to manipulate the compiled class file before converting it into a dex file. There you have it, this method is part of the class file conversion process.

Scan class files and JAR files to save routing class information

What does that process do? Take a look at the code RegisterTransform#transform:

@Override
void transform(Context context, Collection<TransformInput> inputs
               , Collection<TransformInput> referencedInputs
               , TransformOutputProvider outputProvider
               , boolean isIncremental) throws IOException, TransformException, InterruptedException {

    Logger.i('Start scan register info in jar file.')

    long startTime = System.currentTimeMillis()
    boolean leftSlash = File.separator == '/'

    inputs.each { TransformInput input ->

        // Use the AMS ClassVisistor to scan all JAR files and obtain all the scanned IRouteRoot IInterceptorGroup IInterceptorGroup classes
        // Add to ScanSetting classList
        // See ScanClassVisitor for details
        // If the jar package is logisticscenter. class, mark the class file to fileContainsInitClass
        
        input.jarInputs.each { JarInput jarInput ->
            // Exclude scanning for support libraries and third-party libraries in m2Repository. scan jar file to find classes
            if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) {
                / / scan
                ScanUtil.scanJar(src, dest)
            }
            / /.. Omit the code for renaming scanned JAR packages
        }
        // scan class files
        / /.. Omit the code related to scanning class files. The method is similar to scanning JAR packages
    }

    Logger.i('Scan finish, current cost time ' + (System.currentTimeMillis() - startTime) + "ms")
    // If LogisticsCenter. Class file exists
    // Insert the registration code into logisticscenter.class
    if (fileContainsInitClass) {
        registerList.each { ext ->
            / /... Omit some nulls and logging code
            // insert the initialization code
            RegisterCodeGenerator.insertInitCodeTo(ext)
        }
    }
    Logger.i("Generate code finish, current cost time: " + (System.currentTimeMillis() - startTime) + "ms")}Copy the code

From the code, there are four key points in this piece of code.

  1. The corresponding JAR files and class files have been scanned through ASM, and the corresponding jar files have been scannedroutesThe class under the package is added toScanSettingclassListProperties of the
  2. If contains are detectedLogisticsCenter.classClass file, which is logged tofileContainsInitClassIn the field.
  3. Rename the scanned file.
  4. At last,RegisterCodeGenerator.insertInitCodeTo(ext)Method to insert initialization code intoLogisticsCenter.classIn the.

Now that we understand the scanning process, let’s look at how the code is inserted.

Walk through the JAR file containing the entry class, ready to insert the code

In RegisterCodeGenerator. InsertInitCodeTo (ext) code, the first judgment ScanSetting# classList whether is empty, then to determine whether the file jar file. If the judgment is passed, the last walk to RegisterCodeGenerator# insertInitCodeIntoJarFile code:

private File insertInitCodeIntoJarFile(File jarFile) {
  // Insert the initialization code into the JAR file containing logisticscenter.class
  // The operation is done in the ***.jar.opt temporary file
    if (jarFile) {
        def optJar = new File(jarFile.getParent(), jarFile.name + ".opt")
        if (optJar.exists())
            optJar.delete()
        /// Through JarFile and JarEntry
        def file = new JarFile(jarFile)
        Enumeration enumeration = file.entries()
        JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar))
        // Iterate over all classes in the JAR to query for changes
        while (enumeration.hasMoreElements()) {
            JarEntry jarEntry = (JarEntry) enumeration.nextElement()
            String entryName = jarEntry.getName()
            ZipEntry zipEntry = new ZipEntry(entryName)
            InputStream inputStream = file.getInputStream(jarEntry)
            jarOutputStream.putNextEntry(zipEntry)
            /// If it is a LogisticsCenter. Class file, call referHackWhenInit to insert the code
            /// If not, write the data directly without changing it
            if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) {
                Logger.i('Insert init code to class >> ' + entryName)
                //!!!! Key code, insert initialization code
                def bytes = referHackWhenInit(inputStream)
                jarOutputStream.write(bytes)
            } else {
                jarOutputStream.write(IOUtils.toByteArray(inputStream))
            }
            inputStream.close()
            jarOutputStream.closeEntry()
        }
        jarOutputStream.close()
        file.close()

        if (jarFile.exists()) {
            jarFile.delete()
        }
        optJar.renameTo(jarFile)
    }
    return jarFile
}
Copy the code

From the code, we can see that the steps are:

  1. Create a temporary file,***.jar.opt
  2. Through the input and output streams, traversaljarFile below allclassTo determine whetherLogisticCenter.class
  3. rightLogisticCenter.class callreferHackWhenInitMethod to insert the initialization code to the opt temporary file
  4. On the otherclassWrite intactoptThe temporary file
  5. Delete the originaljarFile, rename the temporary file to the originaljarThe file name

This step completes the modification of the JAR file. The automatic registration initialization code for ARouter is inserted.

Insert the initialization code

The key insert code is RegisterCodeGenerator#referHackWhenInit:

private byte[] referHackWhenInit(InputStream inputStream) {
    ClassReader cr = new ClassReader(inputStream)
    ClassWriter cw = new ClassWriter(cr, 0)
    ClassVisitor cv = new MyClassVisitor(Opcodes.ASM5, cw)
    cr.accept(cv, ClassReader.EXPAND_FRAMES)
    return cw.toByteArray()
}
Copy the code

You can see that the code utilizes the AMS framework ClassVisitor to access the entry class. Look at the visitMethod implementation of MyClassVisistor:

@Override
MethodVisitor visitMethod(int access, String name, String desc,
                          String signature, String[] exceptions) {
    MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
    //generate code into this method
   // The loadRouterMap method is used
    if (name == ScanSetting.GENERATE_TO_METHOD_NAME) {
        mv = new RouteMethodVisitor(Opcodes.ASM5, mv)
    }
    return mv
}
Copy the code

As you can see, when asm accesses a method named loadRouterMap, the operation is performed using a RouteMethodVisitor alignment as follows:

class RouteMethodVisitor extends MethodVisitor {
    RouteMethodVisitor(int api, MethodVisitor mv) {
        super(api, mv)
    }
    @Override
    void visitInsn(int opcode) {
        //generate code before return
        if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
            extension.classList.each { name ->
                name = name.replaceAll("/".".")
                mv.visitLdcInsn(name)/ / the name of the class
                // generate invoke register method into LogisticsCenter.loadRouterMap()
                mv.visitMethodInsn(Opcodes.INVOKESTATIC
                        , ScanSetting.GENERATE_TO_CLASS_NAME
                        , ScanSetting.REGISTER_METHOD_NAME
                        , "(Ljava/lang/String;) V"
                        , false)}}super.visitInsn(opcode)
    }
    @Override
    void visitMaxs(int maxStack, int maxLocals) {
        super.visitMaxs(maxStack + 4, maxLocals)
    }
}
Copy the code

Code involving asm MethodVisistor a number of API, I query, a simple understanding of the blog link here to explain a few methods used

  • VisitLdcInsn: Accesses the LDC instruction to push the parameter on the stack
  • VisitMethodInsn: Instruction for calling a method, in the code above, for callingLogisticsCenter.register(String className)methods
  • VisitMaxs: To determine the stack size of a class method at execution time.

summary

So here we have a pretty clear path to insert the initialization code.

  1. The first is to scan all of themjarandclass, find the correspondingroutesPackage name of class file and containsLogisticsCenter.class Of the classjarFile. Class file names are stored in ScanSetting according to category.
  2. findLogisticsCenter.class, perform bytecode operations on him, insert the initialization code.

The entire register plug-in process is complete

ARouter helper = ARouter helper

The source code of the plugin is in the aroutert-idea-plugin folder

At first, the compilation was not successful, so I changed the version number of the “org.jetbrains. Intellij “plug-in in the source module from 0.3.12 to 0.7.3, and it worked. /gradlew: aroutter-idea-plugin :buildPlugin can compile plug-ins.

Plug-in effect

Let’s look at this usage first. Installation is simple, just search the plugin market for ARouter Helper to install. After the plug-in is installed, the relatedARouter.build()The routingjavaFor the line of code, a positioning icon appears to the right of the line number, as shown in the figure below.Click the location icon to automatically jump to the route definition class.

After seeing the effect, we directly look at the source code. Plug-in module code is a kind of com. Alibaba. Android. Arouter. Idea. Extensions. NavigationLineMarker.

Determine if it is arouter.build

NavigationLineMarker ` inherited ` LineMarkerProviderDescriptor `, realized the ` GutterIconNavigationHandler < PsiElement >Copy the code
  • LineMarkerProviderDescriptor: Displays small ICONS (16×16 or smaller) as line markers. This is when the plugin recognizes the Navigation method and displays the standard icon to the right of the line number.
  • GutterIconNavigationHandlerIcon click handler handles icon click events.

Look at the fetching code for the row icon:

override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? {
    return if (isNavigationCall(element)) {
        LineMarkerInfo<PsiElement>(element, element.textRange, navigationOnIcon,
                Pass.UPDATE_ALL, null.this,
                GutterIconRenderer.Alignment.LEFT)
    } else {
        null}}Copy the code
  1. First, aisNavigationCallDetermine whetherARouter.build() Methods.
  2. Then configureLineMarkerInfoThat will bethisConfigure as a click handler

So let’s look at isNavigationCall first:

private fun isNavigationCall(psiElement: PsiElement): Boolean {
    if (psiElement is PsiCallExpression) {
        ///resolveMethod: Resolves the reference to the called method and returns the method. Null if the resolution fails.
        valmethod = psiElement.resolveMethod() ? :return false
        val parent = method.parent
         
        if (method.name == "build" && parent is PsiClass) {
            if (isClassOfARouter(parent)) {
                return true}}}return false
}
Copy the code

This method determines if the arouter. build method was called and returns true if it was. Displays the row tag icon.

Click the location icon to jump source

Next, click on the relevant Jump navigate method for the icon:

override fun navigate(e: MouseEvent? , psiElement:PsiElement?). {
    if (psiElement is PsiMethodCallExpression) {
        /// Build method parameter list
        val psiExpressionList = (psiElement as PsiMethodCallExpressionImpl).argumentList
        if (psiExpressionList.expressions.size == 1) {
            // Support `build(path)` only now.
            /// Search for all classes with @route annotation, match the annotated path with a path parameter, if so, jump
            val targetPath = psiExpressionList.expressions[0].text.replace("\" "."")
            val fullScope = GlobalSearchScope.allScope(psiElement.project)
            valrouteAnnotationWrapper = AnnotatedMembersSearch.search(getAnnotationWrapper(psiElement, fullScope) ? :return, fullScope).findAll()
            valtarget = routeAnnotationWrapper.find { it.modifierList? .annotations? .map { it.findAttributeValue("path")? .text? .replace("\" "."")}? .contains(targetPath) ? :false
            }

            if (null! = target) {// Redirect to target.
                NavigationItem::class.java.cast(target).navigate(true)
                return
            }
        }
    }

    notifyNotFound()
}
Copy the code
  1. Gets the parameters of the build method as the target path
  2. Search for all classes with the @route annotation and match whether the path of the annotation contains the target path parameter
  3. Find the target file directly jump toNavigationItem::class.java.cast(target).navigate(true)

End and spend

Combed through a wave of Arouter source, spent a lot of energy. But I also learned something I didn’t know much about before.

  1. Arouter’s routing principle
  2. Apt annotation handler
  3. Gradle plugin + ASM bytecode scan pile
  4. Idea line tags plug-ins and jumps

Next, try to make your own Gradle and idea plugins. Of course, the premise is to have a good stroke and rest first ~