preface

The routing framework boils down to the mapping between path and the specified jump target. By storing these mappings in the routing table, all jump requests are processed in a unified manner, and the interception and degradation functions are provided. This article explores some of the technical implementation details on the routing framework. First step need to start from the routing of the functional analysis, is the scope of application of routing is suitable for the communication between different modules/call, can pass the Path to remove direct coupling between modules, so across modules collect routing information has become the necessary function, even cited do not pack compiled code, at the same time also can collect to the global routing information correctly. In addition, routes should also provide repeated Path detection, parameter injection, interceptors, jump degradation, etc.

Global Routing information

Common routing frameworks such as ARouter can also collect all the information by traversing the dex file during initialization, looking for anything that conforms to the structure of the stored routing table information, but this is a time-consuming and inefficient run-time traversal process. A compile-time data collection scheme is proposed to shift the overhead of the look-through process to compile-time. Since you need to collect data at compile time, you need to save the routing table in some form that can be used at compile time. The simplest is to use the class file to save, there are two obvious advantages: first, the classloader for data codec, just use a tool to generate the file can save the data and comply with the class rules. Second, class is a bytecode file with a compact format and small size. Having access to the routing table data at compile time solves the main problem. The routing table can be divided into two levels to better manage routing data throughout the application — a sub-table manages routing information for the current code package (JAR or AAR), and a master table manages runtime data for the entire application. We can use this formula to express the relationship between sub-tables and total tables:

App General table = App Sub-table + Module1 Sub-table + Module2 Sub-table +……

To be more realistic, the interface is abstracted here.

Public interface IRouterNodeProvider {// Provides the routing table. List<RouterNode> getRouterNodes(); } public interface IRouterLoader {List<RouterNode> loadNode(); }Copy the code

The submodule collects routing information of the current Module through parsing annotations during compilation, and implements IRouterNodeProvider through bytecode tools. An example of an implementation class is shown below

Public class ComponentNodeProvider implements IRouterNodeProvider {public List getRouterNodes() { ArrayList var1 = new ArrayList(); var1.add(RouterNodeFactory.produceRouterNode(NodeType.FRAGMENT, "/login/loginFrag", LoginFragment.class)); var1.add(RouterNodeFactory.produceRouterNode(NodeType.ACTIVITY, "/login/name", NameActivity.class)); var1.add(RouterNodeFactory.produceRouterNode(NodeType.ACTIVITY, "/login/choice", ChoiceActivity.class)); var1.add(RouterNodeFactory.produceRouterNode(NodeType.ACTIVITY, "/login/codeActivity", CodeActivity.class)); var1.add(RouterNodeFactory.produceRouterNode(NodeType.ACTIVITY, "/login/test", TestActivity.class)); return var1; }}Copy the code

During the compilation of the main module, the input submodules are traversed, all routing tables that meet the requirements of the submodules are collected, and combined into the master table. The ability to perform global routing information at compile time can be achieved in this way. Then we need to deal with another problem, if the amount of routing information is very large, if all the information is loaded into the memory when the project is used for the first time, it will inevitably cause a great load on the memory, so we need to load it in blocks according to the actual call situation. However, in actual situations, specific business requirements are complicated and varied, and the automatic grouping method may not be effective (for example, dividing information into several equal pieces and loading a whole block when a certain piece of content is used). In this case, the customized method may be more suitable for the routing block strategy.

Group lazy loading

If you enter group when creating a path, the routing information can be divided into different blocks based on the group content. These blocks have a high correlation. If one path is used, the remaining paths of the same group have a high probability of being invoked in the following process. To support grouping, you need to make some changes to the interface that generates the bytecode.

// There may be more than one grouping table in a module, All routing information in a group corresponds to a group table. Public interface IRouterNodeProvider {List<RouterNode> getRouterNodes(); }Copy the code

List<IRouterNodeProvider> Public interface IRouterLazyLoader {List<IRouterNodeProvider> lazyLoadFactoryByGroup(String group); }Copy the code

After the change, the master table can directly return a series of group table routing information based on the group information and use. A summary table might look like this:

Public class RouterLazyLoaderImpl implements IRouterLazyLoader {public List lazyLoadFactoryByGroup(String) group) { ArrayList var2 = new ArrayList(); // here is the group hash switch(group.hashcode ()) {case-1399907075: // Main Module generated routing information group table var2.add(new NodeProvider()); return var2; Case: 3151346 / / retrieval through compile time, the other of the module set table var2. Add (new android. Khala [2]. Gen. The router. Frag. Java_component. NodeProvider ()); return var2; case 1984153269: var2.add(new android.khala.gen.router.service.java_component.NodeProvider()); return var2; default: return var2; }}}Copy the code

After obtaining routing information based on the Path, you can determine the forward type based on the information and perform response operations.

Parameters of the injection

In the case of activities, fragments and other parameters that need to be transmitted through the bundle, filling in keys and values is tedious in many cases, verifying parameters transmission and obtaining parameters require separate logic. In addition, once the parameters transmitted need to be modified, the above logic also needs to be changed. Parameter injection can automatically generate the logical code to get the parameters by mapping the key to the corresponding field in the class. The solution here is to implement an additional interface (new method) for Activity and Fragment classes that use parameter injection.

public interface IInjectable { void autoSynthetic$FieldInjectComponent(); } // Before using the injected parameter, Call inject() // Inject () public static void inject(Object target) {if (target instanceof IInjectable) {((IInjectable)  target).autoSynthetic$FieldInjectComponent(); }}Copy the code

@inject (name = "a") private int testInt; @Inject private String testString; @Inject private int color; Public void autoSynthetic$FieldInjectSoulComponent() {if (this instanceof IInjectable) {Bundle var1 =  this.getArguments(); if (var1 ! = null) { this.testInt = var1.getInt("a", this.testInt); this.testString = var1.getString("testString"); this.color = var1.getInt("color", this.color); }}}Copy the code

This approach also reduces some of the aOP-induced package size increase by directly adding a new method to the existing class, eliminating the need for an additional class.

Repeated Path detection

Module development, may maintain different modules is also different, some corresponding specification will be different, the amount of routing information to a certain degree, can appear the Path repeated problems, from the perspective of implementation and in principle, the best Path and jump targets is one-to-one mapping relationship, repeated Path will lead to jump behavior can not be sure, Therefore, it is necessary to prompt the development of Path duplication as soon as possible. Human development rules are sometimes not so reliable, and human mistakes are often much more than machines, so it is necessary to conduct a routing Path duplication detection process. Based on the first section, path detection is actually divided into two modules:

  1. Path detection for the current Module
  2. Path detection for the entire application

The first point is very simple. When collecting information about the current Module at compile time, you can make repeated judgments and throw exceptions to ensure that there is no duplicate Path in the routing information of the current Module. The second point is much easier to implement under this premise. As mentioned above, the routing information of this scheme is stored through class, so it is completely possible to collect sub-tables during the compilation period of the overall application, load these routing table information with a new ClassLoader, get the corresponding routing information, and then put it into a set for repeated judgment. The application Path duplication can also be thrown at compile time and notified to the corresponding module for modification. The problem with this scenario is that it leads to an increase in compile time and memory usage on the packaging machine. In addition, if a new and long-iterated component is added to the application, it may lead to a lot of repetition and require a lot of modification.

In fact, we can change the idea here. Instead of repeated detection between different modules, we can avoid the repetition between different modules by some extra means. For example, each module can be assigned a path prefix or scheme at compile time, transparent to development. In the current scenario, for example, the simplest way to delegate the logic to get the specified routing information is to the NodeProvider. The modified IRouterNodeProvider#getRouterNodes method might look like this:

public class ComponentNodeProvider implements IRouterNodeProvider { private String prefix = "prefix"; Public RouterNode obtainNode(String path){return noderepository.getbypath (prefix + path); } public List getRouterNodes() { ArrayList var1 = new ArrayList(); var1.add(RouterNodeFactory.produceRouterNode(NodeType.FRAGMENT, prefix + "/login/loginFrag", LoginFragment.class)); var1.add(RouterNodeFactory.produceRouterNode(NodeType.ACTIVITY, prefix + "/login/name", NameActivity.class)); return var1; }}Copy the code

After this process, you only need to configure a unique prefix or scheme for a new component.

other

The routing implementation described in this article, as part of componentized interactive communication, is open sourced on Github at github.com/nebulae-pan… . Welcome to use and put forward suggestions.