Front-end development often encounters a word: routing. In Android APP development, routing is often strongly associated with componentized development. So what is routing on the ground, what functions should a routing framework have on the ground, and what is the implementation principle? Is routing a strong requirement for your APP? And componentization in the end what relation, this article is simple analysis of the above problems.

Concepts of routing

The word route itself is supposed to be a word in Internet protocol, as Wikipedia explains:

Routing is the activity of transferring information from a source address to a destination address over an interconnected network. Routing occurs at the third layer in the OSI network reference model, the network layer.Copy the code

Personally, in front-end development, routing is the ability to map to a business through a string of strings. The routing box of APP can first collect the routing scheme of each component and generate the routing table. Then, it can match the corresponding page or service in the routing table according to the external input string, jump or call, and provide the return value, etc., as shown below

So a basic routing framework should have the following capabilities:

    1. APP route scan and registration logic
    1. Redirect to the target page
    1. The ability of the route to invoke the target service

In APP, when conducting page routing, it is often necessary to judge whether to log in and other additional authentication logic, so it is also necessary to provide interception logic, such as: log in.

Whether the tripartite routing framework is strongly required by APP

Answer: No, the system provides routing capability natively, but with few functions. Slightly large-scale APPS all adopt tripartite routing framework.

The Android system itself provides the ability of page skipping: for example, startActivity is sufficient for tool apps or stand-alone apps, without the need for a special routing framework. Why do many apps still use a routing framework? This is related to the nature of APP and the advantages of routing framework. For example, taobao, JINGdong, Meituan and other large apps are very large both in terms of APP functions and the size of their R&D teams. Different businesses are often maintained by different teams, which adopt the component-based development mode and eventually integrate into one APK. Business interaction is often involved between multiple teams, such as switching from movie ticket business to food business, but the two businesses are two independent R&D teams, and the code implementation is completely isolated, so how to communicate? The first thought is to introduce it in code, but that would break the purpose of low coupling and introduce all sorts of problems. For example, part of the business is done by the outsourcing team, which involves the problem of code security. Therefore, we still hope to call the target business in a way similar to black box, which requires the support of routing. Therefore, many Domestic APPS use the routing framework. Secondly, our various jump rules do not want to be related to the specific implementation class. For example, when the jump business is detailed, we do not want to know which Activity is implemented, but only need to map a string to the past, which is very standard for H5 or back-end development to deal with the jump.

Limitations of the native route: Single function, poor flexibility in expansion, and difficult coordination

Traditional routing is basically limited to startActivity or startService to redirect or start a service. StartActivity can be used in two ways, either explicitly or implicitly, to display the following calls:

<! - 1 import dependence - > import com. Q. Activityforresultexample. Test. SecondActivity; public class MainActivity extends AppCompatActivity { void jumpSecondActivityUseClassName(){ <! Intent Intent =new Intent(mainActivity.this, secondActivity.class); startActivity(intent); }Copy the code

The downside of the display invocation is that it must rely strongly on the class implementation of the target Activity. In some scenarios, especially in the case of large APP component-development, where the business logic does not want to be dependent on the source code or AAR for security reasons, the explicit dependency approach does not work. Take a look at the implicit invocation method.

Step 1: Configure the activity’s intent-filter in the manifest. Configure at least one action

<? The XML version = "1.0" encoding = "utf-8"? > <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.snail.activityforresultexample"> <application ... <activity android:name=".test.SecondActivity"> <intent-filter> <! - implicit call use must be configured android. Intent. The category. The DEFAULT - > < category android: name = "android. Intent. The category. The DEFAULT" / > <! - at least, can configure an action by implicit call - > < action android: name = "com. Q. Activityforresultexample. SecondActivity" / > <! -- Optional --> <! -- <data android:mimeType="video/mpeg" android:scheme="http" ... />--> </intent-filter> </activity> </application> </manifest>Copy the code

Step 2: Invoke

void jumpSecondActivityUseFilter() {
    Intent intent = new Intent();
    intent.setAction("com.snail.activityforresultexample.SecondActivity");
    startActivity(intent);
}
Copy the code

If it involves data passing, the writing is more complicated. Implicit calls have the following disadvantages:

  • First of all, the manifest definition is complex, which will cause exposed protocols to become complex and difficult to maintain and extend.
  • Secondly, different activities require different action configurations. It is very troublesome to add or subtract activities every time, which is very unfriendly to developers and increases the difficulty of collaboration.
  • Finally, the export attribute of an Activity is not recommended to be set to True, which is a way to reduce risks. Generally, it is added to an Activity, and DeeplinkActivitiy handles jumps in a unified manner. In this scenario, DeeplinkActivitiy also has routing functions. In the scenario of implicit call, the increase or decrease of new Activitiy is bound to adjust the routing table every time, which will reduce development efficiency and increase risks.

It can be seen that the system’s native routing framework does not give much consideration to the development mode of team collaboration. It is limited to direct reference between multiple businesses within a module, and basically requires code-level dependence, which is unfriendly to code and business isolation. Regardless of the Dex method tree overlimit, it can be considered that the tripartite routing framework was created entirely for team collaboration.

Capabilities required by APP tripartite routing framework

At present, most routing frameworks on the market can solve the above problems. The ability of three-party routing can be summarized as follows:

  • Routing table generation capability: Business components **[UI business and Services]** automatically scan and register logic, need to be extensible, without invading the original code logic
  • Scheme and business mapping logic: Do not rely on concrete implementation to achieve code isolation
  • Basic route hop capability: supports the page hop capability
  • Service component support: go to a service component to get some configuration, etc
  • Route interception logic: for example, login, unified authentication
  • Customizable downgrade logic: The bottom line when components are not found

The next typical use of Arouter is step 1: add Arouter Scheme declaration to the new page,

	@Route(path = "/test/activity2")
	public class Test2Activity extends AppCompatActivity {
		 ...
	}
Copy the code

In the Build phase, routing schemes are collected based on annotations and routing tables are generated. Step 2 Use

        ARouter.getInstance()
                .build("/test/activity2")
                .navigation(this);
Copy the code

As above, in the ARouter framework, only the string scheme is needed to implement route jumps without relying on any Test2Activity.

Implementation of APP routing framework

The core of the implementation of the routing framework is the ability to establish the mapping between Scheme and component **[Activity or other services]**, that is, the routing table, and route to the corresponding component according to the routing table. In fact, there are two parts: the first part is the generation of routing table, and the second part is the query of routing table

Automatic generation of routing tables

There are many ways to generate routing tables. The simplest is to maintain a common file or class that maps each implementation component to scheme,

However, the disadvantages of this approach are obvious: the table has to be modified every time it is added or deleted, which is very unfriendly to collaboration and does not meet the original intention of solving the problem of collaboration. However, the final routing table is the same way, which is to collect all the Scheme into one object, but the implementation is different. At present, almost all tripartite routing frameworks are implemented with the help of annotations +APT[Annotation Processing Tool] +AOP (aspect-oriented Programming, section-oriented Programming). The basic process is as follows:

The involved technologies include Annotation, APT(Annotation Processing Tool) and AOP (aspect-oriented Programming). JavaPoet is commonly used in APT. It mainly traverses all classes, finds annotated Java classes, and then aggregates to generate routing tables. Since there may be many components and routing tables, these generated auxiliary classes will be compiled into class files together with the source code. Then use AOP techniques (such as ASM or JavaAssist) to scan the generated classes, aggregate the routing table, and populate it with the placeholder methods to complete the automatic registration logic.

How does JavaPoet collect and generate a collection of routing tables?

Using the ARouter framework as an example, we will define the annotations required by the Router framework as follows:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {

    /**
     * Path of route
     */
    String path();
Copy the code

This annotation is used to annotate components that need to be routed as follows:

@Route(path = "/test/activity1", Public class Test1Activity extends BaseActivity {@autoWired int age = 10;Copy the code

After that, APT was used to scan all annotated classes and generate routing table. The implementation reference is as follows:

@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { if (CollectionUtils.isNotEmpty(annotations)) { <! Route.class --> Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class); <! --> this.parseroutes (routeElements); . return false; } <! Private void parseRoutes(Set<? extends Element> routeElements) throws IOException { ... // Generate groups String groupFileName = NAME_OF_GROUP + groupName; JavaFile.builder(PACKAGE_OF_GENERATE_FILE, TypeSpec.classBuilder(groupFileName) .addJavadoc(WARNING_TIPS) .addSuperinterface(ClassName.get(type_IRouteGroup)) .addModifiers(PUBLIC) .addMethod(loadIntoMethodOfGroupBuilder.build()) .build() ).build().writeTo(mFiler);Copy the code

The product is as follows: contains routing table, and local registration entry.

Automatic registration: ASM collects the above routing table and aggregates it into the Init code area

In order to be able to insert into the Init code area, we first need to reserve a space, usually defining an empty function to be filled later:

public class RouterInitializer { public static void init(boolean debug, Class webActivityClass, IRouterInterceptor... interceptors) { ... loadRouterTables(); Public static void loadRouterTables() {}}Copy the code

First, use AOP tools to traverse the APT intermediates mentioned above, aggregate the routing table, and register them to the pre-initialized location. The process of traverse involves gradle Transform.

  • Collect the target and aggregate the routing table

    JarFile: File, dest: File? { val file = JarFile(jarFile) var enumeration = file.entries() while (enumeration.hasMoreElements()) { val jarEntry = enumeration.nextElement() if (jarEntry.name.endsWith("XXRouterTable.class")) { val inputStream = file.getInputStream(jarEntry) val classReader = ClassReader(inputStream) if (Arrays.toString(classReader.interfaces) .contains("IHTRouterTBCollect") ) { tableList.add( Pair( classReader.className, dest?.absolutePath ) ) } inputStream.close() } else if (jarEntry.name.endsWith("HTRouterInitializer.class")) { registerInitClass = dest } } file.close() }Copy the code
  • Inject routing table initialization code into the target Class

    fun asmInsertMethod(originFile: File?) { val optJar = File(originFile? .parent, originFile? .name + ".opt") if (optJar.exists()) optJar.delete() val jarFile = JarFile(originFile) val enumeration = jarFile.entries() val jarOutputStream = JarOutputStream(FileOutputStream(optJar)) while (enumeration.hasMoreElements()) { val jarEntry = enumeration.nextElement() val entryName = jarEntry.getName() val zipEntry = ZipEntry(entryName) val InputStream = jarfile.getinputStream (jarEntry) class if (entryname.endswith ("RouterInitializer.class")) { . / / the class file handling jarOutputStream putNextEntry (zipEntry) val classReader = classReader (IOUtils. ToByteArray (inputStream) val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS) val cv = RegisterClassVisitor(Opcodes.ASM5, classWriter,tableList) classReader.accept(cv, EXPAND_FRAMES) val code = classWriter.toByteArray() jarOutputStream.write(code) } else { jarOutputStream.putNextEntry(zipEntry) jarOutputStream.write(IOUtils.toByteArray(inputStream)) } JarOutputStream. CloseEntry ()} / / end jarOutputStream. Close () jarFile. Close () if (originFile?. The exists () = = true) { Files.delete(originFile.toPath()) } optJar.renameTo(originFile) }Copy the code

The loadRouterTables of the RouterInitializers. Class will be changed to the following code:

public static void loadRouterTables() { <! ----> register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava"); register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin"); register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi"); register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava"); . }Copy the code

This completes the collection and registration of the routing table, and this is the general process. Slightly different for support services, fragments, etc., of course, but broadly similar.

Router framework support for service-class components

Access service belongs to the APP by routing routing is more unique ability, such as a user center components, we can through the way of routing to query whether the user is logged in, this is not a special page on the concept of routing, through a series of string how to look up to the corresponding component and call its methods? There are many ways to achieve this, and each way has its own advantages and disadvantages.

  • One way is to abstract the service as an interface and sink to the bottom layer, where the upper implementation maps objects through routing
  • One is to map implementation methods directly through routing

It has the advantage of exposing all exposed services to the interface class, which is very friendly to external callers, i.e. service consumers, as shown in the following example:

Define the abstract service first and sink to the bottom

public interface HelloService extends IProvider {
    void sayHello(String name);
}
Copy the code

Implement the service and tag it with the Router annotation

@Route(path = "/yourservicegroupname/hello")
public class HelloServiceImpl implements HelloService {
    Context mContext;

    @Override
    public void sayHello(String name) {
        Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
    }
Copy the code

Use: Use Router plus Scheme to get service instances, map them to abstract classes, and then call methods directly.

  ((HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation()).sayHello("mike");
Copy the code

This approach is actually very convenient for users, especially when a service has multiple operable methods, but the disadvantage is extensibility, if you want to extend the method, you have to change the underlying library.

The second option is to map implementation methods directly through routing

Add a method routing table to the Page routing table. Add a method routing table to the Page routing table. Add a method routing table to the Page routing table.

Define the Router for Method

public class HelloService { <! @methodrouter (url = {"arouter:// sayHello "}) public void sayHello(String name) {toa.makeText (mContext, mContext) "Hello " + name, Toast.LENGTH_SHORT).show(); }Copy the code

Can be used

RouterCall.callMethod("arouter://sayhello? name=hello");Copy the code

The disadvantages mentioned above are that external calls are somewhat complicated, especially when dealing with parameters, which need to be handled strictly according to the protocol. The advantage is that there is no abstraction layer, and there is no need to change the bottom layer if the service method needs to be extended.

Both approaches have their pros and cons, but for the sake of the left service component, the first is better: it’s more caller-friendly. In addition to support for CallBack, Arouter’s processing may also be more convenient, which can be easily defined by the server. If the service is mapped directly through the route, it will be more difficult to handle, especially if the parameters in the Callback need to be encapsulated as JSON and maintain the parse protocol, which may not be very good to handle.

Routing table matching

The matching of the routing table is relatively simple. The routing table matches the target component according to String input in the global Map, and then relies on common operations such as reflection to locate the target.

Relationship between componentization and routing

Componentization is a development integration mode, which is more like a development specification and more convenient for team collaborative development. Componentization eventually fall to the ground is to separate business and functional components, these components may be a different team, between in different purpose in their respective maintenance, even need to code the isolation, if involved in calls and communication between components, is inevitable with the aid of routing, because of isolation, can only be general string scheme is adopted to improve the communication, This is the functional area of routing.

The root reason componentization requires routing support: isolation of code implementations between components

conclusion

  • Routing is not a required feature of an APP, but is generally required for large, cross-team apps
  • Basic capabilities of routing framework: automatic route registration, routing table collection, service and UI interface routing and interception and other core functions
  • Componentization versus routing: Componentized code isolation makes routing frameworks necessary

APP routing framework and componentization