preface

This article mainly covers the following contents: 1. Why ARouter is needed and the basic principles of ARouter 2. What are APT and ARoutr annotations and how do they work? 3. What’s wrong with ARouter? 4. What is bytecode staking and how to optimize ARouter with bytecode staking?

Why is it neededARouter

We know that traditional activities communicate with each other via startActivity(Intent), but in componentized projects, the upper module has no dependencies (even if the two modules have dependencies, So how to achieve in the case of no dependency interface jump? ARoutr helped us do that

The reason for using ARouter is for decoupling, meaning you can jump to each other without dependencies

What is theAPT

APT stands for Annotation Processing Tool. It parses the annotations specified in the code at compile time and then does something else (such as generating a new Java file through Javapoet). The principle of ButterKnife is to scan @bindView, @onclick and other annotations added by annotation processor in the compile time scanning code, and then generate XXX_ViewBinding class to realize the view binding.

The annotation processor used in ARouter is Javapoet (1). Javapoet is square’s open source Java code generation framework. Javapoet (2) is a simple and easy-to-understand API, and (3) automates the generation of complex and repetitive Java files. Compared to the original APT methods, JavaPoet is OOP

ARoutrHow does the annotation work?

We add annotations to our Activity whenever we use ARouter

@Route(path = "/kotlin/test")
class KotlinTestActivity : Activity() {
	...
}

@Route(path = "/kotlin/java")
public class TestNormalActivity extends AppCompatActivity {
	...
}
Copy the code

These annotations are processed by arouter-Compiler at compile time, and the resulting file is generated using JavaPoet to generate the class file at compile time as follows:

public class ARouter$$Group$$kotlin implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/kotlin/java", RouteMeta.build(RouteType.ACTIVITY, TestNormalActivity.class, "/kotlin/java"."kotlin".null, -1, -2147483648));
    atlas.put("/kotlin/test", RouteMeta.build(RouteType.ACTIVITY, KotlinTestActivity.class, "/kotlin/test"."kotlin".new java.util.HashMap<String, Integer>(){{put("name".8); put("age".3); }} -1, -2147483648)); }}public class ARouter$$Root$$modulekotlin implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("kotlin", ARouter$$Group$$kotlin.class); }}Copy the code

As shown above, the annotated key is associated with the class path through a Map. Once we get the Map, we can use the annotated key to retrieve the class path at runtime, enabling the jump without dependency

How do I get this Map?

ARouterdefects

The problem with ARouter is that we need to initialize the Map when we use ARouter. What ARouter does is to use reflection to scan all classnames in the specified package name, and then add the Map source code as follows

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
    //load by plugin first
    loadRouterMap();
    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.
        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.
            // reflection scans corresponding packets
            routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
            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 {
            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, newHashSet<String>())); }... }}Copy the code

As shown above: 1. The first open when using ClassUtils. GetFileNameByPackageName to scan all the className 2 under the corresponding package. After the initial scan, it is stored in SharedPreferences so that subsequent scans are not required, which is another optimization 3. Both of the above processes are time-consuming operations, which is why the ARouter may be slow to open for the first time. Is there a way to optimize the process so that you don’t need to scan it the first time?

Optimization using bytecode stakingARouterInitial startup Time

Let’s look at the code above

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
    //load by plugin first
    loadRouterMap();
    if (registerByPlugin) {
        logger.info(TAG, "Load router map by arouter-auto-register plugin.");
    } else{... }}private static void loadRouterMap(a) {
	//registerByPlugin is always set to false
    registerByPlugin = false;
}
Copy the code

During initialization, registerByPlugin is checked before scanning. If the map we need is already registered by the plugin, we don’t need to do the following time-consuming operations. However, we can see that in loadRouterMap, RegisterByPlugin is always set to false. Does registerByPlugin never work? Bytecode pegs are used to insert code into the loadRouterMap method

What is a compilation peg?

As the name suggests, compilation staking means modifying existing code or generating new code during compilation. In fact, the Dagger, ButterKnife and even Kotlin languages that we use frequently in our projects all use the technique of compiling pins.

Before we understand how to compile staking, we need to review itAndroidIn the project.javaFile compilation process:



As you can see from the figure above, we can change the code in places 1 and 2.

1. In.javaFile compiled into.classA file,APT,AndroidAnnotationThis is where the etc triggers code generation.

In 2..classThe file is further optimized into.dexFile, that is, direct manipulation of bytecode files, this is word code staking

ARouterAnnotation generation takes the first approach, and startup optimization takes the second



ASMIs a very powerful bytecode processing framework, basically can achieve any bytecode operation, that is, a high degree of freedom and development control.

But it’s a little bit moreAspectJThe difficulty to get started is high, it needs to be rightJavaHave some understanding of bytecode.

However,ASMProvides us with the visitor mode to access bytecode files, this mode can be relatively simple to do some bytecode operations, to achieve some functions.

At the same timeASMWe can inject exactly what we want, no extra wrappers are generated, so the performance impact is minimal.

For more details about ASM use, see:

Bytecode staking pairARouterWhat optimization was done?

// source code, before plugging
private static void loadRouterMap(a) {
	//registerByPlugin is always set to false
    registerByPlugin = false;
}
// Decompile the code after staking
private static void loadRouterMap(a) {
    registerByPlugin = false;
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
    register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
    register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulejava");
    register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulekotlin");
    register("com.alibaba.android.arouter.routes.ARouter$$Providers$$arouterapi");
}
Copy the code

The source code before piling and the decompiled code after piling are shown above. The code after piling inserts register code 3 into loadRouterMap at compile time. This way you can avoid scanning classnames by reflection at runtime, optimizing startup speed

The plug-in USES

Automatic loading of routing table using Gradle plug-in

apply plugin: 'com.alibaba.arouter'

buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath "com.alibaba:arouter-register:?"}}Copy the code

1. Optional automatic loading of the routing table through the registration plug-in provided by ARouter 2. By default, dex is scanned for loading. Automatic registration using gradle plug-in can shorten the initialization time and solve the problem 3 that the application hardening fails to access the dex file directly. Note that this plug-in must be used with API 1.3.0 or later! 4. The ARouter plug-in is developed based on AutoRegister, which is a more efficient automatic component registration scheme

conclusion

1. The root reason for using ARouter is to realize decoupling 2. 4.ARouter provides a plug-in to automatically load the routing table at compile time, thus avoiding startup time. The principle is bytecode staking

The resources

ARouter principle analysis and manual implementation of ARouter incurable disease analysis after reinforcement ARouter strike? See here AutoRegister: a more efficient solution for automatically registering components