I Arouter framework analysis

Arouter frame structure

There are annotation definitions and annotation processor related contents in arouter framework. Arouter itself can also be regarded as an example.

Arouter API initializes arouter

The Arouter framework uses static annotation processing. In order to adapt to multiple modules, it uses the moduleName suffix to generate a set of registration classes with unified rules. These registration classes are distributed within their respective modules. A management class is needed to aggregate them to provide a unified registration and call entry.

Initialization entry

The integration of the Arouter routing framework calls for initialization of the Arouter framework in the Application initialization process by calling the following method.

ARouter.init(sInstance);

protected static synchronized boolean init(Application application) {
    mContext = application;
    LogisticsCenter.init(mContext, executor);
    logger.info(Consts.TAG, "ARouter init success!");
    hasInit = true;
    mHandler = new Handler(Looper.getMainLooper());
    return true;
}

Dynamic scan route registration class

The routing table is initialized in logisticscenter init(mContext, executor); Complete in. Note the following judgment:

ARouter.debuggable() || PackageUtils.isNewVersion(context). The routing table will be updated only after debugging or updating the app version. The scanned routing file list is saved in SharedPreference.

  • registerByPlugin is com alibaba. The arouter plug-in flag indicates whether the routing table registration has been carried out in the compilation stage. Skip it directly;
  • Use ClassUtils to scan all class files in the package(com.alibaba.android.arouter.routes) - because the routes of all modules are created in this package path;
  • Generate instances of all scanned files according to the rules and register them in the manager Warehouse;
/**
 * LogisticsCenter init, load all metas in memory. Demand initialization
 */
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
    // With deletion
    loadRouterMap();
    if (!registerByPlugin) {
	Set<String> routerMap;
        if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
            routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
            if (!routerMap.isEmpty()) {
                context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
            }
            PackageUtils.updateVersion(context);    // Save new version name when router map update finishes.
	} else {
            routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
        }
        for (String className : routerMap) {
            if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
            	((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
            } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
                ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
            } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
            	((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
            }
        }
    }
}

Using the arouter register plug-in

Arouter register is the implementation of AutoRegister plug-in in arouter framework. Its main purpose is to complete the initialization of routing table in the compilation stage and reduce the time-consuming of arouter initialization.

Routing file and initialization class scanning

@Override
void transform(Context context, Collection<TransformInput> inputs , Collection<TransformInput> referencedInputs,
               TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {

    // With deletion
    boolean leftSlash = File.separator == '/'
    if (!isIncremental){
        outputProvider.deleteAll()
    }

    inputs.each { TransformInput input ->

        // scan all jars
        input.jarInputs.each { JarInput jarInput ->
            String destName = jarInput.name
            // rename jar files
            def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath)
            if (destName.endsWith(".jar")) {
                destName = destName.substring(0, destName.length() - 4)
            }
            // input file
            File src = jarInput.file
            // output file
            File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)

            //scan jar file to find classes
            if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) {
                ScanUtil.scanJar(src, dest)
            }
            FileUtils.copyFile(src, dest)

        }
        // scan class files
        input.directoryInputs.each { DirectoryInput directoryInput ->
            File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
            String root = directoryInput.file.absolutePath
            if (!root.endsWith(File.separator))
                root += File.separator
            directoryInput.file.eachFileRecurse { File file ->
                def path = file.absolutePath.replace(root, '')
                if (!leftSlash) {
                    path = path.replaceAll("\\", "/")
                }
                if(file.isFile() && ScanUtil.shouldProcessClass(path)){
                    ScanUtil.scanClass(file)
                }
            }

            // copy to dest
            FileUtils.copyDirectory(directoryInput.file, dest)
        }
    }

    if (fileContainsInitClass) {
        registerList.each { ext ->
            if (!ext.classList.isEmpty()) {
                ext.classList.each {
                    Logger.i(it)
                }
                RegisterCodeGenerator.insertInitCodeTo(ext)
            }
        }
    }
}

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()
            if (entryName.startsWith(ScanSetting.ROUTER_CLASS_PACKAGE_NAME)) {
                InputStream inputStream = file.getInputStream(jarEntry)
                scanClass(inputStream)
                inputStream.close()
            } else if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) {
				// com/alibaba/android/arouter/core/LogisticsCenter
                RegisterTransform.fileContainsInitClass = destFile
            }
        }
        file.close()
    }
}

/**
 * scan class file
 * @param class file
 */
static void scanClass(File file) {
    scanClass(new FileInputStream(file))
}

static void scanClass(InputStream inputStream) {
    ClassReader cr = new ClassReader(inputStream)
    ClassWriter cw = new ClassWriter(cr, 0)
    ScanClassVisitor cv = new ScanClassVisitor(Opcodes.ASM5, cw)
    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)
        RegisterTransform.registerList.each { ext ->
            if (ext.interfaceName && interfaces != null) {
                interfaces.each { itName ->
                    if (itName == ext.interfaceName) {
                        //fix repeated inject init code when Multi-channel packaging
                        if (!ext.classList.contains(name)) {
                            ext.classList.add(name)
                        }
                    }
                }
            }
        }
    }
}

Target file bytecode operation

Through the above scanning operation: get the path to the class generated by Arouter framework and store it in registertransform The ScanSetting object in registerlist and the file where the Arouter API initialization class LogisticsCenter is located are controlled by registertransform Filecontainsinitclass holds. After the scan is completed, call RegisterCodeGenerator.. Insertinitcodeto (EXT), traversing registertransform The ScanSetting object in registerlist is used as input to read and write the file where the LogisticsCenter class is located.

// Reading and writing process: create a temporary file optJar, read data from the source file jarFile, and transfer it to the temporary optJar file. After completion, use optJar to overwrite the source file jarFile.
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)
            if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) {
                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
}

// Find COM / Alibaba / Android / arouter / core / logisticscenter Class, call this method
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()
}

// In logisticscenter Class to find the loadRouterMap method
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)
        //generate code into this method
        if (name == ScanSetting.GENERATE_TO_METHOD_NAME) {
            mv = new RouteMethodVisitor(Opcodes.ASM5, mv)
        }
        return mv
    }
}

// Insert statement into loadRouterMap: register (classname)
// The register function is defined in the LogisticsCenter. It is used to generate an instance of a given class name and register it in the Warehouse manager.
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("/", ".")
                mv.visitLdcInsn(name)//Class name
                // generate invoke register method into LogisticsCenter.loadRouterMap()
                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)
    }
}

Arouter register and arouter API initialize arouter

Arouter register is an implementation of AutoRegister. You can refer to it.

  • During runtime, arouter API performs initialization operations by scanning class files.
  • Arouter-register scans jar files and at the end of the compilation phase Class file, find the route registration class in logisticscenter Insert logisticscenter. In the class#loadRouterMap() method The class #register (classname) call statement enables loadRouterMap to directly complete route registration, saving the time of runtime scanning.
  • Arouter register works on the basis of arouter API. Both register(className) and loadRouterMap() are provided by arouter API library.

Method records used in plug-ins

JarFile

  • file. Entries() - > enumeration: packaged in jar file class file collection
  • enumeration. Nexterelement() - > jarentry: packaged in jar file class file

Class access

Use classvisitor, ClassReader and ClassWriter pairs class files are read and written in this group of classes.

void scanClass(InputStream inputStream) {
    ClassReader cr = new ClassReader(inputStream)
    ClassWriter cw = new ClassWriter(cr, 0)
    ScanClassVisitor cv = new ScanClassVisitor(Opcodes.ASM5, cw)
    cr.accept(cv, ClassReader.EXPAND_FRAMES)
    inputStream.close()
}

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()
}

static class MyClassVisitor extends ClassVisitor {

    ScanClassVisitor(int api, ClassVisitor cv) {
        super(api, cv)
    }

    @Override
    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)
        //generate code into this method
        if (name == ScanSetting.GENERATE_TO_METHOD_NAME) {
            mv = new RouteMethodVisitor(Opcodes.ASM5, mv)
        }
        return mv
    }
}

Method access

Use MethodVisitor to read and write class methods and insert code.

class RouteMethodVisitor extends MethodVisitor {

    RouteMethodVisitor(int api, MethodVisitor mv) {
        super(api, mv)
    }

    @Override
    void visitInsn(int opcode) {
        super.visitInsn(opcode)
    }
    @Override
    void visitMaxs(int maxStack, int maxLocals) {
        super.visitMaxs(maxStack + 4, maxLocals)
    }
}

ASM Library

Arouter register plug-in is implemented based on bytecode stake technology. ASM is a plug-in framework, which is integrated in com android. tools. Build: in gradle, the following table has been deleted.

  • Byte code pile insertion technology
  • ASM: a framework implementation of bytecode instrumentation
\--- com.android.tools.build:gradle:2.1.3
     \--- com.android.tools.build:gradle-core:2.1.3
          +--- com.android.tools.build:builder:2.1.3
          |    +--- org.ow2.asm:asm:5.0.3
          |    \--- org.ow2.asm:asm-tree:5.0.3
          |         \--- org.ow2.asm:asm:5.0.3
          +--- org.ow2.asm:asm:5.0.3
          +--- org.ow2.asm:asm-commons:5.0.3
          |    \--- org.ow2.asm:asm-tree:5.0.3 (*)
          +--- net.sf.proguard:proguard-gradle:5.2.1
          |    \--- net.sf.proguard:proguard-base:5.2.1
          +--- org.jacoco:org.jacoco.core:0.7.6.201602180812
          |    \--- org.ow2.asm:asm-debug-all:5.0.4
          \--- org.antlr:antlr:3.5.2
               +--- org.antlr:antlr-runtime:3.5.2
               \--- org.antlr:ST4:4.0.8 (*)

This article is transferred from https://juejin.cn/post/7044826883719954469 , in case of infringement, please contact to delete.

Keywords: Java Android Apache Programmer

Added by devai on Sat, 22 Jan 2022 06:23:19 +0200