These days have been in componentization architecture knowledge, the following main analysis of “get” componentization scheme and Arouter implementation of component routing function.

Knowledge points involved in componentization

Resulting scheme

Recently for a while in explore the implementation scheme of componentization is build in each component gradle annotationProcessorOptions setting parameters of the host, this parameter is our current components Group, Apt gets the Group name and assembles the full path of routing table classes to be generated (different modules will generate different routing table classes), then scans the classes annotated by RouteNode in the current module, and stores path and class information in the generated classes. Class generation is mainly implemented by Javapoet framework

Below is the routing table for the App module

public class AppUiRouter extends BaseCompRouter {
    public AppUiRouter() {
    }
    public String getHost() {
        return "app";
    }
    public void initMap() {
        super.initMap();
        this.routeMapper.put("/main", MainActivity.class);
        this.paramsMapper.put(MainActivity.class, new HashMap() {
            {
                this.put("name", Integer.valueOf(8)); }}); this.routeMapper.put("/test", TestActivity.class); }}Copy the code

This is all implemented at compile time, but what about runtime? At runtime, by registering the routing table class with Application,

UIRouter.getInstance().registerUI("app");
Copy the code

This app parameter is the host value (Group value) that we set in build.gradle. Then we use UIRouter’s fetch method to concatenate the path of the registry class generated before APT. Then we use reflection to fetch this class and archive it into the map collection

Private IComponentRouter fetch(@nonNULL String host) {// String path = RouteUtils.genHostUIRouterClass(host);if (routerInstanceCache.containsKey(path))
            return routerInstanceCache.get(path);
        try {
            Class cla = Class.forName(path);
            IComponentRouter instance = (IComponentRouter) cla.newInstance();
            routerInstanceCache.put(path, instance);
            return instance;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }
Copy the code

So the next time you launch an openUri to open another component’s Activity, you can get the host value from the openUri, then the IComponentRouter, then the path, Retrieve the activity. class from the registry and start the Activity as usual. For details, see BaseCompRouter

Of course, the componentization obtained is not only these, but also the application registration, because some component modules need to be initialized in the application, but one app does not allow multiple applications, so it provides two solutions, the reflection way, Gradle plugin is used to set the combuild parameter in the build.gradle plugin. It provides parameters for the plugin.

combuild {
    applicationName = 'com.luojilab.share.runalone.application.ShareApplication'
    isRegisterCompoAuto = true
}
Copy the code

Modules then rely on the apply plugin: ‘com.dd.comgradle’

There’s a lot of stuff that’s done in add-ons, and you can look at it, and I’m going to talk about it in general, the sub-module generates an AAR, and it goes to componentRelease, and the main module goes to ComponentRelease, and compile depends on those AArs, and if the component is a separate debug module, Set the sourceSet for the module, set the AndroidManifest for the different paths, and register the transform. The transform takes the applicationName set by combuild into the classpath. Then insert bytecode into the onCreate method of the main Application via Javassist and see what it looks like

public class AppApplication extends Application {
    public AppApplication() {
    }
    public void onCreate() {
        super.onCreate();
        UIRouter.getInstance().registerUI("app"); Object var2 = null; (new ReaderAppLike()).onCreate(); (new ShareApplike()).onCreate(); (new KotlinApplike()).onCreate(); }}Copy the code

That’s about it. Let me comment:

The name of moudle is set in build.grdle, which must be consistent with the registered name of application. These two names do not have a unified source, so it is easy to cause the integration developer to make a mistake, leading to the failure to find the registry. My suggestion is as follows: Build. Gradle set an ext extension variable to the name of our module, and apt host takes that extension variable. BuildTypes set a buildConfigField that points to that same variable. We can get that variable by BuildConfig

General idea code:

apply plugin: 'com.dd.comgradle'

ext.moduleName = [
        host: "share"
]
android {
   ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [host: moduleName.host]
            }
        }
   ...
    buildTypes {
        debug {
            buildConfigField "String"."HOST"."\"${moduleName.host}\ ""
        }
        release {
            buildConfigField "String"."HOST"."\"${moduleName.host}\ ""}} / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- child components ShareApplike. Class @ Override public voidonCreate() {/ / child components package name + BuildConfig. Get the value of the host uiRouter registerUI (com) luojilab) share. BuildConfig. Host); Log.i("ShareApplike"."ShareApplike-----");
        new ShareApplike().onCreate();
    }

Copy the code

This ensures that the registered components are consistent with the generated components

Another thing THAT I don’t like is the function of application that uses transform to insert bytecode. The application path corresponding to comBuild needs to be configured in build.gradle. For the integrator, the less configuration and more powerful implementation is the best method. The function of Transform is to insert bytecode into the application of each component. In fact, transform can be completely abandoned. Although transfrom can be used to insert bytecode, reflection can be avoided, but after all, there are only a few applications of components, and reflection is only a few classes. If the component registers too many routes, the routing table will be too large, and reflection will affect performance. I think a better way is to define a annotation like RouteApplication, Then mark all the applications that the component needs to execute with RouteApplication annotation. Apt parses these classes and generates the corresponding moudle name +Application class. Then in the run phase, when openUri opens other components, it concatenates the path class and reflects it. As with the routing table approach, this eliminates transfrom completely and requires less configuration

Another is that, for the sake of performance, apt is not used, apt will always encounter reflection, it is recommended to use transfrom to insert bytecode, and insert all routes into a routing table management class, which is written by ourselves, but there is nothing in it. Both are inserted at compile time by Transform, which uses Either Javassist or ASM to manipulate bytecode, but one is useful, time-consuming, and the other is not, fast, but it doesn’t matter which one is used, and both are at compile time, as long as they don’t affect run time

In addition, APT can only scan the classes of the current Module to get the class information, and it cannot scan the classes in JAR packages, Maven and AAR. Therefore, it is relatively limited. Transfrom can scan APT but cannot solve the problems


Arouter solution

Last year, the authors of CC componentization submitted a PR to Arouter. Auto-register provides Arouter with the ability to automatically register routes at compile time. Arouter used to register routing tables by reflection, but now inserts bytecode via transfrom.

Unlike “get” componentization, Arouter’s component modules cannot run independently and need to be developed to solve their own problems. Arouter only provides routing solutions

Arouter provides three main annotation handlers

  • RouteProcessor: Handles annotated routes on classes
  • InterceptorProcessor: route interceptor, which acts on a class
  • AutowiredProcessor: Injects the values passed from the previous page and acts on the fields

Once again, each component has to rely on the annotation handler. Arouter and “get” are not the same parameters. Arouter provides the routing table Group, while Arouter provides the module parameter to generate and collect all the current module groups. Then the collected groups correspond to each routing table

  javaCompileOptions {
            annotationProcessorOptions {
                arguments = [ moduleName : project.getName() ]
            }
Copy the code

RouteProcessor scans the classes annotated by Route and retrits the path, group, and Autowired fields of the current Route annotated class. This information is stored in the RouteMeta class, mainly for ease of management. Here’s an example of the group field:

/ * * * sotestGroup */ @route (path ="/test/activity1")
Copy the code

When the group field is assigned to RouteMeta, it is when the Categories method is called and the routeVerify method is used to verify that the path is true, See the routeVerify method of the RouteProcessor class.

The categories method looks at groupMap, which is a Map

> type. The main function is sorting, with Group as the key. A RouteMeta like Group is placed in a set as a basis for generating registry classes later
,>

Once the grouping information is sorted, the groupMap collection is traversed. The main function of this groupMap collection is to create class files through javapoet. Let’s look at the code that generates the class, which is a little more core.

/ / stitching Arouter $$Group? <test> class (groupName) String groupFileName = NAME_OF_GROUP + groupName; // Generate the corresponding 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); Rootmap.put (groupName, groupFileName); rootmap.put (groupName, groupFileName); rootmap.put (groupName, groupFileName); }Copy the code

At the end of the loop, the rootMap comes into play, first to populate the fields and concatenate the field information to add to methodSpec.Builder

   if (MapUtils.isNotEmpty(rootMap)) {
      // Generate root meta by group name, it must be generated before root, then I can find out the class of group.
     for (Map.Entry<String, String> entry : rootMap.entrySet()) {
        loadIntoMethodOfRootBuilder.addStatement("routes.put($S.$T.class)", entry.getKey(), ClassName.get(PACKAGE_OF_GENERATE_FILE, entry.getValue())); }}Copy the code

This is why we set moduleName to javaCompileOptions in build.gradle. The main function is to generate Arouter? Root? Class, and then store the Group’s class information in the moduleName class

/ / stitching Arouter $$Root? <moduleName> class String rootFileName = NAME_OF_ROOT + SEPARATOR + moduleName; JavaFile.builder(PACKAGE_OF_GENERATE_FILE, TypeSpec.classBuilder(rootFileName) .addJavadoc(WARNING_TIPS) .addsuperInterface (classname.get (elements. GetTypeElement (ITROUTE_ROOT))).addmodiFIERS (PUBLIC) // Add concatenated fields .addMethod(loadIntoMethodOfRootBuilder.build()) .build()).build().writeTo(mFiler);Copy the code

Next I’ll post the two generated classes

ARouter? Root? App. Java: Collects the routing table classpath corresponding to all groups in the App Module

public class ARouter$$Root$$app implements IRouteRoot {
    public ARouter$$Root$$app() {
    }

    public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
        routes.put("service", service.class);
        routes.put("test", test.class);
        routes.put("test2".test2.class); }}Copy the code

ARouter? Group? Test2. Java: routing table of the Test group in app Module

public class ARouter$$Group$$test2 implements IRouteGroup {
    public ARouter$$Group$$test2() {
    }

    public void loadInto(Map<String, RouteMeta> atlas) {
        atlas.put("/test2/activity2", RouteMeta.build(RouteType.ACTIVITY, Test2Activity.class, "/test2/activity2"."test2", new HashMap<String, Integer>() {
            {
                this.put("key1", Integer.valueOf(8)); }} - 1-2147483648)); }}Copy the code

For instance, if the host of a Reader component is set to Reader, then the host of a Reader component is set to Reader. If the host of a Reader component is set to Reader, then the host of a Reader component is set to Reader. All routing table groups are readers, and all routing table classes are generated based on the host name. If the current module has a lot of groups, it will generate a lot of classes. The bad part is not intuitive. The generated class information is too much, but they are all the same. The object that “gets” the reflection is the Group.

Groups Grouping is not a very important concept in Arouter, unlike the “get” scenario, where each component specifies a Group and Arouter can define any Group it wants. There may be many groups in a single component.

Arouter’s previous solution was to go through the Dex file and get the class information and reflect it, get it into the registry, put it in a cache, and then after auto-Register was introduced, inject bytecode, The main logic is to look at the LogisticsCenter class.

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
           
            long startInit = System.currentTimeMillis();
            //billy.qi modified at 2017-12-06
            //load by plugin first
            loadRouterMap();
            if (registerByPlugin) {
                logger.info(TAG, "Load router map by arouter-auto-register plugin.");
            } else{... routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE); . }}Copy the code

The loadRouterMap method is used to set whether to use auto-register. The default value of registerByPlugin is false, or to use ClassUtils to reflect the registry. Set registerByPlugin to true and rely on the plugin apply plugin: ‘com.alibaba. Arouter ‘in build.gradle

What are the benefits of auto-Register? Just had a chat with the author

  • Optimized startup speed
  • The problem that routes cannot be found after hardening is solved
The AutoRegister plug-in fundamentally solves the problem of not finding the dex file: bytecode is scanned for the implementation classes of the corresponding three interfaces at compile time, and the registration code is generated into ARouter's LogisticsCenter class. There is no need to read the dex file at runtime, thus avoiding the compatibility problem of hardening.Copy the code

Arouter would have traversed the APK dex to find the registry class information. However, due to security problems, the dex file could not be found. Traversing the dex file is a time-consuming operation, and the initial application is not as fast as automatic registration.

Another interesting thing about this site is that let’s take a look at the loadRouterMap method, mainly this comment

  private static void loadRouterMap() {
        registerByPlugin = false; //auto generate register code by gradle plugin: arouter-auto-register // looks like below: // registerRouteRoot(new ARouter.. Root.. modulejava()); // registerRouteRoot(new ARouter.. Root.. modulekotlin()); }Copy the code

When I first looked at it, I always thought that the bytecode that auto-Register was doing was inserting registerRouteRoot(new ARouter.. Root.. Modulejava ()), we said, when I was in the previous analysis of the registry Group, the Group is in the class information of each module, if directly to the module class, took out his Group map collections, according to the map set you can find the Route Route set, and, The auto-register (“ARouter? “) register(“ARouter? Root? ModuleName classpath “); The regiter method is used to register classes that store groups for each module

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

With (new registerRouteRoot ARouter.. Root.. Modulejava ()) is an extra reflection. I wonder why the transform can find the Module class that stores the Group by inserting bytecode. Wouldn’t getting rid of reflection better optimize performance? Then I asked the auto-Register author, and he told me the story goes like this:

After I submitted PR, the author of ARouter gave feedback that the size of the first dex was increased, and the class name should be changed to reflect the way objects are created (confusion rules need to be configured). However, I did not find much impact of this registration on the first dex after testing, so the AutoRegister continues toregister as an objectCopy the code

In the end, Arouter went with reflection

Finally, what auto-Register does is it uses a transform to iterate over the class information of all modules. Looking for the full path of the class start whether com/alibaba/android/arouter/routes, so, to join a cache registerList set inside, waiting to be inserted into the bytecode

Insert the bytecode section, let’s take a look, paste a little bit of code

        @Override
        void visitInsn(int opcode) {
            //generate code before return
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
                extension.classList.each { name ->
                    name = name.replaceAll("/".".") mv. VisitLdcInsn (name) / / storage group, a group of the module name of the class / / generate invoke the register method into LogisticsCenter. LoadRouterMap () mv.visitMethodInsn(Opcodes.INVOKESTATIC , ScanSetting.GENERATE_TO_CLASS_NAME//com/alibaba/android/arouter/core/LogisticsCenter , ScanSetting.REGISTER_METHOD_NAME//register ,"(Ljava/lang/String;) V"
                            , false)
                }
            }
            super.visitInsn(opcode)
        }
Copy the code

This code uses ASM to insert the bytecode, asm to find the classpath using a slash, but the class to insert the bytecode needs to be a dot, this code is the loadRouterMap method of the LogisticsCenter class, Insert a register(” Name of module class that stores group “); code

Arouter’s analysis is over, so let’s just conclude

conclusion

Knowledge points involved

  • The use of the apt
  • The use of transfrom

If you want to discuss it together, you can add QQ group 492386431. After all, a person’s idea will have limitations. Transfrom also needs to know the knowledge of gradle Plugin.

Here I would like to thank Billy, the author of CC componenization and auto-register. He is really a great developer. If there is any problem, he will explain it one by one in the group to help developers solve their confusion. Wang Longhai, the author of Andromeda, the cross-process componentization scheme of iQiyi open source, is also here, hoping that we can learn and communicate together