preface

When I was doing startup optimization, I found that ARouter took nearly 2s to initialize the first time I started the application. When you query the optimization solution, you find that you only need a plug-in to solve the problem. I feel that the solution is quite novel, but since I don’t know much about ARouter’s underlying implementation, so this article was born, from a small point of view to analyze how to do this plug-in, the implementation of the idea for us and what inspiration.

Basic use of ARouter

The basic use of ARouter is relatively simple, and the official README is quite clear. You can also refer to ARouter’s basic use blog. I won’t tell you much here.

ARouter starts optimization

Automatic registration by using gradle plug-in can shorten the initialization time and solve the problem that the dex file cannot be directly accessed due to application hardening

apply plugin: 'com.alibaba.arouter' buildscript { repositories { mavenCentral() } dependencies { classpath "com.alibaba:arouter-register:?" }}Copy the code

Source code design analysis

Here’s how ARouter was designed from a startup optimization perspective, why it took so long to start up, and how it was optimized with a plug-in.

Initialization entry

The search for entry must be at initialization time

public synchronized static void init(Context context, ThreadPoolExecutor tPE) throws HandlerException {// Check whether the arouter-register plug-in automatically loads the routing table loadRouterMap(); If (registerByPlugin) {// 1. Set<String> routerMap; / / to get into the apk prefix for the com. Alibaba. Android. Arouter. Routes class routerMap = ClassUtils. GetFileNameByPackageName (mContext, ROUTE_ROOT_PAKCAGE); / / load the prefix for the com. Alibaba. Android. Arouter. The routes of the class, on the set set inside the if (! routerMap.isEmpty()) { context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE). edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply(); } for (String className: routerMap) { if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) { // This one of root elements, load root. ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex); } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) { // Load interceptorMeta ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex); } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) { // Load providerIndex ((IProviderGroup)(Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex); }}}}Copy the code

LoadRouterMap () registerByPlugin=true if arouter-register is used to automatically load the routing table.

Non-plug-in mode

Let’s look at the non-plug-in approach and see where the initialization time is.

public static Set<String> getFileNameByPackageName(Context context, final String packageName){ final Set<String> classNames = new HashSet<>(); List<String> Paths = getSourcePaths(context); final CountDownLatch parserCtl = new CountDownLatch(paths.size()); // ⭐️ Enable thread pool scanning dex file for (final String path: paths) { DefaultPoolExecutor.getInstance().execute(new Runnable() { @Override public void run() { DexFile dexfile = null; // EXTRACTED_SUFFIX = ".zip"; if (path.endsWith(EXTRACTED_SUFFIX)) { dexfile = DexFile.loadDex(path, path + ".tmp", 0); } else { dexfile = new DexFile(path); } Enumeration<String> dexEntries = dexfile.entries(); while (dexEntries.hasMoreElements()) { String className = dexEntries.nextElement(); if (className.startsWith(packageName)) { classNames.add(className); }}}}); } parserCtl.await(); }Copy the code

Open a thread pool to fetch and scan dex files, thread pool configuration:

private static final int INIT_THREAD_COUNT = CPU_COUNT + 1; private static final int MAX_THREAD_COUNT = INIT_THREAD_COUNT; private static final long SURPLUS_THREAD_LIFE = 30L; public class DefaultPoolExecutor extends ThreadPoolExecutor { public static DefaultPoolExecutor getInstance() { if (null = = {synchronized instance) (DefaultPoolExecutor. Class) {if (null = = instance) {/ / core number of threads is the number of CPU + 1; Maximum thread = core thread; Instance = new DefaultPoolExecutor(INIT_THREAD_COUNT, MAX_THREAD_COUNT, SURPLUS_THREAD_LIFE, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(64), new DefaultThreadFactory()); } } } return instance; }}Copy the code

The number of threads configured in the thread pool is related to the number of cpus. If the number of cpus is large, the number of threads that can be started is large, so the scanning speed of DEX file is fast and the whole processing time is relatively short.

This is relatively fast on high-end multi-core machines, but it magnifies the problem on low-end machines, especially for large projects with large DEX files and poor CPU performance, making the whole time much longer and unfriendly to first-time applications.

Optimization with plug-ins

A quote from the official document:

ARouter is used to automatically load the routing table. Gradle is used to automatically register the routing table, which can shorten the initialization time and solve the problem that the dex file cannot be directly accessed due to application hardening. Note that this plug-in must be used with API 1.3.0 or later!

Now look again at the plug-in logic in the logisticscenter. init method:

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException { loadRouterMap();  If (registerByPlugin) {// 1. Plugin logic}}Copy the code

LoadRouterMap () is called. From the code comments, the arouter auto-register plugin automatically generates registerRouteRoot(new arouter.. Root.. Modulejava ()). It can also be inferred from the judgment logic that the registerByPlugin must be changed to true

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

The arouter-gradle-plugin plugin is used to create the arouter-gradle plugin.

1. Plug-in entry
public class PluginLaunch implements Plugin<Project> { @Override public void apply(Project project) { def isApp = project.plugins.hasPlugin(AppPlugin) if (isApp) { def android = project.extensions.getByType(AppExtension) // RegisterTransform def transformImpl = new RegisterTransform(project) // ⭐️ ArrayList<ScanSetting> list = new ArrayList<>(3) list.add(new ScanSetting('IRouteRoot')) list.add(new ScanSetting('IInterceptorGroup')) list.add(new ScanSetting('IProviderGroup')) RegisterTransform.registerList = list // Register the transform android. RegisterTransform (transformImpl)}}Copy the code

The key logic is in RegisterTransform. Look what its transform does:

class RegisterTransform extends Transform { Project project static ArrayList<ScanSetting> registerList static File fileContainsInitClass; @Override void transform(Context context, Collection<TransformInput> inputs , Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider , boolean isIncremental) { inputs.each { TransformInput input -> // 1. JarInputs. Each {JarInput JarInput -> String destName = JarInput. Name def hexName = DigestUtils. Md5Hex (jarInput. File. AbsolutePath) / / removed. Jar suffix if (destName. EndsWith (" jar ")) {destName = destName.substring(0, destName.length() - 4) } File src = jarInput.file File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format. The JAR) if (ScanUtil shouldProcessPreDexJar (SRC) absolutePath)) {/ / scanning JAR ScanUtil. ScanJar (SRC, dest) } FileUtils.copyFile(src, dest) } } } // 2. Scan all class files (like JAR types, omitted here)... // 3. }Copy the code
2. Scan files

(1) Iterate over all jar files

static void scanJar(File jarFile, File destFile) { if (jarFile) { def file = new JarFile(jarFile) Enumeration enumeration = file.entries() while (enumeration.hasMoreElements()) { JarEntry jarEntry = (JarEntry) enumeration.nextElement() String entryName = jarEntry.getName() // ROUTER_CLASS_PACKAGE_NAME = 'com/alibaba/android/arouter/routes/' if (entryName.startsWith(ScanSetting.ROUTER_CLASS_PACKAGE_NAME)) { InputStream inputStream = file.getInputStream(jarEntry) // ⭐️ scan scanClass(inputStream) inputstream.close () // GENERATE_TO_CLASS_FILE_NAME = in the ROUTER_CLASS_PACKAGE_NAME package 'com/alibaba/android/arouter/core/LogisticsCenter.class' } else if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == EntryName) {// Scan to logisticScenter. class, Record all the jar files RegisterTransform. FileContainsInitClass = destFile}} file. The close ()}}Copy the code

The key is the scanClass method, which uses ASM to manipulate bytecode:

static void scanClass(InputStream inputStream) { ClassReader cr = new ClassReader(inputStream) ClassWriter cw = new ClassWriter(cr, 0) // Handle ScanClassVisitor CV = new ScanClassVisitor(opcodes.asm5, The cw) / / EXPAND_FRAMES: Unzip these frames cr.accept(CV, ClassReader.EXPAND_FRAMES) inputStream.close() } static class ScanClassVisitor extends ClassVisitor { ScanClassVisitor(int api, ClassVisitor cv) { super(api, cv) } void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, Interfaces) // Iterate over the registry (registered on the plug-in entry) (IRouteRoot IInterceptorGroup IProviderGroup) RegisterTransform. RegisterList. Each {ext - > / / ext:  ScanSetting if (ext.interfaceName && interfaces ! = null) { interfaces.each { itName -> if (itName == ext.interfaceName) { if (! Ext.classlist. contains(name)) {// ⭐️ adds the qualified name to the ScanSetting collection array ext.classlist. add(name)}}}}}}Copy the code

Iterate through the registry list (registered at the plug-in entrance) interfaceName: IRouteRoot, IInterceptorGroup, IProviderGroup

And then store the scanned class name that contains the interfaceName into ScanSetting classList.

(2) Scanning class files is similar to scanning JAR files

(3) Perform classList operation on the results obtained in the above two steps, traverse classList, and use insertInitCodeTo method of RegisterCodeGenerator to insert code into JAR file.

File fileContainsInitClass if (fileContainsInitClass) { registerList.each { ext -> Logger.i('Insert register code to file ' + fileContainsInitClass.absolutePath) if (ext.classList.isEmpty()) { Logger.e("No class implements found for Interface :" + ext.interfacename)} else {// classList not null ext.classList.each {logger.i (it)} RegisterCodeGenerator.insertInitCodeTo(ext) } } } # RegisterCodeGenerator.groovy static void insertInitCodeTo(ScanSetting registerSetting) { if (registerSetting ! = null && ! registerSetting.classList.isEmpty()) { RegisterCodeGenerator processor = new RegisterCodeGenerator(registerSetting) File The file = RegisterTransform. FileContainsInitClass if (file. The getName () endsWith (' jar ')) / / ⭐ ️ inserted into the code for the jar file processor.insertInitCodeIntoJarFile(file) } }Copy the code
3. Insert code

The key step is to see what code is inserted:

private File insertInitCodeIntoJarFile(File jarFile) { if (jarFile) { def optJar = new File(jarFile.getParent(), jarFile.name + ".opt") if (optJar.exists()) optJar.delete() def file = new JarFile(jarFile) Enumeration enumeration = file.entries() JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar)) while (enumeration.hasMoreElements()) { JarEntry jarEntry = (JarEntry) enumeration.nextElement() String entryName = jarEntry.getName() ZipEntry zipEntry = new ZipEntry(entryName) InputStream inputStream = file.getInputStream(jarEntry) jarOutputStream.putNextEntry(zipEntry) // GENERATE_TO_CLASS_FILE_NAME -> LogisticsCenter.class if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) { Logger.i('Insert init code to class >> ' + entryName) // ⭐️ dumping def bytes = referHackWhenInit(inputStream) jaroutputStream.write (bytes)} else { jarOutputStream.write(IOUtils.toByteArray(inputStream)) } inputStream.close() jarOutputStream.closeEntry() } jarOutputStream.close() file.close() if (jarFile.exists()) { jarFile.delete() } optJar.renameTo(jarFile) } return jarFile }Copy the code

The key step is again the referHackWhenInit method, which invents at initialization:

private byte[] referHackWhenInit(InputStream inputStream) { ClassReader cr = new ClassReader(inputStream) ClassWriter cw  = new ClassWriter(cr, 0) ClassVisitor cv = new MyClassVisitor(Opcodes.ASM5, cw) cr.accept(cv, ClassReader.EXPAND_FRAMES) return cw.toByteArray() }Copy the code

The process of intrusion is to modify bytecode technology with ASM, and the whole process is roughly like this:

Process bytecode with MyClassVisitor:

class MyClassVisitor extends ClassVisitor { MyClassVisitor(int api, ClassVisitor cv) { super(api, cv) } void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces) } @Override MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, Exceptions) // ⭐️ do the GENERATE_TO_METHOD_NAME = loadRouterMap method if (name == scansetting.generate_to_method_name) {mv =  new RouteMethodVisitor(Opcodes.ASM5, mv) } return mv } }Copy the code

Use the RouteMethodVisitor to operate on the loadRouterMap method:

class RouteMethodVisitor extends MethodVisitor { RouteMethodVisitor(int api, MethodVisitor mv) { super(api, mv) } @Override void visitInsn(int opcode) { //generate code before return if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) { extension.classList.each { name -> name = name.replaceAll("/", ".") // name push operand stack mv.visitldcinsn (name) // ⭐️ access the logisticScenter.register () method, The last parameter false indicates that it is not the interface mv.visitmethodinsn (opcodes.invokestatic, scansetting.generate_to_class_name, ScanSetting.REGISTER_METHOD_NAME , "(Ljava/lang/String;) V" , false) } } super.visitInsn(opcode) } @Override void visitMaxs(int maxStack, int maxLocals) { super.visitMaxs(maxStack + 4, maxLocals) } }Copy the code

The key step has been revealed. Using ASM to call logisticScenter.register () is equivalent to inserting the register code into the loadRouterMap method. Take the official Demo as an example, the code of piling is as follows:

private static void loadRouterMap() {
    registerByPlugin = false;
    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");
    register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulejava");
    register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulekotlin");
    register("com.alibaba.android.arouter.routes.ARouter$$Providers$$arouterapi");
}
Copy the code

The register method invokes different registration methods based on the generated class name type (IRouteRoot, IProviderGroup, IInterceptorGroup)

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, e);
        }
    }
}
Copy the code

Using the IRouteRoot type as an example, call registerRouteRoot

private static void registerRouteRoot(IRouteRoot routeRoot) { markRegisteredByPlugin(); if (routeRoot ! = null) { routeRoot.loadInto(Warehouse.groupsIndex); } } private static void markRegisteredByPlugin() { if (! registerByPlugin) { registerByPlugin = true; Public class ARouter$$Root$$moduleJava implements IRouteRoot {@override public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) { routes.put("m2", ARouter$$Group$$m2.class); routes.put("module", ARouter$$Group$$module.class); routes.put("test", ARouter$$Group$$test.class); routes.put("yourservicegroupname", ARouter$$Group$$yourservicegroupname.class); }}Copy the code

The registerRouteRoot method sets whether to use the plug-in flag to true, and then adds routing table information written by the developer through annotations to the Warehouse collection.

conclusion

ARouter initially scans dex to find the classes that meet the criteria and completes the storage of registry information. The downside is that the first boot can be time-consuming, especially for low-end models. In addition, scanning dex files may fail for some hardened applications.

The optimization method is cleverly solved through plug-ins, which are very less intrusive. If the plug-in is used, the compiler will automatically register it, and the registry information will be stored. Avoid time-consuming operations like scanning dex files.

This gives me an idea that plug-ins can be considered as a low-intrusion solution when solving problems.

In addition, the visitor pattern, which is an uncommon and difficult design pattern to understand, is appropriate for ASM.

Refer to the blog

Ali ARouter comprehensive comprehensive comprehensive analysis (use introduction + source analysis + design ideas)

One visitor pattern is enough

ARouter’s principle and defect resolution