preface

DRouter is a set of Android routing framework developed by Didi Passenger side. Based on the idea of platform decoupling, DRouter serves for communication between components. Based on the principle of comprehensive function and ease of use, the project supports various routing scenarios and provides diversified services in page routing, service acquisition and filtering, cross-process and cross-application, VirtualApk plug-in support and other aspects. At present, it has been used in more than a dozen Didi apps, such as Didi Passenger Terminal, Hitch, Bike, internationalization, Didi customized car central control, And Didi on-board screen, and has been verified in various scenarios.

Access mode and usage

Access to the

Project address on Github: github.com/didi/DRoute…

Add plugin dependencies under build.gradle in the project root directory:

buildscript {    
	dependencies {        
		classpath "IO. Making. Didi: drouter plugin - proxy: 1.0.1." "}}Copy the code

Apply the plugin to the build.gradle file in the main Module:

plugins {
    id 'com.didi.drouter'
}
Copy the code

Add dependencies to dependencies in the build.gradle file of the main Module:

dependencies {
		api "IO. Making. Didi: drouter - API: 2.1.0." "
}
Copy the code

After successful introduction, we need to initialize it in Application:

DRouter.init(application);
Copy the code

Method of use

For details, please refer to the official document: github.com/didi/DRoute…

Activity, Fragment, View

Static registration

Activities, Fragments, and Views only support static jumps. Register routing addresses in the Activity:

@Router(scheme = "didi", host = "router", path = "/test1")
class TestActivity1 : AppCompatActivity(a)Copy the code
Initiating a Page redirect
            DRouter.build("didi://router/test1")
                .putExtra("tag".this.javaClass.name)
                .start(this.object : RouterCallback.ActivityCallback() {
                    / / the Activity callback
                    override fun onActivityResult(resultCode: Int.data: Intent?).{}})Copy the code

Handler

The typical application scenarios of the Handler are as follows:

  1. Call native code out of end, Push and H5
  2. Componentized communication without sinking interface, data return capability
  3. Implement enhanced EventBus

Handler supports both static and dynamic registration modes.

Static registration
@Router(scheme = "didi", host = "router", path = "/sendOrder", thread = Thread.WORKER)   // Specify the thread of execution
public class SendOrderHandler implements IRouterHandler {
    @Override
    void handle(@NonNull Request request, @NonNull Result result);
        // result can add data and will be returned to the caller
        // If you need to intercept all nodes after the interception can be executed, the default is not interceptionrequest.getInterceptor().onInterrupt(); }}Copy the code
Dynamic definition and registration

If you register a listener internally, it will not be re-instantiated, similar to EventBus, which has the ability to return data. If you register with lifecycleOwner, it will be unregistered automatically

// Dynamic registration
IRegister register = 
       DRouter.register(
              RouterKey.build("/dynamic/handler").setLifecycleOwner(lifecycleOwner), 
              new IRouterHandler() {
                     @Override
                     public void handle(@NonNull Request request, @NonNull Result result) {}});// Unregister, if the life cycle is used, save
register.unregister();
Copy the code
A navigation
DRouter.build("didi://router/sendOrder")
       .setHoldTimeout(3000)         // Set the automatic callback if no response is returned within the timeout period
       .start(context, new RouterCallback() {
            @Override
            public void onResult(@NonNull Result result) {
                // Only after the target is released will the callback be made and the thread will not be blocked}});Copy the code

Hold

Normally, Result in the RouterCallback is returned immediately after the Activity is started and the Handle method of the RouterHandler completes. If you want the Result in the RouterCallback to wait for the target to fire before returning the Result without blocking the current thread, consider holding the Result temporarily

Application scenarios
  • Launch the login page and wait for the user to log in successfully
  • The client cold starts the App and waits for MainActivity’s onResume callback or something ready to callback
  • RouterHandler has a time-consuming task that it wants to return to the caller without blocking it
configuration

Add the hold parameter to the Activity and RouterHandler to enable HoldResult

Asynchronous Activity
@Router(scheme = "didi", host = "router", path = "/login" hold = "true")
public class ActivityLogin extends Activity {

    @Override
    protected void onCreate(a) {
        super.onCreate();
        Request request = RouterHelper.getRequest(this);
        Result result = RouterHelper.getResult(this);

        // Trigger this release at some point in time, depending on the business (must have)
        RouterHelper.release(this); }}Copy the code
Asynchronous RouterHandler
@Router(scheme = "didi", host = "router", path = "/sendOrder", hold = "true")
public class SendOrderHandler implements IRouterHandler {
    @Override
    void handle(@NonNull Request request, @NonNull Result result);
        
        // Trigger this release at some point in time, depending on the business (must have)RouterHelper.release(request); }}Copy the code
A navigation
DRouter.build("didi://router/sendOrder")
       .setHoldTimeout(3000)         // Set the automatic callback if no response is returned within the timeout period
       .start(context, new RouterCallback() {
            @Override
            public void onResult(@NonNull Result result) {
                // Only after the target is released will the callback be made and the thread will not be blocked}});Copy the code

Interceptor

Interceptors can perform some actions when opening a specified page, such as:

  • The target page requires certain permissions, for example, to determine whether a user is logged in in some scenarios
  • The target page needs to do something first, such as turn on the location function
Defining interceptors

A global interceptor means that any request will be executed

@interceptor (name = "interceptor1", // optional, reference to priority = 1, // optional, priority = 1, global = true)                  // Optional, whether global interceptor
public class LoginInterceptor implements IRouterInterceptor {
    @Override
    public void handle(@NonNull Request request) {

        if (isGo?) {
            request.getInterceptor().onContinue();
        } else{ request.getInterceptor().onInterrupt(); }}}Copy the code
Apply to the page
@Router(path = "/main", interceptor = LoginInterceptor.class)
public class MainActvity extends Activity {}
Copy the code

Source code analysis

Initialization Process

The above is the initialization flowchart of DRouter. The project structure of DRouter is as follows:

  • Drouter-api: directly invoked by the business layer
  • Drouter-api-stub: Defines MetaLoader and its implementation classes, generating routing table code at compile time
  • Drouter-plugin: The plug-in that actually performs the drouter build process, executes the RouterTask to create the actual implementation of the RouterLoader and handle annotations and load the routing table.
  • Drouter-plugin-proxy is a Gradle Task that pulls the latest version of the drouter-plugin-.jar package

Drouter-api is the module that is called directly by the business layer. In the API module, it depends on drouter-api-stub:

    compileOnly project(':drouter-api-stub')
Copy the code

You can use this method in your own moudle to rely on common libraries such as com.android.support and gson to avoid conflicts.

Drouter-api-stub defines MetaLoader and its implementation class: RouterLoader, ServiceLoader, and InterceptorLoader are all empty implementations. At compile time, methods are implemented using the JavaAssit API and Gradle Transform API.

Drouter-plugin-proxy = drouter-plugin-proxy = build.gradle

classpath 'IO. Making. Didi: drouter - plugin - proxy: 1.0.1'
Copy the code

Drouter-plugin-proxy is a Gradle Task that pulls the latest version of the drouter-plugin-.jar package

Inside the drouter-Plugin is the plug-in that actually performs the drouter build process, executing the RouterTask to create the actual implementation of the RouterLoader and handle annotations and load the routing table.

So the DRouter initialization can be analyzed in both compile and run phases.

Compile time

During project compilation by Transform, all class classes can be obtained and processed during the compilation of class to dex. DRouter uses the Groovy plugin + Transform API to automatically generate the routing table at compile time. First, we create a RouterPlugin in the drouter-plugin-proxy Module:

class RouterPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        ProxyUtil.confirm(project)
        project.extensions.create('drouter', RouterSetting)
        project.android.registerTransform(new TransformProxy(project))
    }
}
Copy the code

The RouterPlugin finally registers a TransformProxy:

class TransformProxy extends Transform {
    // ... 
  	@Override
    void transform(TransformInvocation invocation) throws TransformException, 
  			InterruptedException, IOException {
        // Plugin version
        String pluginVersion = ProxyUtil.getPluginVersion(invocation)
        if(pluginVersion ! =null) {
            File pluginJar = new File(project.rootDir, ".gradle/drouter/drouter-plugin-${pluginVersion}.jar")
            if(! pluginJar.exists()) {// If it does not exist, try downloading
                // ...
            }
            if (pluginJar.exists()) {
                URLClassLoader newLoader = newURLClassLoader([pluginJar.toURI().toURL()] as URL[], getClass().classLoader) Class<? > transformClass = newLoader.loadClass("com.didi.drouter.plugin.RouterTransform") / / 【 1 】
                ClassLoader threadLoader = Thread.currentThread().getContextClassLoader()
                Thread.currentThread().setContextClassLoader(newLoader)
                Constructor constructor = transformClass.getConstructor(Project.class)
                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")}}else {
            ProxyUtil.Logger.e("Error: there is no drouter-plugin version")
        }
        copyFile(invocation)
    }
}
Copy the code

TransformProxy on download the latest drouter – plugin plug-ins, and in [1], created a com. Didi. Drouter. Plugin. RouterTransform object, and perform the transform (invocation), Look at the code for the RouterTransform:

class RouterTransform extends Transform {
    @Override
    void transform(TransformInvocation invocation) throws TransformException, InterruptedException, IOException {
        long timeStart = System.currentTimeMillis()
        SystemUtil.debug = project.drouter.debug
        File cacheFile = new File(tmpDir, "cache")
        boolean configChanged = SystemUtil.configChanged(project)
        booleanuseCache = ! isWindow && invocation.incremental && project.drouter.cache && cacheFile.exists() && !configChanged
        if (useCache) {
            cachePathSet.addAll(cacheFile.readLines())
        }
        if(! invocation.incremental) { invocation.outputProvider.deleteAll() } compilePath =new ConcurrentLinkedQueue<>(project.android.bootClasspath)
        for (TransformInput transformInput : invocation.inputs) {
            handleDirectory(invocation, transformInput)
            handleJar(invocation, transformInput)
        }
        File dest = invocation.outputProvider.getContentLocation("DRouterTable", TransformManager.CONTENT_CLASS,
                ImmutableSet.of(QualifiedContent.Scope.PROJECT), Format.DIRECTORY) / / [2]
        (new RouterTask(project, compilePath, cachePathSet, useCache, dest, tmpDir, project.drouter, isWindow)).run()	/ / [3]
        FileUtils.writeLines(cacheFile, cachePathSet)
        Logger.v("Link: https://github.com/didi/DRouter")
        Logger.v("DRouterTask done, time used: " + (System.currentTimeMillis() - timeStart) / 1000f  + "s")}}Copy the code

[2] creates a DRouterTable directory for RouterTask to use, and finally executes the RouterTask’s run method:

    void run(a) {
        StoreUtil.clear();
        JarUtils.printVersion(project, compileClassPath);
        pool = new ClassPool();
        classClassify = new ClassClassify(pool, setting);
        startExecute();
    }
    
    private void startExecute(a) {
        try {
            long timeStart = System.currentTimeMillis();
            for (File file : compileClassPath) {
                pool.appendClassPath(file.getAbsolutePath());
            }
            if (useCache) {
                loadCachePaths(cachePathSet);
            } else {
                loadFullPaths(compileClassPath);
            }
            timeStart = System.currentTimeMillis();
            classClassify.generatorRouter(routerDir); / / [4]
        } catch (Exception e) {
            JarUtils.check(e);
            String message = e.getMessage();
            if (message == null| |! message.startsWith("Class:")) {
                e.printStackTrace();
            }
            throw new GradleException("DRouter: Could not generate router table\n" + e.getMessage());
        } finally{ executor.shutdown(); FileUtils.deleteQuietly(wTmpDir); }}Copy the code

[4] generatorRouter with ClassClassify

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

AbsRouterCollect has three implementation classes:

  • RouterCollect
  • InterceptorCollect
  • ServiceCollect

ClassClassify adds these three at initialization:

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

    public ClassClassify(ClassPool pool, RouterSetting setting) {
        // Three AbsRouterCollect types are added at initialization
        classifies.add(new RouterCollect(pool, setting));
        classifies.add(new ServiceCollect(pool, setting));
        classifies.add(new InterceptorCollect(pool, setting));
    }
		// ...
}
Copy the code

Taking RouterCollect as an example, The generate method of RouterCollect can be realized as follows:

    @Override
    public void generate(File routerDir) throws Exception {
      	Create the RouterLoader class
        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 {
                StringBuilder interceptorClass = null;
                StringBuilder interceptorName = null;

                String uriValue = "";
                String schemeValue = "";
                String hostValue = "";
                String pathValue = "";
                Annotation annotation = null;
                String type;
                int thread = 0;
                int priority = 0;
                boolean hold = false;
                if (routerCc.hasAnnotation(Router.class)) {
                    uriValue = ((Router) routerCc.getAnnotation(Router.class)).uri();
                    schemeValue = ((Router) routerCc.getAnnotation(Router.class)).scheme();
                    hostValue = ((Router) routerCc.getAnnotation(Router.class)).host();
                    pathValue = ((Router) routerCc.getAnnotation(Router.class)).path();
                    thread = ((Router) routerCc.getAnnotation(Router.class)).thread();
                    priority = ((Router) routerCc.getAnnotation(Router.class)).priority();
                    hold = ((Router) routerCc.getAnnotation(Router.class)).hold();
                    annotation = getAnnotation(routerCc, Router.class);
                    if (checkSuper(routerCc, "android.app.Activity")) {
                        type = "com.didi.drouter.store.RouterMeta.ACTIVITY";
                    } else if (checkSuper(routerCc,
                            "android.support.v4.app.Fragment"."androidx.fragment.app.Fragment")) {
                        type = "com.didi.drouter.store.RouterMeta.FRAGMENT";
                    } else if (checkSuper(routerCc, "android.view.View")) {
                        type = "com.didi.drouter.store.RouterMeta.VIEW";
                    } else if (checkSuper(routerCc, "com.didi.drouter.router.IRouterHandler")) {
                        type = "com.didi.drouter.store.RouterMeta.HANDLER";
                    } else {
                        throw new Exception("@Router target class illegal, " +
                                "support only Activity/Fragment/View/IRouterHandler"); }}else {
                    pathValue = "/" + routerCc.getName().replace("."."/");
                    type = "com.didi.drouter.store.RouterMeta.ACTIVITY";
                }
                // ... 
              	if (isAnyRegex) {
                    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());
        generatorClass(routerDir, ctClass, builder.toString());
    }
Copy the code

In the project home directory (typically app, here is the official demo) under the demo/build/intermediates/transforms/DRouter/debug /… To view the generated class:

public class RouterLoader extends MetaLoader {
    @Override
    public void load(Map var1) {
        var1.put("@@$$/activity/dynamic", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("".""."/activity/dynamic"."com.didi.demo.handler.DynamicActivity", (IRouterProxy)null, (Class[])null, (String[])null.0.0.false));
        var1.put("@@$$/activity/remote", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("".""."/activity/remote"."com.didi.demo.remote.RemoteActivity", (IRouterProxy)null, (Class[])null, (String[])null.0.0.false));
        var1.put("@@$$/activity/result", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("".""."/activity/result"."com.didi.demo.activity.ActivityResultActivity", (IRouterProxy)null, (Class[])null, (String[])null.0.0.false));
        var1.put("@@$$/activity/router_page_single", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("".""."/activity/router_page_single"."com.didi.demo.fragment.RouterPageSingleActivity", (IRouterProxy)null, (Class[])null, (String[])null.0.0.false));
        var1.put("@@$$/activity/router_page_stack", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("".""."/activity/router_page_stack"."com.didi.demo.fragment.RouterPageStackActivity", (IRouterProxy)null, (Class[])null, (String[])null.0.0.false));
        var1.put("@@$$/activity/router_page_viewpager", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("".""."/activity/router_page_viewpager"."com.didi.demo.fragment.RouterPageViewPagerActivity", (IRouterProxy)null, (Class[])null, (String[])null.0.0.false));
        var1.put("@@$$/activity/test3", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("".""."/activity/test3"."com.didi.demo.activity.ActivityTest3", (IRouterProxy)null, (Class[])null, (String[])null.0.0.true));
        var1.put("@@$$/activity/webview", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("".""."/activity/webview"."com.didi.demo.web.WebActivity", (IRouterProxy)null, (Class[])null, (String[])null.0.0.false));
        var1.put("@@$$/fragment/second", RouterMeta.build(RouterMeta.FRAGMENT).assembleRouter("".""."/fragment/second", FragmentSecond.class, new com_didi_demo_fragment_FragmentSecond(), (Class[])null, (String[])null.0.0.false));
        var1.put("@@$$/handler/test1", RouterMeta.build(RouterMeta.HANDLER).assembleRouter("".""."/handler/test1", HandlerTest1.class, new com_didi_demo_handler_HandlerTest1(), (Class[])null, (String[])null.0.1.false));
        var1.put("@@$$/handler/test3", RouterMeta.build(RouterMeta.HANDLER).assembleRouter("".""."/handler/test3", HandlerTest3.class, new com_didi_demo_handler_HandlerTest3(), (Class[])null, (String[])null.2.0.true));
        var1.put("@@$$/view/bottom", RouterMeta.build(RouterMeta.VIEW).assembleRouter("".""."/view/bottom", BottomView.class, new com_didi_demo_view_BottomView(), (Class[])null, (String[])null.0.0.false));
        var1.put("@@$$/view/headview", RouterMeta.build(RouterMeta.VIEW).assembleRouter("".""."/view/headview", HeadView.class, new com_didi_demo_view_HeadView(), (Class[])null, (String[])null.0.0.false));
        var1.put("didi@@router$$/handler/test2", RouterMeta.build(RouterMeta.HANDLER).assembleRouter("didi"."router"."/handler/test2", HandlerTest2.class, new com_didi_demo_handler_HandlerTest2(), (Class[])null, (String[])null.0.0.false));
        this.put("@@$$/activity/Test1_<Arg1>_<Arg2>", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("".""."/activity/Test1_<Arg1>_<Arg2>"."com.didi.demo.activity.ActivityTest1", (IRouterProxy)null.new Class[]{InnerInterceptor.class}, new String[]{"interceptor1"."interceptor2"}, 0.0.false), var1);
        this.put("@@$$/fragment/first/.*", RouterMeta.build(RouterMeta.FRAGMENT).assembleRouter("".""."/fragment/first/.*", FragmentFirst.class, new com_didi_demo_fragment_FragmentFirst(), (Class[])null, (String[])null.0.0.false), var1);
        this.put("@@$$/handler/.*", RouterMeta.build(RouterMeta.HANDLER).assembleRouter("".""."/handler/.*", HandlerAll.class, new com_didi_demo_handler_HandlerAll(), (Class[])null, (String[])null.0.2.false), var1);
        this.put("didi@@www\\.didi\\.com$$/activity/test2", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("didi"."www\\.didi\\.com"."/activity/test2"."com.didi.demo.activity.ActivityTest2", (IRouterProxy)null, (Class[])null, (String[])null.0.0.false), var1);
    }

    public RouterLoader(a) {}}Copy the code

! [image-20211108182222660](/Users/chunyu/Library/Application Support/typora-user-images/image-20211108182222660.png)

The Load method of the RouterLoader was modified at compile time to add annotated routing information to the routing table.

The run-time

Starting with the use of DRouter, let’s first look at what DRouter does when it initializes:

    public static void init(Application application) {
        SystemUtil.setApplication(application);	// Store application in a static variable
        RouterStore.checkAndLoad(RouterStore.HOST, true); // 【5】RouterStore.HOST = "host"
    }
Copy the code

See the implementation of part [5] :

    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.NAME,"DRouter start load router table sync");
                        load(app);
                    } else {
                        new Thread("drouter-table-thread") {
                            @Override
                            public void run(a) {
                                Log.d(RouterLogger.NAME, "DRouter start load router table in drouter-table-thread"); load(app); } }.start(); }}}}Copy the code

The app parameter is the unique ID of the APP. If it exists in the loading record, the routing table will not be reloaded. The async parameter controls whether the routing table is loaded synchronously or asynchronously. The real routing method is in load(app) :

    // class RouterStore
		private static void load(String app) {
        boolean load;
        if (HOST.equals(app)) {  / / HOST = "HOST"
            load = loadHostTable(); 	/ / [6]
            initialized = true;
            latch.countDown();
        } else {
          	/ / 【 7 】
            load = loadPluginTable(app,
                    Pair.create("Router", routerMetas),
                    Pair.create("Interceptor", interceptorMetas),
                    Pair.create("Service", serviceMetas));
        }
        if(! load) { RouterLogger.getCoreLogger().e("DRouterTable in app \"%s\" not found, " +
                            "please apply drouter plugin first.", app); }}Copy the code

When the passed parameter app is equal to HOST, the logic for loading the routing table is loadHostTable(); otherwise, it is the method at [7]. Analysis one by one:

    private static boolean loadHostTable(a) {
        try {
            new RouterLoader().load(routerMetas);
            new InterceptorLoader().load(interceptorMetas);
            new ServiceLoader().load(serviceMetas);
        }  catch (NoClassDefFoundError e) {
            return false;
        }
        return true;
    }
Copy the code

Another logic for loading routing tables:

	private static boolean loadPluginTable(String packageName, Pair... targets) {
        try {
            for(Pair<String, Map<? ,? >> target : targets) {// Build MetaLoader by reflection
                MetaLoader loader = (MetaLoader) ReflectUtil.getInstance(
                        Class.forName(String.format("com.didi.drouter.loader.%s.%sLoader",
                                packageName, target.first))
                );
                assertloader ! =null; loader.load(target.second); }}catch (ClassNotFoundException e) {
            return false;
        }
        return true;
    }
Copy the code

Both methods end up calling loader.load(), which automatically generates all the annotated content into the method at compile time, and the entire DRouter initialization ends there.

Dynamically adding a routing table

The Handler supports dynamic adding of routing tables, and the loadHostTable parameters are related to dynamic loading:

    // Key is uriKey, value is meta, with dynamic
    // Key is REGEX_ROUTER, value is map
      ,>
    private static final Map<String, Object> routerMetas = new ConcurrentHashMap<>();
    // key is interceptor impl or string name
    private static Map<Object, RouterMeta> interceptorMetas = new ConcurrentHashMap<>();
    // Key is interface, value is set, with dynamic
    private static finalMap<Class<? >, Set<RouterMeta>> serviceMetas =new ConcurrentHashMap<>();
Copy the code

Another way to add routerMetas to the register method is:

    // Register a dynamic handler. The key is the routerKey and the value is the handler instance
		@NonNull
    @MainThread
    public synchronized static IRegister register(final RouterKey key, final IRouterHandler handler) {
        / /...
      	// Build meta dataRouterMeta meta = RouterMeta.build(RouterMeta.HANDLER).assembleRouter( key.uri.getScheme(), key.uri.getHost(), key.uri.getPath(), (Class<? >)null.null, key.interceptor, key.interceptorName,
                key.thread, 0, key.hold);
        meta.setHandler(key, handler);
      	// If regular expressions or placeholders are included
        if (meta.isRegexUri()) {
            Map<String, RouterMeta> regexMap = (Map<String, RouterMeta>) routerMetas.get(REGEX_ROUTER);
            if (regexMap == null) {
                regexMap = new ConcurrentHashMap<>();
                routerMetas.put(REGEX_ROUTER, regexMap);
            }
            regexMap.put(TextUtils.getStandardRouterKey(key.uri), meta);
        } else {
            routerMetas.put(TextUtils.getStandardRouterKey(key.uri), meta);
        }
      	// Unregister an object with a binding lifecycle when it is destroyed
        if(key.lifecycleOwner ! =null) {
            key.lifecycleOwner.getLifecycle().addObserver(new LifecycleObserver() {
                @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
                public void onDestroy(a) { unregister(key, handler); }}); }return new RouterRegister(key, handler);
    }
Copy the code

The serviceMetas register method is also used to add data. The difference is that generics are supported:

  	// Register dynamic services where key is serviceKey and value is the service instance
		@NonNull
    @MainThread
    public synchronized static <T> IRegister register(final ServiceKey<T> key, final T service) {
        // ... 
        RouterMeta meta = RouterMeta.build(RouterMeta.SERVICE).assembleService(
                null.null,
                key.alias, key.feature, 0, Extend.Cache.NO);
        meta.setService(key, service);
        key.meta = meta;
        Set<RouterMeta> metas = serviceMetas.get(key.function);
        if (metas == null) {
            metas = Collections.newSetFromMap(new ConcurrentHashMap<RouterMeta, Boolean>());
            serviceMetas.put(key.function, metas);
        }
        metas.add(meta);
        if(key.lifecycleOwner ! =null) {
            key.lifecycleOwner.getLifecycle().addObserver(new LifecycleObserver() {
                @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
                public void onDestroy(a) { unregister(key, service); }}); }return new RouterRegister(key, service);
    }
Copy the code

InterceptorMetas doesn’t add logic dynamically, because interceptors aren’t registered dynamically, just like activities/fragments/Views.

Calling process

The call method is:

DRouter.build(scheme).start(context);
Copy the code

The build method:

    /**
     * Navigation to router
     * there will be only one activity match at most, but may be several router handler.
     * @param uri String
     * @return request
     */
    @NonNull
    public static Request build(String uri) {
        return Request.build(uri);
    }
Copy the code

The build method returns a Request object. Look at Request.build(URI); Implementation logic:

    public static Request build(String uri) {
        return new Request(uri == null ? Uri.EMPTY : Uri.parse(uri));
    }
Copy the code

Request is constructed as follows:

    private Request(@NonNull Uri uri) {
        this.uri = uri;
        this.serialNumber = String.valueOf(counter.getAndIncrement());
        putExtra(REQUEST_BUILD_URI, uri.toString());
    }
Copy the code

Then look at the Request start method:

    public void start(Context context) {
        start(context, null);
    }

    public void start(Context context, RouterCallback callback) {
        this.context = context == null ? DRouter.getContext() : context;
        RouterLoader.build(this, callback).start(); / / [8]
    }
Copy the code

The real logic lies in [8], where we first look at the Build method of the RouterLoader:

    @NonNull
    static RouterLoader build(Request request, RouterCallback callback) {
        RouterLoader loader = new RouterLoader();
        loader.primaryRequest = request; / / save the request
        loader.callback = callback;			 / / save the callback
        return loader;
    }
Copy the code

The Build method of the RouterLoader returns a loader object. Look at its start method:

    void start(a) {
        if(! TextUtils.isEmpty(primaryRequest.authority)) { startRemote(); }else{ startLocal(); }}Copy the code

Determine whether the request is a remote or local call based on whether the authority property is empty. The value of authority is the ContentProvider permission of the remote process. This can be set when the jump logic is called:

            DRouter.build("/handler/test1")
                    .putExtra("1".1)
                    .putExtra("2".new Bundle())
                    .putAddition("3".new ParamObject())
                    .setRemoteAuthority("com.didi.drouter.remote.demo.host")
                    .start(DRouter.getContext());
Copy the code

Two key methods: startRemote and startLocal

startLocal
    private void startLocal(a) {
        TextUtils.appendExtra(primaryRequest.getExtra(), TextUtils.getQuery(primaryRequest.getUri()));
        // In order, the priority is first, then the exact match is first
        Map<Request, RouterMeta> requestMap = makeRequest(); / / [9]
        if (requestMap.isEmpty()) {
            RouterLogger.getCoreLogger().w("warning: there is no request target match");
            new Result(primaryRequest, null, callback);
            ResultAgent.release(primaryRequest, ResultAgent.STATE_NOT_FOUND);
            return;
        }
        final Result result = new Result(primaryRequest, requestMap.keySet(), callback);
        if (requestMap.size() > 1) {
            RouterLogger.getCoreLogger().w("warning: request match %s targets", requestMap.size());
        }
        final boolean[] stopByRouterTarget = {false};
      	/ / traverse the request
        for (final Map.Entry<Request, RouterMeta> entry : requestMap.entrySet()) {
            if (stopByRouterTarget[0]) {
                // one by one
                ResultAgent.release(entry.getKey(), ResultAgent.STATE_STOP_BY_ROUTER_TARGET);
                continue;
            }
          	// Interceptors take precedence
            InterceptorHandler.handle(entry.getKey(), entry.getValue(), new IRouterInterceptor.IInterceptor() {
                @Override
                public void onContinue(a) {
                    entry.getKey().interceptor = new IRouterInterceptor.IInterceptor() {
                        @Override
                        public void onContinue(a) {}@Override
                        public void onInterrupt(a) {
                            RouterLogger.getCoreLogger().w(
                                    "request \"%s\" stop all remains requests", entry.getKey().getNumber());
                            stopByRouterTarget[0] = true; }};// [10] it will release branch auto when no hold or no target
                    RouterDispatcher.start(entry.getKey(), entry.getValue(), result, callback);
                    entry.getKey().interceptor = null;
                }

                @Override
                public void onInterrupt(a) { ResultAgent.release(entry.getKey(), ResultAgent.STATE_STOP_BY_INTERCEPTOR); }}); }}Copy the code

[9] Sort requests by order and priority.

    private Map<Request, RouterMeta> makeRequest(a) {
        Map<Request, RouterMeta> requestMap = new LinkedHashMap<>();
        // Get serialized data from the intent
        Parcelable parcelable = primaryRequest.getParcelable(Extend.START_ACTIVITY_VIA_INTENT);
        if (parcelable instanceof Intent) { // intent
            primaryRequest.getExtra().remove(Extend.START_ACTIVITY_VIA_INTENT); // Delete additional information
            Intent intent = (Intent) parcelable;
            PackageManager pm = primaryRequest.getContext().getPackageManager();
            List<ResolveInfo> activities = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);  // Find the matching activities
            if(! activities.isEmpty()) {// A matching activity exists
                primaryRequest.routerType = RouterType.ACTIVITY;    // Set the request type to ACTIVITY
                requestMap.put(this.primaryRequest, RouterMeta.build(RouterType.ACTIVITY).assembleRouter(intent));   // Build a RouterMeta and store it in the map}}else {
            List<RouterMeta> metas = getAllRouterMetas();   // Get all routing table information
            int index = 0;
            for (RouterMeta routerMeta : metas) {   // Start traversal
                // Inject placeholder keys and values into the bundle
                if(! routerMeta.injectPlaceHolder(primaryRequest.getUri(), primaryRequest.extra)) {continue;
                }
                / / build request
                Request request = createBranchRequest(
                        this.primaryRequest, metas.size() > 1, routerMeta.getRouterType(), index++); requestMap.put(request, routerMeta); }}return requestMap;
    }
Copy the code

It then iterates through the request collection, executes the request interceptor, and dispatches the request through [10] RouterDispatcher:

class RouterDispatcher {
		static void start(Request request, RouterMeta meta, Result result, RouterCallback callback) {
        switch (meta.getRouterType()) {
            case RouterType.ACTIVITY:
                startActivity(request, meta, result, callback);
                break;
            case RouterType.FRAGMENT:
                startFragment(request, meta, result);
                break;
            case RouterType.VIEW:
                startView(request, meta, result);
                break;
            case RouterType.HANDLER:
                startHandler(request, meta, result, callback);
                break;
            default:
                break; }}}Copy the code

As you can see, only activities and handlers support callback.

    private static void startActivity(Request request, RouterMeta meta, Result result, RouterCallback callback) {
       	// ...
        boolean hasRequestCode = request.getExtra().containsKey(Extend.START_ACTIVITY_REQUEST_CODE);
        int requestCode = hasRequestCode? request.getInt(Extend.START_ACTIVITY_REQUEST_CODE) : 1024;
        if (context instanceof Activity && callback instanceof RouterCallback.ActivityCallback) { / / [11]
            ActivityCompat2.startActivityForResult((Activity) context, intent,
                    requestCode, (RouterCallback.ActivityCallback) callback);
        } else if (context instanceof Activity && hasRequestCode) {		/ / [12]
            ActivityCompat.startActivityForResult((Activity) context, intent,
                    requestCode, intent.getBundleExtra(Extend.START_ACTIVITY_OPTIONS));
        } else {	/ / [13]
            ActivityCompat.startActivity(context, intent, intent.getBundleExtra(Extend.START_ACTIVITY_OPTIONS));
        }
      	// Process the page animation
        int[] anim = request.getIntArray(Extend.START_ACTIVITY_ANIMATION); 
        if (context instanceofActivity && anim ! =null && anim.length == 2) {
            ((Activity) context).overridePendingTransition(anim[0], anim[1]);
        }
        result.isActivityStarted = true;
      	/ / hold processing
        if(meta.isHold() && callback ! =null) {
            RouterLogger.getCoreLogger().w("request \"%s\" will be hold", request.getNumber());
            Monitor.startMonitor(request, result); / / [14]
        } else{ ResultAgent.release(request, ResultAgent.STATE_COMPLETE); }}Copy the code

Here [11], [12], [13], calls the different methods of the Activity start, when the callback types for RouterCallback. ActivityCallback, Through custom tools ActivityCompat2. StartActivityForResult started, 12, 13 is called system API startActivity directly:

    static void startActivityForResult(@NonNull final Activity activity,
                                       @NonNull final Intent intent, final int requestCode,
                                       RouterCallback.ActivityCallback callback) {
        final int cur = sCount.incrementAndGet();
        sCallbackMap.put(cur, new Pair<>(new WeakReference<>(activity), callback));
        final Active active;
        if (activity instanceof FragmentActivity) { // Create a Fragment and bind it to the activity
            active = new HolderFragmentV4();
        } else {
            active = new HolderFragment();
        }
        active.getCompat().cur = cur;
        active.attach(activity, intent, requestCode); / / [15]
    }
Copy the code

If the Activity is FragmentActivity, create HolderFragmentV4, otherwise HolderFragment. Both implement the Active interface and call attach:

// public static class HolderFragmentV4 extends Fragment implements Active
        @Override
        public void attach(Activity activity, Intent intent, int requestCode) {
            this.intent = intent;
            this.requestCode = requestCode;
            FragmentManager fragmentManager = ((FragmentActivity)activity).getSupportFragmentManager();
            FragmentTransaction transaction = fragmentManager.beginTransaction();
            transaction.add(this, TAG);
            transaction.commit(); // add fragment
        }
// public static class HolderFragment extends android.app.Fragment implements Active
        @Override
        public void attach(Activity activity, Intent intent, int requestCode) {
            this.intent = intent;
            this.requestCode = requestCode;
            android.app.FragmentManager fragmentManager = activity.getFragmentManager();
            android.app.FragmentTransaction transaction = fragmentManager.beginTransaction();
            transaction.add(this, TAG);
            transaction.commit();
        }
Copy the code

The life cycles of both fragment types can call the corresponding method on ActivityCompat2:

        @Override
        public void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            activityCompat2.onCreate(savedInstanceState);
        }

        @Override
        public void onActivityResult(int requestCode, int resultCode, Intent data) {
            super.onActivityResult(requestCode, resultCode, data);
            activityCompat2.onActivityResult(getActivity(), resultCode, data);
        }

        @Override
        public void onSaveInstanceState(@NonNull Bundle outState) {
            super.onSaveInstanceState(outState);
            activityCompat2.onSaveInstanceState(outState);
        }

        @Override
        public void onDestroy(a) {
            super.onDestroy();
            activityCompat2.onDestroy();
        }
Copy the code

When the lifecycle calls onActivityResult, the onActivityResult method on activityCompat2 will also be called, which actually calls callback:

    private void onActivityResult(Activity activity, int resultCode, Intent data) {
        RouterCallback.ActivityCallback cb;
        Pair<WeakReference<Activity>, RouterCallback.ActivityCallback> pair = sCallbackMap.get(cur);
        if(pair ! =null&& (cb = pair.second) ! =null) {
            RouterLogger.getCoreLogger().d("HoldFragment ActivityResult callback success");
            cb.onActivityResult(resultCode, data); // [16] Callback is called
        }
        if (pair == null || pair.first == null|| pair.first.get() ! = activity) {// HoldFragment onActivityResult warn, for host activity changed, but still callback last host
        }
        sCallbackMap.remove(cur);
        active.remove();
    }
Copy the code

[16], callback was called.

The hold at [14] results in delayed execution of callback, which is actually processed by a handlerpostDelay message:

class Monitor {

    private static Handler timeoutHandler;

    static void startMonitor(final Request request, final Result result) {
        long period = request.holdTimeout;
        if (period > 0) {
            check();
            RouterLogger.getCoreLogger().d("monitor for request \"%s\" start, count down \"%sms\"",
                    request.getNumber(), period);
            timeoutHandler.postDelayed(new Runnable() {
                @Override
                public void run(a) {
                    RouterExecutor.submit(new Runnable() {
                        @Override
                        public void run(a) { ResultAgent.release(request, ResultAgent.STATE_TIMEOUT); }}); } }, period); }}private static void check(a) {
        if (timeoutHandler == null) {
            synchronized (Monitor.class) {
                if (timeoutHandler == null) {
                    HandlerThread handlerThread = new HandlerThread("timeout-monitor-thread");
                    handlerThread.start();
                    timeoutHandler = new Handler(handlerThread.getLooper());
                }
            }
        }
    }
}
Copy the code

Next is the implementation of handler:

    private static void startHandler(final Request request, final RouterMeta meta,
                                     final Result result, final RouterCallback callback) {
        // dynamic
        IRouterHandler handler = meta.getHandler();
        if (handler == null) { handler = meta.getRouterProxy() ! =null ? (IRouterHandler) meta.getRouterProxy().newInstance(null) : null;
        }
        if(handler ! =null) {
            final IRouterHandler finalHandler = handler;
            RouterExecutor.execute(meta.getThread(), new Runnable() { 
                @Override
                public void run(a) {
                    if (meta.isHold()) {
                        RouterLogger.getCoreLogger().w("request \"%s\" will hold", request.getNumber());
                    }
                    finalHandler.handle(request, result);
                    if(meta.isHold() && callback ! =null) {
                        Monitor.startMonitor(request, result); // [17] postDelay
                    } else {
                        ResultAgent.release(request, ResultAgent.STATE_COMPLETE); // [18] call onResult directly}}}); }else{ ResultAgent.release(request, ResultAgent.STATE_ERROR); }}Copy the code

RouterExecutor is an executor with an internal thread pool object that can be used to specify the thread of execution. In runnable, the handler calls the handle method, and the callback determines whether to execute immediately by holding or not.

At [18], why is callback executed immediately:

    static void release(Request request, String reason) {
        if(request ! =null) { release(request.getNumber(), reason); }}private synchronized static void release(String requestNumber, String reason) {
        Result result = getResult(requestNumber);
        if(result ! =null) {
            if (result.agent.primaryRequest.getNumber().equals(requestNumber)) {
                // all clear
                for (String number : result.agent.branchRequestMap.keySet()) {
                    if(! result.agent.branchReasonMap.containsKey(number)) { completeBranch(number, reason); }}}else {
                // branch only
                completeBranch(requestNumber, reason);
            }
            // check and release primary
            if(result.agent.branchReasonMap.size() == result.agent.branchRequestMap.size()) { completePrimary(result); }}}Copy the code

Here are two methods:

completeBranch:

    private synchronized static void completeBranch(String branchNumber, String reason) {
        Result result = numberToResult.get(branchNumber);
        if(result ! =null) {
            if (STATE_TIMEOUT.equals(reason)) {
               // Enforce log when timeout expires} result.agent.branchReasonMap.put(branchNumber, reason); numberToResult.remove(branchNumber); }}Copy the code

CompletePrimary:

    private synchronized static void completePrimary(@NonNull Result result) {
        numberToResult.remove(result.agent.primaryRequest.getNumber());
        if(result.agent.callback ! =null) {
            result.agent.callback.onResult(result); // [19] Callback gets called
        }
        if(! numberToResult.containsKey(result.agent.primaryRequest.getNumber())) { RouterLogger.getCoreLogger().d("Request finish "); }}Copy the code

View and Fragment startup:

    private static void startFragment(Request request, RouterMeta meta, Result result) {
        result.routerClass = meta.getRouterClass();
        if (request.getExtra().getBoolean(Extend.START_FRAGMENT_NEW_INSTANCE, true)) { Object object = meta.getRouterProxy() ! =null ?
                    meta.getRouterProxy().newInstance(null) : null;
            if (object instanceof Fragment) {
                result.fragment = (Fragment) object;
                result.fragment.setArguments(request.getExtra());  
            }
        }
        ResultAgent.release(request, ResultAgent.STATE_COMPLETE);
    }

    private static void startView(Request request, RouterMeta meta, Result result) {
        result.routerClass = meta.getRouterClass();
        if (request.getExtra().getBoolean(Extend.START_VIEW_NEW_INSTANCE, true)) { Object object = meta.getRouterProxy() ! =null ?
                    meta.getRouterProxy().newInstance(request.getContext()) : null;
            if (object instanceof View) {
                result.view = (View) object;
                result.view.setTag(request.getExtra());
            }
        }
        ResultAgent.release(request, ResultAgent.STATE_COMPLETE);
    }
Copy the code

View and Fragment implementation logic is basically the same, create an object to store in result.

Finally, a remote call to startRemote:

    private void startRemote(a) {
        Result result = newResult(primaryRequest, Collections.singleton(primaryRequest), callback); RemoteBridge.load(primaryRequest.authority, primaryRequest.resendStrategy, primaryRequest.lifecycleOwner ! =null ? new WeakReference<>(primaryRequest.lifecycleOwner) : null)
                .start(primaryRequest, result, callback);
    }
Copy the code

RemoteBridge’s load method holds the data for the remote call, and the start method actually calls it:

    public void start(final Request request, final Result result, RouterCallback callback) {
        final RemoteCommand command = new RemoteCommand(RemoteCommand.REQUEST);
        command.bridge = RemoteBridge.this;
        command.resendStrategy = resendStrategy;
        command.lifecycle = lifecycle;
        command.uri = request.getUri().toString();
        command.extra = request.getExtra();
        command.addition = request.getAddition();
        if(callback ! =null) {
            command.binder = new IClientService.Stub() {
                @Override
                public RemoteResult callback(RemoteCommand resultCommand) {
                    result.extra = resultCommand.extra;
                    result.addition = resultCommand.addition;
                    // callback once, so release it
                    RouterHelper.release(request);
                    return null; }}; }else {
            // no callback, so release immediately
            RouterHelper.release(request);
        }
        execute(command);
    }
Copy the code

The underlying principle is that Binder calls across processes.