Principle Analysis of DRouter, Android Didi Routing Framework

Author: linversion

foreword

A recent new project uses the Clean Architecture+modularization+MVVM architecture, puts the functions corresponding to each tab on the homepage into separate modules and does not depend on each other. At this time, there is a problem of page jumping between modules. After some Didi’s DRouter was selected for the research because of its excellent performance, flexible component splitting, and more importantly, it supports plug-in incremental compilation, multi-threaded scanning when generating routing tables, asynchronously loading routing tables at runtime, and callback ActivityResult. Much better than ARouter. Based on the principle that using a new framework will only be enough, I decided to understand the principles of the framework, and formulated the following questions for myself:

1. What is the design hierarchy of the framework?
2. How does it generate the routing table?
3. How does it load the routing table?
4. Compared with ARouter, how to improve the performance?

Read the official documentation

Compared with directly diving into the source code, it is always right to read the official documentation first. The official gave an introduction article, which is very well written and basically answered all my questions above.

First, I got the answers to questions 2, 3, and 4 in the highlight part of introducing DRouter.

The routing table is dynamically generated by plug-ins during compilation. The plug-in will start multi-threading and process all components asynchronously at the same time; the incremental scanning function can help developers only process the modified code when compiling for the second time, greatly shortening the time for generating the routing table.

In the compiler, use the gradle plug-in and transform to scan all classes, generate routing tables, and support incremental scanning, answering question 2.

In addition, when the framework is initialized, sub-threads are started to load the routing table, without blocking the execution of the main thread, and improving efficiency as much as possible.

Answered question 3.

Loading the routing table, instantiating routing, and distribution of cross-process commands after reaching the server should use reflection scenarios, use pre-occupied or dynamically generated code to replace java's new creation and explicit execution, to maximize To avoid reflective execution and improve performance.

Question 4 is answered, performance is improved by reducing the use of reflection.

In the principle and architecture chapter, a design diagram of the architecture is given:

The overall architecture is divided into three layers, from bottom to top are data flow layer, component layer, and open interface layer.

The data flow layer is the most important core module of DRouter, which carries the routing table generated by the plug-in, routing elements, dynamic registration, and serialized data flow related to cross-process functions. All routing flows will obtain the corresponding data from here, and then flow to the correct target.

RouterPlugin and MetaLoader are responsible for generating the routing table. The routing element refers to RouterMeta, which stores information such as scheme/host/path.

Component layer, core routing distribution, interceptor, life cycle, asynchronous temporary storage and monitoring, ServiceLoader, multi-dimensional filtering, Fragment routing, and cross-process command packaging, etc.

The open interface layer is some classes that come into contact with during use. The API is also designed to be very simple and easy to use. The DRouter class and the Request class only have 75 and 121 lines of code respectively.

Question 1 has been answered, and here we have an overall understanding of the entire framework.

read the source code

1. Initialization process

The sequence diagram after calling DRouter.init(app) is as follows:

The default is to load the routing table in the child thread without affecting the main thread.

    public static void checkAndLoad(final String app, boolean async) {
        if (!loadRecord.contains(app)) {
            // 双重校验锁
            synchronized (RouterStore.class) {
                if (!loadRecord.contains(app)) {
                    loadRecord.add(app);
                    if (!async) {
                        Log.d(RouterLogger.CORE_TAG, "DRouter start load router table sync");
                        load(app);
                    } else {
                        new Thread("drouter-table-thread") {
                            @Override
                            public void run() {
                                Log.d(RouterLogger.CORE_TAG, "DRouter start load router table in drouter-table-thread");
                                load(app);
                            }
                        }.start();
                    }
                }
            }
        }
    }

Finally, I came to the load method of RouterLoader to load the routing table into a map. Look carefully at its import path com.didi.drouter.loader.host.RouterLoader, it does not exist in the source code, because it is generated during compilation, and the location is located in app/build/intermediates/transforms /DRouter/dev/debug/…/com/didi/drouter/loader/host/RouterLoader.

public class RouterLoader extends MetaLoader {
    @Override
    public void load(Map var1) {
        var1.put("@@$$/browse/BrowseActivity", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("", "", "/browse/BrowseActivity", "com.example.demo.browse.BrowseActivity", (IRouterProxy)null, (Class[])null, (String[])null, 0, 0, false));
    }

    public RouterLoader() {
    }
}

public abstract class MetaLoader {

    public abstract void load(Map<?, ?> data);

    // for regex router
    protected void put(String uri, RouterMeta meta, Map<String, Map<String, RouterMeta>> data) {
        Map<String, RouterMeta> map = data.get(RouterStore.REGEX_ROUTER);
        if (map == null) {
            map = new ConcurrentHashMap<>();
            data.put(RouterStore.REGEX_ROUTER, map);
        }
        map.put(uri, meta);
    }

    // for service
    protected void put(Class<?> clz, RouterMeta meta, Map<Class<?>, Set<RouterMeta>> data) {
        Set<RouterMeta> set = data.get(clz);
        if (set == null) {
            set = Collections.newSetFromMap(new ConcurrentHashMap<RouterMeta, Boolean>());
            data.put(clz, set);
        }
        set.add(meta);
    }
}

It is not difficult to guess that a transform is added during the compilation period, and the specific implementation of the load method is added when the RouterLoader class is generated. Specifically, it is javaassit API+Gradle Transform, so let's take a look at what drouter-plugin does during the compilation period.

2. Transform during compilation

Look directly at the timing diagram.

Created a RouterPlugin and registered a Gradle Transform.

class RouterPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        ...
        project.android.registerTransform(new TransformProxy(project))
    }
}

class TransformProxy extends Transform {
        @Override
    void transform(TransformInvocation invocation) throws TransformException, InterruptedException, IOException {
        String pluginVersion = ProxyUtil.getPluginVersion(invocation)
        if (pluginVersion != null) {
            ...

            if (pluginJar.exists()) {
                URLClassLoader newLoader = new URLClassLoader([pluginJar.toURI().toURL()] as URL[], getClass().classLoader)
                Class<?> transformClass = newLoader.loadClass("com.didi.drouter.plugin.RouterTransform")
                ClassLoader threadLoader = Thread.currentThread().getContextClassLoader()
                // 1.设置URLClassLoader
                Thread.currentThread().setContextClassLoader(newLoader)
                Constructor constructor = transformClass.getConstructor(Project.class)
                // 2.反射创建一个RouterTransform
                Transform transform = (Transform) constructor.newInstance(project)
                transform.transform(invocation)
                Thread.currentThread().setContextClassLoader(threadLoader)
                return
            } else {
                ProxyUtil.Logger.e("Error: there is no drouter-plugin jar")
            }
        }
    }
}

Note 2 Reflection creates a com.didi.drouter.plugin.RouterTransform object and executes its transform method, where the transform logic is actually processed, and its location is located in the drouter-plugin module.

class RouterTransform extends Transform {
    @Override
    void transform(TransformInvocation invocation) throws TransformException, InterruptedException, IOException {
        ...
        // 1.创建一个DRouterTable目录
        File dest = invocation.outputProvider.getContentLocation("DRouterTable", TransformManager.CONTENT_CLASS,
                ImmutableSet.of(QualifiedContent.Scope.PROJECT), Format.DIRECTORY)
        // 2.执行RouterTask
        (new RouterTask(project, compilePath, cachePathSet, useCache, dest, tmpDir, setting, isWindow)).run()
        FileUtils.writeLines(cacheFile, cachePathSet)
        Logger.v("Link: https://github.com/didi/DRouter")
        Logger.v("DRouterTask done, time used: " + (System.currentTimeMillis() - timeStart) / 1000f  + "s")
    }
}

A RouterTask object is newly created in Note 2, and its run method is executed. The log output after that is the information that can be seen during normal compilation, indicating the time-consuming of transform.

public class RouterTask {
    void run() {
        StoreUtil.clear();
        JarUtils.printVersion(project, compileClassPath);
        pool = new ClassPool();
        // 1.创建ClassClassify
        classClassify = new ClassClassify(pool, setting);
        startExecute();
    }

    private void startExecute() {
        try {
            ...
            // 2.执行ClassClassify的generatorRouter
            classClassify.generatorRouter(routerDir);
            Logger.d("generator router table used: " + (System.currentTimeMillis() - timeStart) + "ms");
            Logger.v("scan class size: " + count.get() + " | router class size: " + cachePathSet.size());
        } catch (Exception e) {
            JarUtils.check(e);
            throw new GradleException("Could not generate d_router table\n" + e.getMessage(), e);
        } finally {
            executor.shutdown();
            FileUtils.deleteQuietly(wTmpDir);
        }
    }
}

The focus is on the ClassClassify class, whose generatorRouter method is the final processing logic for generating the routing table.

public class ClassClassify {
    private List<AbsRouterCollect> classifies = new ArrayList<>();

    public ClassClassify(ClassPool pool, RouterSetting.Parse setting) {
        classifies.add(new RouterCollect(pool, setting));
        classifies.add(new ServiceCollect(pool, setting));
        classifies.add(new InterceptorCollect(pool, setting));
    }

    public void generatorRouter(File routerDir) throws Exception {
        for (int i = 0; i < classifies.size(); i++) {
            AbsRouterCollect cf = classifies.get(i);
            cf.generate(routerDir);
        }
    }
}

RouterCollect// is added to the constructor , and their generate method is finally executed to process the routing table, service, and interceptor respectively. We only look at the routing table ServiceCollect.InterceptorCollect

class RouterCollect extends AbsRouterCollect {
    @Override
    public void generate(File routerDir) throws Exception {
        // 1.创建RouterLoader类
        CtClass ctClass = pool.makeClass(getPackageName() + ".RouterLoader");
        CtClass superClass = pool.get("com.didi.drouter.store.MetaLoader");
        ctClass.setSuperclass(superClass);

        StringBuilder builder = new StringBuilder();
        builder.append("public void load(java.util.Map data) {\n");
        for (CtClass routerCc : routerClass.values()) {
            try {
                // 处理注解、class类型等逻辑
                ...
                StringBuilder metaBuilder = new StringBuilder();
                metaBuilder.append("com.didi.drouter.store.RouterMeta.build(");
                metaBuilder.append(type);
                metaBuilder.append(").assembleRouter(");
                metaBuilder.append("\"").append(schemeValue).append("\"");
                metaBuilder.append(",");
                metaBuilder.append("\"").append(hostValue).append("\"");
                metaBuilder.append(",");
                metaBuilder.append("\"").append(pathValue).append("\"");
                metaBuilder.append(",");
                if ("com.didi.drouter.store.RouterMeta.ACTIVITY".equals(type)) {
                    if (!setting.isUseActivityRouterClass()) {
                        metaBuilder.append("\"").append(routerCc.getName()).append("\"");
                    } else {
                        metaBuilder.append(routerCc.getName()).append(".class");
                    }
                } else {
                    metaBuilder.append(routerCc.getName()).append(".class");
                }
                metaBuilder.append(", ");
                ...
                metaBuilder.append(proxyCc != null ? "new " + proxyCc.getName() + "()" : "null");
                metaBuilder.append(", ");
                metaBuilder.append(interceptorClass != null ? interceptorClass.toString() : "null");
                metaBuilder.append(", ");
                metaBuilder.append(interceptorName != null ? interceptorName.toString() : "null");
                metaBuilder.append(", ");
                metaBuilder.append(thread);
                metaBuilder.append(", ");
                metaBuilder.append(priority);
                metaBuilder.append(", ");
                metaBuilder.append(hold);
                metaBuilder.append(")");
                ...
                if (isAnyRegex) {
                    // 2. 插入路由表
                    items.add("    put(\"" + uri + "\", " + metaBuilder + ", data); \n");
                    //builder.append("    put(\"").append(uri).append("\", ").append(metaBuilder).append(", data); \n");
                } else {
                    items.add("    data.put(\"" + uri + "\", " + metaBuilder + "); \n");
                    //builder.append("    data.put(\"").append(uri).append("\", ").append(metaBuilder).append("); \n");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            Collections.sort(items);
            for (String item : items) {
                builder.append(item);
            }
            builder.append("}");

            Logger.d("\nclass RouterLoader" + "\n" + builder.toString());
            // 3.生成代码
            generatorClass(routerDir, ctClass, builder.toString());
        }
    }
}

There is a lot of logic here, but the overall is clear. After processing annotations and type judgments, obtaining routing information, constructing the code to be inserted, and finally processing the generation of the load method in the generatorClass of the parent class AbsRouterCollect, at this time the compiler The job is done.

ARouter also provides the arouter-register plug-in, which also generates routing tables during compilation. The difference is that when generating code, ARouter uses ASM, and DRouter uses Javassist. After checking the information, ASM performance is better than Javassist, but more Difficult to get started, you need to understand bytecode knowledge, Javassist provides a higher level of abstraction on complex bytecode-level operations, so it is easier and faster to implement, and only requires a little knowledge of bytecode, it Use reflection mechanism.

3. Load the routing table during runtime

Repost the load method for loading the routing table.

public class RouterLoader extends MetaLoader {
    @Override
    public void load(Map var1) {
        var1.put("@@$$/browse/BrowseActivity", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("", "", "/browse/BrowseActivity", "com.example.demo.browse.BrowseActivity", (IRouterProxy)null, (Class[])null, (String[])null, 0, 0, false));
    }

    public RouterLoader() {
    }
}

Look at the build method of RouteMeta.

public static RouterMeta build(int routerType) {
    return new RouterMeta(routerType);
}

It can be seen that it is a routing class that is directly new, which is different from ARouter directly creating routing classes through reflection, and has better performance.

private static void register(String className) {
    if (!TextUtils.isEmpty(className)) {
        try {
            // 1.反射创建路由类
            Class<?> clazz = Class.forName(className);
            Object obj = clazz.getConstructor().newInstance();
            if (obj instanceof IRouteRoot) {
                registerRouteRoot((IRouteRoot) obj);
            } else if (obj instanceof IProviderGroup) {
                registerProvider((IProviderGroup) obj);
            } else if (obj instanceof IInterceptorGroup) {
                registerInterceptor((IInterceptorGroup) obj);
            } else {
                logger.info(TAG, "register failed, class name: " + className
                        + " should implements one of IRouteRoot/IProviderGroup/IInterceptorGroup.");
            }
        } catch (Exception e) {
            logger.error(TAG,"register class error:" + className, e);
        }
    }
}

4. Summary

This article analyzes the principle of the routing part of DRouter. It uses Gradle Transform and Javassist to generate routing tables in the compiler, new routing classes at runtime, and asynchronously initializes and loads routing tables to achieve high performance.


In order to help everyone better grasp all the knowledge points in Android componentization, the "Android Architecture Learning Manual" + "In-depth Understanding of the Gradle Framework" study notes are compiled below, based on some notes made in my own study, mainly It is also convenient for follow-up review and reading, saving the time of searching on the Internet, so as not to step on the pit, if you need it, you can directly click here ↓↓↓ for reference study ​qr21.cn/CaZQLo?BIZ=ECOMMERCE
:

Android Architecture Study Manual

In-depth understanding of the Gradle framework

insert image description here

Guess you like

Origin blog.csdn.net/maniuT/article/details/131142508