Public number: byte array, hope to help you ๐Ÿคฃ๐Ÿคฃ

For Android Developer, many open source libraries are essential knowledge points for development, from the use of ways to implementation principles to source code parsing, which require us to have a certain degree of understanding and application ability. So I’m going to write a series of articles about source code analysis and practice of open source libraries, the initial target is EventBus, ARouter, LeakCanary, Retrofit, Glide, OkHttp, Coil and other seven well-known open source libraries, hope to help you ๐Ÿคฃ๐Ÿคฃ

Article Series Navigation:

  • Tripartite library source notes (1) -EventBus source detailed explanation
  • Tripartite library source notes (2) -EventBus itself to implement one
  • Three party library source notes (3) -ARouter source detailed explanation
  • Third party library source notes (4) -ARouter own implementation
  • Three database source notes (5) -LeakCanary source detailed explanation
  • Tripartite Library source note (6) -LeakCanary Read on
  • Tripartite library source notes (7) -Retrofit source detailed explanation
  • Tripartite library source notes (8) -Retrofit in combination with LiveData
  • Three party library source notes (9) -Glide source detailed explanation
  • Tripartite library source notes (10) -Glide you may not know the knowledge point
  • Three party library source notes (11) -OkHttp source details
  • Tripartite library source notes (12) -OkHttp/Retrofit development debugger
  • Third party library source notes (13) – may be the first network Coil source analysis article

A, ARouter

Routing frameworks are common in large projects, especially when the project has multiple Modules. In order to realize componentization, the communication between multiple modules cannot be realized by reference between modules directly, so it needs to rely on the routing framework to realize the communication and decoupling between modules

ARouter is a framework to help Android App componentization, support routing, communication, decoupling between modules

1. Supported functions

  1. Support to directly parse the standard URL to jump, and automatically inject parameters to the target page
  2. Support multi-module project use
  3. You can add multiple interceptors and customize the interception order
  4. Support for dependency injection and can be used as a separate dependency injection framework
  5. Support InstantRun
  6. Support for MultiDex(Google solution)
  7. Mapping relationships are classified by group, managed at different levels, and initialized on demand
  8. You can specify global and local demotion policies
  9. Pages, interceptors, services, and other components are automatically registered with the framework
  10. Supports multiple ways to configure transitions
  11. Obtaining Fragments
  12. Full support for Kotlin and blending (see Other #5 at the end of this article for configuration)
  13. Support third-party App hardening (automatic registration using arouter-register)
  14. Support for generating routing documents
  15. Provides IDE plug-ins with easy association paths and target classes
  16. Support for incremental compilation (no incremental compilation after document generation is enabled)

2. Typical applications

  1. Mapping from external URLS to internal pages, as well as parameter passing and parsing
  2. Cross – module page jump, module decoupling
  3. Intercept the jump process, processing logics such as landing and burial point
  4. Cross-module API calls do component decoupling by controlling reverse

That’s according to ARouter’s Github website README_CN

In this article, based on its current (2020/10/04) the latest version of ARouter, ARouter for a comprehensive source code analysis and principle of the introduction, do know that also know why, hope to help you ๐Ÿ˜‚๐Ÿ˜‚

dependencies {
    implementation 'com. Alibaba: arouter - API: 1.5.0'
    kapt 'com. Alibaba: arouter - compiler: 1.2.2'
}
Copy the code

Second, the preface

Suppose you have a project with multiple modules. In the module named user, there is a UserHomeActivity with a routing path of/Account /userHome. So, when we want to jump to the page from another Module, we just need to specify the path to jump to

package github.leavesc.user

/ * * * the author: leavesC * time: 2020/10/3 18:05 * description: * GitHub:https://github.com/leavesC * /
@Route(path = RoutePath.USER_HOME)
class UserHomeActivity : AppCompatActivity(a){

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user_home)
    }

}

// Other pages use the following code to jump to UserHomeActivity
ARouter.getInstance().build(RoutePath.USER_HOME).navigation()
Copy the code

How does an ARouter locate a specific Activity based on a single path? This is done by generating auxiliary code during compilation. We know that to jump to an Activity, we need to get the Activity’s Class object. During compilation, ARouter automatically generates a mapping file that contains the mapping between path and ActivityClass based on the routing rules we set

For example, for UserHomeActivity, the following auxiliary files are automatically generated during the compile phase. As you can see, the ARouter$$Group$$Account class saves path and ActivityClass as key-value pairs in the Map. That’s what ARouter relies on to jump

package com.alibaba.android.arouter.routes;

/** * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Group$$account implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/account/userHome", RouteMeta.build(RouteType.ACTIVITY, UserHomeActivity.class, "/account/userhome"."account".null, -1, -2147483648)); }}Copy the code

There is also a key to note that such a package name path automatically generated files are com. Alibaba. Android. Arouter. Routes, and the name of the class prefix is also have specific rules. Although the ARouter$$Group$$Account class implements the logic to save the mappings to a Map, the loadInto method still needs to be called by ARouter at run time. Then ARouter to get ARouter $$Group $$account this class, and ARouter is by scanning com. Alibaba. Android. ARouter. The package name path routes to get all the supporting documents

The basic idea behind ARouter is:

  1. Developer maintenanceA specific path ๅ’ŒA specific target classARouter only requires developers to use the one that contains the path@RouteAnnotations to modifyThe target class
  2. ARouter automatically generates the mapping between path and a specific target Class by annotating the handler during compilation. That is, path is used as the key and the Class object of the target Class is stored in the Map as one of the values
  3. During the run phase, the application makes a request using the path, and the ARouter obtains the target class from the Map based on the path, and then makes the jump

3. Initialization

ARouter is initialized by calling init in the Application. Let’s take a look at the initialization process

/ * * * the author: leavesC * time: 2020/10/4 18:05 * description: * GitHub:https://github.com/leavesC * /
class MyApp : Application() {

    override fun onCreate(a) {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            ARouter.openDebug()
            ARouter.openLog()
        }
        ARouter.init(this)}}Copy the code

The ARouter class uses the singleton pattern. The ARouter class is only responsible for exposing apis that can be called from outside. Most of the implementation logic is left to the _ARouter class

public final class ARouter {
    
    private volatile static ARouter instance = null;
    
    private ARouter(a) {}/** * Get instance of router. A * All feature U use, will be starts here. */
    public static ARouter getInstance(a) {
        if(! hasInit) {throw new InitException("ARouter::Init::Invoke init(context) first!");
        } else {
            if (instance == null) {
                synchronized (ARouter.class) {
                    if (instance == null) {
                        instance = newARouter(); }}}returninstance; }}/** * Init, it must be call before used router. */
    public static void init(Application application) {
        if(! hasInit) {// Prevent repeated initialization
            logger = _ARouter.logger;
            _ARouter.logger.info(Consts.TAG, "ARouter init start.");
            // Initialize by _ARouter
            hasInit = _ARouter.init(application);
            if (hasInit) {
                _ARouter.afterInit();
            }
            _ARouter.logger.info(Consts.TAG, "ARouter init over."); ...}}}Copy the code

The _ARouter class is package private and also uses singleton mode. Its init(Application) method focuses on LogisticsCenter. Init (mContext, executor)

final class _ARouter {
    
    private volatile static _ARouter instance = null;
    
    private _ARouter(a) {}protected static _ARouter getInstance(a) {
        if(! hasInit) {throw new InitException("ARouterCore::Init::Invoke init(context) first!");
        } else {
            if (instance == null) {
                synchronized (_ARouter.class) {
                    if (instance == null) {
                        instance = new_ARouter(); }}}returninstance; }}protected static synchronized boolean init(Application application) {
        mContext = application;
        / / the key
        LogisticsCenter.init(mContext, executor);
        logger.info(Consts.TAG, "ARouter init success!");
        hasInit = true;
        mHandler = new Handler(Looper.getMainLooper());
        return true; }...}Copy the code

LogisticsCenter implements the logic mentioned above to scan the specific package name path to get all the automatically generated auxiliary files, that is, when initialization, we need to load all the groups included in the current project, and the routing information table corresponding to each group. The main logic is:

  1. If the debug mode is enabled or the version of the app is changed, then the global routing information is obtained again. Otherwise, the data cached in the SP is used
  2. Getting global routing information is a time-consuming operation, so ARouter caches global routing information into SP to reuse it. Because new routing tables can be added at any time during development, and the release of a new version normally increases the version number of the application, ARouter only reobtains routing information when debug mode is enabled or when the version number changes
  3. The obtained routing information contains the following informationcom.alibaba.android.arouter.routesThe full path of the auxiliary file automatically generated under this package. By judging the prefix string of the path name, we can know what type of the file corresponds to. Then, we can build different types of objects through reflection, and save the routing information in the method of calling the objectWarehouseIn the Map. At this point, the entire initialization process is over
public class LogisticsCenter {
    
    /** * LogisticsCenter init, load all metas in memory. Demand initialization */
    public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
        mContext = context;
        executor = tpe;

        try {
            long startInit = System.currentTimeMillis();
            //billy.qi modified at 2017-12-06
            //load by plugin first
            loadRouterMap();
            if (registerByPlugin) {
                logger.info(TAG, "Load router map by arouter-auto-register plugin.");
            } else {
                Set<String> routerMap;

                // If the debug mode is enabled or the local SP cache determines that the app version has changed before and after
                // Then get the routing information again, or use the data cached in the SP before
                // It will rebuild router map every times when debuggable.
                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.
                    // Get all classnames contained in the ROUTE_ROOT_PAKCAGE package name path
                    routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
                    if(! routerMap.isEmpty()) {// Cache it in SP
                        context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
                    }
                    // Update the version of the App
                    PackageUtils.updateVersion(context);    // Save new version name when router map update finishes.
                } else {
                    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();

                for (String className : routerMap) {
                    // Use the prefix of className to determine what type the class corresponds to and cache it to Warehouse
                    //1.IRouteRoot
                    //2.IInterceptorGroup
                    //3.IProviderGroup
                    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); }}} ยทยทยท}catch (Exception e) {
            throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]"); }}}Copy the code

For the third step, an example can be given to enhance understanding

For the UserHomeActivity mentioned above, put it in a module named user. In accordance with the requirements of ARouter, declare the following configuration for this module in gradle file, with projectName as the parameter value. The path of the UserHomeActivity is /account/userHome, and by default the ARouter uses the first word of the path, account, as its group

javaCompileOptions {
    kapt {
        arguments {
            arg("AROUTER_MODULE_NAME", project.getName())
        }
    }
}
Copy the code

When ARouter generates auxiliary files from the annotation handler, the class name is generated based on this information, so the end result is the following two files:

package com.alibaba.android.arouter.routes;

/** * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Root$$user implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("account", ARouter$$Group$$account.class); }}package com.alibaba.android.arouter.routes;

/** * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Group$$account implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/account/userHome", RouteMeta.build(RouteType.ACTIVITY, UserHomeActivity.class, "/account/userhome"."account".null, -1, -2147483648)); }}Copy the code

The LogisticsCenter init method will scan ARouter$$Root$$user for the name ARouter$$Root$$, and then build the object by reflection. The key-value pair is then saved to the Warehouse. GroupsIndex by calling its loadInto method. ARouter$$group $$account loadInto = ARouter$$group $$Account loadInto = ARouter$$group $$Account loadInto = ARouter$$group $$Account

For a large App, there may be dozens or even hundreds of pages. If all the routing information is loaded into the memory at one time, the memory will be under great pressure. However, users may only open a dozen pages every time they use the App, so the on-demand loading is realized

Jump to the Activity

After the initialization process, let’s see how ARouter implements the Activity jump

The easiest way to jump to an Activity is to specify only the path:

ARouter.getInstance().build(RoutePath.USER_HOME).navigation()
Copy the code

The build() method calls the build() method of _ARouter via ARouter, and eventually returns a Postcard object

    /** * Build postcard by path and default group */
    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) {// for path replacement, which is useful in some scenarios where you need to control the flow of the page
                // For example, if a page requires a login to be displayed
                // You can replace loginPagePath with path via PathReplaceService
                path = pService.forString(path);
            }
            // Use the first word in the string path as the group
            returnbuild(path, extractGroup(path)); }}/** * Build postcard by path and group */
    protected Postcard build(String path, String group) {
        if (TextUtils.isEmpty(path) || TextUtils.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 newPostcard(path, group); }}Copy the code

The returned Postcard object can be used to pass some jump configuration parameters, such as carrying the mBundle parameter, opening the greenChannel, and jumping animation optionsCompat

public final class Postcard extends RouteMeta {
    // Base
    private Uri uri;
    private Object tag;             // A tag prepare for some thing wrong.
    private Bundle mBundle;         // Data to transform
    private int flags = -1;         // Flags of route
    private int timeout = 300;      // Navigation timeout, TimeUnit.Second
    private IProvider provider;     // It will be set value, if this postcard was provider.
    private boolean greenChannel;
    private SerializationService serializationService;
    
}
Copy the code

The Postcard navigation() method in turn calls the following method of _ARouter to complete the jump to the Activity. The method is not logically complicated, and the comments are clear

final class _ARouter {

    /**
     * Use router navigation.
     *
     * @param context     Activity or null.
     * @param postcard    Route metas
     * @param requestCode RequestCode
     * @param callback    cb
     */
    protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        PretreatmentService pretreatmentService = ARouter.getInstance().navigation(PretreatmentService.class);
        if (null! = pretreatmentService && ! pretreatmentService.onPretreatment(context, postcard)) {// Pretreatment failed, navigation canceled.
            // This method is used to perform preprocessing operations before the jump. The return value of onPretreatment determines whether to cancel the jump or not
            return null;
        }

        try {
            LogisticsCenter.completion(postcard);
        } catch (NoRouteFoundException ex) {
            // No matching target class was found
            // Next, perform some prompt actions and event callback notification
            
            logger.warning(Consts.TAG, ex.getMessage());
            if (debuggable()) {
                // Show friendly tips for user.
                runInMainThread(new Runnable() {
                    @Override
                    public void run(a) {
                        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) {// A matching target class was found
            callback.onFound(postcard);
        }

        if(! postcard.isGreenChannel()) {// It must be run in async thread, maybe interceptor cost too mush time made ANR.
            // If the green channel is not turned on, then all interceptors need to be executed
            // The external can be implemented through the interceptor: control whether to allow jump, change jump parameters, and other logic
            
            interceptorService.doInterceptions(postcard, new InterceptorCallback() {
                /**
                 * Continue process
                 *
                 * @param postcard route meta
                 */
                @Override
                public void onContinue(Postcard postcard) {
                    // The interceptor allows jumps
                    _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 {
            // Open the green channel, direct jump, do not need to traverse the interceptor
            return _navigation(context, postcard, requestCode, callback);
        }

        return null;
    }
    
    // Since the target page of this example is the Activity, you can only look at the Activity
    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:
                // Build intent
                //Destination is the class object that points to the target Activity
                final Intent intent = new Intent(currentContext, postcard.getDestination());
                // Insert the carried parameter
                intent.putExtras(postcard.getExtras());

                // Set flags.
                int flags = postcard.getFlags();
                if (-1! = flags) { intent.setFlags(flags); }else if(! (currentContextinstanceof Activity)) {    // Non activity, need less one flag.
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }

                // Set Actions
                String action = postcard.getAction();
                if(! TextUtils.isEmpty(action)) { intent.setAction(action); }// Navigation in main looper.
                // Finally complete the jump on the main thread
                runInMainThread(new Runnable() {
                    @Override
                    public void run(a) { startActivity(requestCode, currentContext, intent, postcard, callback); }});break; ...// Omit other types of judgments
        }

        return null; }}Copy the code

Navigation method is focused on LogisticsCenter.com pletion (it) this code. We talked about the ARouter initialization process: ARouter$$group $$account loadInto = ARouter$$group $$Account loadInto = ARouter$$group $$Account

The Completion method is used to obtain detailed route information. This method takes the value from Warehouse via the path and group messages the Postcard carries, saves the message to the postcard if it is not null, and throws a NoRouteFoundException if it is

	/**
     * Completion the postcard by route metas
     *
     * @param postcard Incomplete postcard, should complete by this method.
     */
    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) {    // Null indicates that the target class does not exist or that the group has not been loaded
            Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());  // Load route meta.
            if (null == groupMeta) {
                // If groupMeta is null, the group corresponding to the postcard path does not exist, and an exception is raised
                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()));
                    }
				  // It will execute at this point, indicating that the group has not been loaded, so it will reflect all the path information corresponding to the loading group
                   // Save it to Warehouse. Routes
                    IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
                    iGroupInstance.loadInto(Warehouse.routes);
                    
                    // Remove this group
                    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() + "]");
                }
			   
                // Run this command again
                completion(postcard);   // Reload}}else {
            // Get the detailed routing information and save it in the Postcard
            
            postcard.setDestination(routeMeta.getDestination());
            postcard.setType(routeMeta.getType());
            postcard.setPriority(routeMeta.getPriority());
            postcard.setExtra(routeMeta.getExtra());

            // Omit some code that is irrelevant to this example...}}Copy the code

Jump to the Activity and inject parameters

ARouter also supports automatic injection of parameters to the target page while jumping to the Activity

Specifies the key-value pair to carry when jumping:

ARouter.getInstance().build(RoutePath.USER_HOME)
        .withLong(RoutePath.USER_HOME_PARAMETER_ID, 20)
        .withString(RoutePath.USER_HOME_PARAMETER_NAME, "leavesC")
        .navigation()

object RoutePath {

    const val USER_HOME = "/account/userHome"

    const val USER_HOME_PARAMETER_ID = "userHomeId"

    const val USER_HOME_PARAMETER_NAME = "userName"

}
Copy the code

Modify the variable with the @autowired annotation on the target page. The annotation can also declare its name argument, which corresponds to the key in the passed key-value pair, so that the ARouter knows which variable to assign to. If the name argument is not declared, the name argument is equal to the variable name by default

Thus, after we call arouter.getInstance ().inject(this), ARouter will automatically complete the parameter assignment

package github.leavesc.user

/ * * * the author: leavesC * time: 2020/10/4 14:05 * description: * GitHub:https://github.com/leavesC * /
@Route(path = RoutePath.USER_HOME)
class UserHomeActivity : AppCompatActivity() {

    @Autowired(name = RoutePath.USER_HOME_PARAMETER_ID)
    @JvmField
    var userId: Long = 0

    @Autowired
    @JvmField
    var userName = ""

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user_home)
        ARouter.getInstance().inject(this)
        tv_hint.text = "$userId $userName"}}Copy the code

ARouter’s implementation of automatic parameter injection also relies on auxiliary files generated by the annotation handler, which generate the following auxiliary code:

package github.leavesc.user;

/** * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class UserHomeActivity$$ARouter$$Autowired implements ISyringe {
    
  // For serialization and deserialization
  private SerializationService serializationService;

  @Override
  public void inject(Object target) {
    serializationService = ARouter.getInstance().navigation(SerializationService.class);
    UserHomeActivity substitute = (UserHomeActivity)target;
    substitute.userId = substitute.getIntent().getLongExtra("userHomeId", substitute.userId);
    substitute.userName = substitute.getIntent().getStringExtra("userName"); }}Copy the code

Since the parameters to be carried when jumping to an Activity also need to be placed in the Intent, the inject method only helps us implement the logic that values from the Intent are then assigned to the variable, which requires that the corresponding variable be public. This is why we need to annotate the @jVMField variable as well in our Kotlin code

Arouter.getinstance ().inject(this) ยถ Arouter.getInstance ().inject(this) ยถ

final class _ARouter {
   
    static void inject(Object thiz) {
        AutowiredService autowiredService = ((AutowiredService) ARouter.getInstance().build("/arouter/service/autowired").navigation());
        if (null! = autowiredService) { autowiredService.autowire(thiz); }}}Copy the code

ARouter obtains the instance object of AutowiredServiceImpl, which corresponds to AutowiredService, through inversion of control, and then calls its Autowire method to complete parameter injection

$$ARouter$$Autowired, $$ARouter$$Autowired, $$ARouter$$Autowired, $$ARouter$$Autowired Therefore, in AutowiredServiceImpl, we can generate the auxiliary class object according to the passed instance parameter and reflection, and finally call its inject method to complete the parameter injection

@Route(path = "/arouter/service/autowired")
public class AutowiredServiceImpl implements AutowiredService {
    private LruCache<String, ISyringe> classCache;
    private List<String> blackList;

    @Override
    public void init(Context context) {
        classCache = new LruCache<>(66);
        blackList = new ArrayList<>();
    }

    @Override
    public void autowire(Object instance) {
        String className = instance.getClass().getName();
        try {
            // If it is in the whitelist, then the parameter injection is not performed
            if(! blackList.contains(className)) { ISyringe autowiredHelper = classCache.get(className);if (null == autowiredHelper) {  // No cache.
                    autowiredHelper = (ISyringe) Class.forName(instance.getClass().getName() + SUFFIX_AUTOWIRED).getConstructor().newInstance();
                }
                // Complete parameter injection
                autowiredHelper.inject(instance);
                // Cache to avoid repeated reflectionsclassCache.put(className, autowiredHelper); }}catch (Exception ex) {
            // If the parameter injection process throws an exception, whitelist it
            blackList.add(className);    // This instance need not autowired.}}}Copy the code

6. Inversion of Control

Jumping to an Activity and automatically injecting parameters, as described in the previous section, is a form of dependency injection. ARouter also supports inversion of control: obtaining an instance of its implementation class through the interface

For example, if we have an ISayHelloService interface and we need to get an instance of its implementation class, but we don’t want to use it with strong coupling to the specific implementation class SayHelloService, we can use ARouter’s Inversion of Control. This also requires that the ISayHelloService interface inherits from the IProvider interface

/ * * * the author: leavesC * time: 2020/10/4 13:49 * description: * GitHub:https://github.com/leavesC * /
interface ISayHelloService : IProvider {

    fun sayHello(a)

}

@Route(path = RoutePath.SERVICE_SAY_HELLO)
class SayHelloService : ISayHelloService {

    override fun init(context: Context){}override fun sayHello(a) {
        Log.e("SayHelloService"."$this sayHello")}}Copy the code

ARouter returns SayHelloService as a singleton, eliminating the need for developers to manually build SayHelloService objects, thus enabling decoupling

ARouter.getInstance().navigation(ISayHelloService::class.java).sayHello()
Copy the code

As with Activity jump, ARouter automatically generates the following files containing the mapping of the routing table

package com.alibaba.android.arouter.routes;

/** * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Group$$account implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/account/sayHelloService", RouteMeta.build(RouteType.PROVIDER, SayHelloService.class, "/account/sayhelloservice"."account".null, -1, -2147483648)); }}/** * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Providers$$user implements IProviderGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> providers) {
    providers.put("github.leavesc.user.ISayHelloService", RouteMeta.build(RouteType.PROVIDER, SayHelloService.class, "/account/sayHelloService"."account".null, -1, -2147483648)); }}/** * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Root$$user implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("account", ARouter$$Group$$account.class); }}Copy the code

Here’s how it works

As mentioned in the initialization process, LogisticsCenter implements the logic to scan a specific package name path to get all automatically generated auxiliary files. So, eventually the Warehouse will get the following data at initialization time

Warehouse. GroupsIndex:

  • account -> class com.alibaba.android.arouter.routes.ARouter$$Group$$account

Warehouse. ProvidersIndex:

  • github.leavesc.user.ISayHelloService -> RouteMeta.build(RouteType.PROVIDER, SayHelloService.class, "/account/sayHelloService", "account", null, -1, -2147483648)

ARouter. GetInstance (). Navigation (ISayHelloService: : class. Java) final assembly _ARouter modulation used the following method

	protected <T> T navigation(Class<? extends T> service) {
        try {
            // Get the path and group stored in the RouteMeta from the Warehouse. ProvidersIndex value
            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.
                postcard = LogisticsCenter.buildProvider(service.getSimpleName());
            }

            if (null == postcard) {
                return null;
            }
		   / / the key
            LogisticsCenter.completion(postcard);
            return (T) postcard.getProvider();
        } catch (NoRouteFoundException ex) {
            logger.warning(Consts.TAG, ex.getMessage());
            return null; }}Copy the code

LogisticsCenter.com pletion (it) method to interpretation of the process and before about the same, just at the time of access to an object instance will be cached instance at the same time, for later reuse, thus completes the inversion of control process

	/**
     * Completion the postcard by route metas
     *
     * @param postcard Incomplete postcard, should complete by this method.
     */
    public synchronized static void completion(Postcard postcard) {...// Omit the code already explained
  
	   RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
        
        switch (routeMeta.getType()) {
                case PROVIDER:  // if the route is provider, should find its instance
                    // Its provider, so it must implement IProvider
                	// Get the SayHelloService Class object
                    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
                        // Instance is null
                        // Then build the SayHelloService object through reflection and cache it to the Warehouse. Providers
                        // So objects obtained through INVERSION of control will have only one instance for the entire life of the application
                        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()); }}// Save the obtained instance
                    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

7. Interceptors

ARouter’s interceptor is a useful feature for some business logic that needs to control the flow of page hops. For example, if a user wants to jump to a profile page, we can use an interceptor to determine if the user is logged in. If not, we can intercept the request and automatically open the login page for the user

We can set multiple interceptors at the same time, each with a different priority

/ * * * the author: leavesC * time: 2020/10/5 11:49 * description: * GitHub:https://github.com/leavesC * /
@Interceptor(priority = 100, name = "An interceptor that does nothing.")
class NothingInterceptor : IInterceptor {

    override fun init(context: Context){}override fun process(postcard: Postcard, callback: InterceptorCallback) {
        // Do not intercept, let it jump
        callback.onContinue(postcard)
    }

}

@Interceptor(priority = 200, name = "Landing interceptor.")
class LoginInterceptor : IInterceptor {

    override fun init(context: Context){}override fun process(postcard: Postcard, callback: InterceptorCallback) {
        if (postcard.path == RoutePath.USER_HOME) {
            / / intercept
            callback.onInterrupt(null)
            // Go to the landing page
            ARouter.getInstance().build(RoutePath.USER_LOGIN).navigation()
        } else {
            // Do not intercept, let it jump
            callback.onContinue(postcard)
        }
    }

}
Copy the code

So when we run arout.getInstance ().build(routePath.user_home).navigation() to jump, we’ll see that the login page, routepath.user_login, is opened

How is the interceptor implemented

For the above two interceptors, the following auxiliary files are generated. The helper file takes all of our custom interceptor implementation classes and stores them in the Map according to their priority

package com.alibaba.android.arouter.routes;

/** * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Interceptors$$user implements IInterceptorGroup {
  @Override
  public void loadInto(Map<Integer, Class<? extends IInterceptor>> interceptors) {
    interceptors.put(100, NothingInterceptor.class);
    interceptors.put(200, LoginInterceptor.class); }}Copy the code

And these interceptors will be at the time of initialization, as through the LogisticsCenter. The init method to Warehouse. InterceptorsIndex

 /** * LogisticsCenter init, load all metas in memory. Demand initialization */
    public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {...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
                        // Get the custom interceptor implementation class
                        ((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); ...}}}Copy the code

Then, in the _ARouter navigation method, how to determine that the green channel mode is not enabled for the request, and pass the request to the interceptorService to iterate over each interceptor

final class _ARouter {
 
    /**
     * Use router navigation.
     *
     * @param context     Activity or null.
     * @param postcard    Route metas
     * @param requestCode RequestCode
     * @param callback    cb
     */
    protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {...if(! postcard.isGreenChannel()) {// It must be run in async thread, maybe interceptor cost too mush time made ANR.
            
            // Iterate through the interceptor
            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; }}Copy the code

The interceptorService variable is of the interceptorService interface type. The implementation class of this interface is InterceptorServiceImpl. The internal ARouter obtains the interceptorService instance using the inversion of control (INVERSION of Control) method during initialization

The main logic of InterceptorServiceImpl is:

  1. On the first acquisitionInterceptorServiceImplInstance of theinitMethod is called immediately, which is executed internally by a thread pool, generating each interceptor object by reflection, and calling each interceptor’sinitMethod to complete the initialization of interceptors and save each interceptor object toWarehouse.interceptorsIn the. If the initialization is complete, the wake waits atinterceptorInitLockOn the thread
  2. When interceptor logic is triggered, i.edoInterceptionsWhen the method is called, it passes if the first step has not been completed at this pointcheckInterceptorsInitStatus()Method waits for the first step to complete. If it is not completed within ten seconds, then go to the failure process and return directly
  3. The list of interceptors is traversed in the thread pool and is called if one intercepts the requestcallback.onInterruptMethod is notified externally, otherwise calledcallback.onContinue()Method continues the jump logic
@Route(path = "/arouter/service/interceptor")
public class InterceptorServiceImpl implements InterceptorService {
    private static boolean interceptorHasInit;
    private static final Object interceptorInitLock = new Object();
    
    @Override
    public void init(final Context context) {
        LogisticsCenter.executor.execute(new Runnable() {
            @Override
            public void run(a) {
                if (MapUtils.isNotEmpty(Warehouse.interceptorsIndex)) {
                    // Iterate through the list of interceptors, building objects from reflection and initializing them
                    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);
                            / / save it
                            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(); }}}}); }@Override
    public void doInterceptions(final Postcard postcard, final InterceptorCallback callback) {
        if (null! = Warehouse.interceptors && Warehouse.interceptors.size() >0) {

            checkInterceptorsInitStatus();

            if(! interceptorHasInit) {// The initialization process is too long
                callback.onInterrupt(new HandlerException("Interceptors initialization takes too much time."));
                return;
            }

            LogisticsCenter.executor.execute(new Runnable() {
                @Override
                public void run(a) {
                    CancelableCountDownLatch interceptorCounter = new CancelableCountDownLatch(Warehouse.interceptors.size());
                    try {
                        _excute(0, interceptorCounter, postcard);
                        interceptorCounter.await(postcard.getTimeout(), TimeUnit.SECONDS);
                        if (interceptorCounter.getCount() > 0) {    // Cancel the navigation this time, if it hasn't return anythings.
                            // If the value is greater than 0, the request is intercepted by an interceptor
                            callback.onInterrupt(new HandlerException("The interceptor processing timed out."));
                        } else if (null! = postcard.getTag()) {// Maybe some exception in the tag.
                            callback.onInterrupt(new HandlerException(postcard.getTag().toString()));
                        } else{ callback.onContinue(postcard); }}catch(Exception e) { callback.onInterrupt(e); }}}); }else{ callback.onContinue(postcard); }}/**
     * Excute interceptor
     *
     * @param index    current interceptor index
     * @param counter  interceptor counter
     * @param postcard routeMeta
     */
    private static void _excute(final int index, final CancelableCountDownLatch counter, final Postcard postcard) {
        if (index < Warehouse.interceptors.size()) {
            IInterceptor iInterceptor = Warehouse.interceptors.get(index);
            iInterceptor.process(postcard, new InterceptorCallback() {
                @Override
                public void onContinue(Postcard postcard) {
                    // Last interceptor excute over with no exception.
                    counter.countDown();
                    _excute(index + 1, counter, postcard);  // When counter is down, it will be execute continue ,but index bigger than interceptors size, then U know.
                }

                @Override
                public void onInterrupt(Throwable exception) {
                    // Last interceptor excute over with fatal exception.

                    postcard.setTag(null == exception ? new HandlerException("No message.") : exception.getMessage());    // save the exception message for backup.
                    counter.cancel();
                    // Be attention, maybe the thread in callback has been changed,
                    // then the catch block(L207) will be invalid.
                    // The worst is the thread changed to main thread, then the app will be crash, if you throw this exception!
// if (! Looper.getMainLooper().equals(Looper.myLooper())) { // You shouldn't throw the exception if the thread is main thread.
// throw new HandlerException(exception.getMessage());
/ /}}}); }}private static void checkInterceptorsInitStatus(a) {
        synchronized (interceptorInitLock) {
            while(! interceptorHasInit) {try {
                    interceptorInitLock.wait(10 * 1000);
                } catch (InterruptedException e) {
                    throw new HandlerException(TAG + "Interceptor init cost too much time error! reason = [" + e.getMessage() + "]");
                }
            }
        }
    }
    
}
Copy the code

Viii. Annotation processor

Throughout this article, the reader should be able to feel that the annotation processor plays a big role in ARouter, relying on the annotation processor to generate auxiliary files, ARouter to complete the automatic parameter injection and other functions. Here we will introduce the implementation principle of ARouter on the annotation processor

The Annotation Processing Tool is an Annotation Processing Tool that scans and processes annotations at compile time to generate Java files from annotations. That is, using annotations as a bridge to automatically generate Java files through predefined code generation rules. Representatives of such annotation frameworks are ButterKnife, Dragger2, EventBus, and so on

The Java API already provides a framework for scanning source code and parsing annotations, and developers can implement their own annotation parsing logic by inheriting AbstractProcessor classes. The principle of APT is that after some code elements (such as fields, functions, classes, etc.) are annotated, the compiler will check subclasses of AbstractProcessor at compile time, and automatically call its process() method, and then pass all code elements with specified annotations to the method as parameters. The developer then outputs Java code at compile time based on the annotation elements

For more information on how APT works and how it works, see this article: APT For Android

There are two modules associated with the annotation processor in the ARouter source code:

  • Arouter – the annotation. The Java Module contains annotations like Autowired and Interceptor, as well as Javabeans like RouteMeta
  • Arouter – the compiler. Android Module, which contains several AbstractProcessor implementation classes for generating code

Here we will focus on the aroutter-compiler. Here we will use the custom NothingInterceptor as an example

package github.leavesc.user

/ * * * the author: leavesC * time: 2020/10/5 11:49 * description: * GitHub:https://github.com/leavesC * /
@Interceptor(priority = 100, name = "An interceptor that does nothing.")
class NothingInterceptor : IInterceptor {

    override fun init(context: Context){}override fun process(postcard: Postcard, callback: InterceptorCallback) {
        // Do not intercept, let it jump
        callback.onContinue(postcard)
    }

}
Copy the code

Auxiliary files generated:

package com.alibaba.android.arouter.routes;

import com.alibaba.android.arouter.facade.template.IInterceptor;
import com.alibaba.android.arouter.facade.template.IInterceptorGroup;
import github.leavesc.user.NothingInterceptor;
import java.lang.Class;
import java.lang.Integer;
import java.lang.Override;
import java.util.Map;

/** * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Interceptors$$user implements IInterceptorGroup {
  @Override
  public void loadInto(Map<Integer, Class<? extends IInterceptor>> interceptors) {
    interceptors.put(100, NothingInterceptor.class); }}Copy the code

Then, the generated auxiliary file should contain the following elements:

  1. The package name
  2. Guide package
  3. annotation
  4. Implement the interface of the class and its inheritance
  5. Contains methods and method parameters
  6. Method body
  7. The modifier

It is possible to hard-code the code by concatenating strings, but this makes the code unmaintainable and unreadable, so ARouter uses the JavaPoet open source library to generate the code. JavaPoet is square’s open source Java code generation framework that makes it easy to generate code in specified formats (modifiers, return values, arguments, function bodies, and so on) using an API

The interceptor subclass of AbstractProcessor is InterceptorProcessor, and its main logic is:

  1. The process method obtains all the code elements that are modified by the @Interceptor annotation from RoundEnvironment, and then iterates through all the items
  2. Check whether each item inherits from the IInterceptor interface. If it does, it is the interceptor implementation class we are looking for
  3. Get the @interceptor annotation object contained in each item, and store each item in the order of interceptors based on the priority we set for it
  4. If there are two interceptors of the same priority, an exception is thrown
  5. After all interceptors are stored in the interceptors in order, multiple code elements such as package name, guide package, annotation, implementation class and so on are generated by the API provided by JavaPoet, and finally a complete class file is generated
@AutoService(Processor.class)
@SupportedAnnotationTypes(ANNOTATION_TYPE_INTECEPTOR)
public class InterceptorProcessor extends BaseProcessor {

    // Used to store interceptors in order of priority
    private Map<Integer, Element> interceptors = new TreeMap<>();

    private TypeMirror iInterceptor = null;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        iInterceptor = elementUtils.getTypeElement(Consts.IINTERCEPTOR).asType();

        logger.info(">>> InterceptorProcessor init. <<<");
    }

    / * * * {@inheritDoc}
     *
     * @param annotations
     * @param roundEnv
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (CollectionUtils.isNotEmpty(annotations)) {
            // Obtain all code elements decorated with @interceptor
            Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Interceptor.class);
            try {
                parseInterceptors(elements);
            } catch (Exception e) {
                logger.error(e);
            }
            return true;
        }

        return false;
    }

    /**
     * Parse tollgate.
     *
     * @param elements elements of tollgate.
     */
    private void parseInterceptors(Set<? extends Element> elements) throws IOException {
        if (CollectionUtils.isNotEmpty(elements)) {
            logger.info(">>> Found interceptors, size is " + elements.size() + "< < <");

            // Verify and cache, sort incidentally.
            for (Element element : elements) {
                / / judge whether using the @ Interceptor was modified code elements at the same time realize the com. Alibaba. Android. Arouter. The facade. The template. The IInterceptor this interface
                // Both are indispensable
                if (verify(element)) {  // Check the interceptor meta
                    logger.info("A interceptor verify over, its " + element.asType());
                    Interceptor interceptor = element.getAnnotation(Interceptor.class);

                    Element lastInterceptor = interceptors.get(interceptor.priority());
                    if (null! = lastInterceptor) {// Added, throw exceptions
                        // Not null indicates that there are two interceptors with the same priority. This is not allowed and an exception is thrown
                        throw new IllegalArgumentException(
                                String.format(Locale.getDefault(), "More than one interceptors use same priority [%d], They are [%s] and [%s].",
                                        interceptor.priority(),
                                        lastInterceptor.getSimpleName(),
                                        element.getSimpleName())
                        );
                    }
                    // Save interceptors in order of priority
                    interceptors.put(interceptor.priority(), element);
                } else {
                    logger.error("A interceptor verify failed, its "+ element.asType()); }}// Interface of ARouter.
            / / get com. Alibaba. Android. Arouter. The facade. The template. The IInterceptor abstract this interface type
            TypeElement type_ITollgate = elementUtils.getTypeElement(IINTERCEPTOR);
            / / get com. Alibaba. Android. Arouter. The facade. The template. The IInterceptorGroup abstract this interface type
            TypeElement type_ITollgateGroup = elementUtils.getTypeElement(IINTERCEPTOR_GROUP);

            /** * Build input type, format as : * * ```Map
      
       >``` */
      ,>
            Map
      
       > Abstract encapsulation of this code
      ,>
            ParameterizedTypeName inputMapTypeOfTollgate = ParameterizedTypeName.get(
                    ClassName.get(Map.class),
                    ClassName.get(Integer.class),
                    ParameterizedTypeName.get(
                            ClassName.get(Class.class),
                            WildcardTypeName.subtypeOf(ClassName.get(type_ITollgate))
                    )
            );

            // Build input param name.
            // Generate the interceptors parameter for the loadInto method
            ParameterSpec tollgateParamSpec = ParameterSpec.builder(inputMapTypeOfTollgate, "interceptors").build();

            // Build method : 'loadInto'
            // Generate the loadInto method
            MethodSpec.Builder loadIntoMethodOfTollgateBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
                    .addAnnotation(Override.class)
                    .addModifiers(PUBLIC)
                    .addParameter(tollgateParamSpec);

            // Generate
            if (null! = interceptors && interceptors.size() >0) {
                // Build method body
                for (Map.Entry<Integer, Element> entry : interceptors.entrySet()) {
                    // Iterates through each interceptor to generate interceptors. Put (100, noThingInterceptor.class); This type of code
                    loadIntoMethodOfTollgateBuilder.addStatement("interceptors.put(" + entry.getKey() + ", $T.class)", ClassName.get((TypeElement) entry.getValue())); }}// Write to disk(Write file even interceptors is empty.)
            / / the package name is fixed PACKAGE_OF_GENERATE_FILE, i.e., com. Alibaba. Android. Arouter. Routes
            JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                    TypeSpec.classBuilder(NAME_OF_INTERCEPTOR + SEPARATOR + moduleName) // Set the class name
                            .addModifiers(PUBLIC) // Add the public modifier
                            .addJavadoc(WARNING_TIPS) // Add comments
                            .addMethod(loadIntoMethodOfTollgateBuilder.build()) // Add the loadInto method
                            .addSuperinterface(ClassName.get(type_ITollgateGroup)) // The resulting class also implements the IInterceptorGroup interface
                            .build()
            ).build().writeTo(mFiler);

            logger.info(">>> Interceptor group write over. <<<"); }}/**
     * Verify inteceptor meta
     *
     * @param element Interceptor taw type
     * @return verify result
     */
    private boolean verify(Element element) {
        Interceptor interceptor = element.getAnnotation(Interceptor.class);
        // It must be implement the interface IInterceptor and marked with annotation Interceptor.
        return null != interceptor && ((TypeElement) element).getInterfaces().contains(iInterceptor);
    }
}
Copy the code

Nine, the end

ARouter’s implementation principle and source code analysis are about the same, I think it is quite comprehensive, then the next article to enter the actual combat, to achieve a simple version of ARouter ๐Ÿ˜‚๐Ÿ˜‚