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:
- Call native code out of end, Push and H5
- Componentized communication without sinking interface, data return capability
- 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.