ARouter groovy plugin 源码笔记

ARouter groovy plugin 源码笔记

21 March 2018 Android

一个 APT 的工具库,随着业务的增大,生成的 .java 就越多,反射注册的 .class 就越多,性能损耗就越厉害...
这是 APT 库无法避免的...
关键都还是在于,反射。注册,并不耗时...

ARouter 就 groovy plugin 就是为了解决这个问题存在的... 当然,也是后来才更新进来的...




ARouter groovy plugin 插件原理


这个插件原理非常简单... 真的是那种 懂的人自然懂,不懂的人自然不懂

  • 整个 APK 扫描,扫出 IRouteRoot IInterceptorGroupIProviderGroupAPT 生成的实现类,并且记录下来。

  • 开始写字节码,找出 LogisticsCenter.class从刚才记录的几千几万个类的信息列表中,挨个开始生成几千几万个对应的注册方法字节码。写到 LogisticsCenter#loadRouterMap「这样,就不需要反射了,编译时就补全了所有注册字节码」。




源码笔记


ScanSetting

package com.camnter.gradle.plugin.arouter.utils

/**
 * 配置类
 * 一些写死的路径 和 配置
 *
 * register setting
 * @author billy.qi email: qiyilike@163.com
 * @since 17/3/28 11:48
 */
class ScanSetting {
    static final String PLUGIN_NAME = "com.alibaba.arouter"
    /**
     * The register code is generated into this class
     * */
    static final String GENERATE_TO_CLASS_NAME = 'com/alibaba/android/arouter/core/LogisticsCenter'
    /**
     * you know. this is the class file(or entry in jar file) name
     * */
    static final String GENERATE_TO_CLASS_FILE_NAME = GENERATE_TO_CLASS_NAME + '.class'
    /**
     * The register code is generated into this method
     * */
    static final String GENERATE_TO_METHOD_NAME = 'loadRouterMap'
    /**
     * The package name of the class generated by the annotationProcessor
     * */
    static final String ROUTER_CLASS_PACKAGE_NAME = 'com/alibaba/android/arouter/routes/'
    /**
     * The package name of the interfaces
     * */
    private static final INTERFACE_PACKAGE_NAME = 'com/alibaba/android/arouter/facade/template/'

    /**
     * register method name in class: {@link #GENERATE_TO_CLASS_NAME}
     */
    static final String REGISTER_METHOD_NAME = 'register'
    /**
     * scan for classes which implements this interface
     * */
    String interfaceName = ''

    /**
     * jar file which contains class: {@link #GENERATE_TO_CLASS_NAME}
     */
    File fileContainsInitClass
    /**
     * scan result for {@link #interfaceName}
     * class names in this list
     * */
    ArrayList<String> classList = new ArrayList<>()

    /**
     * constructor for arouter-auto-register settings
     * @param interfaceName interface to scan
     */
    ScanSetting(String interfaceName) {
        this.interfaceName = INTERFACE_PACKAGE_NAME + interfaceName
    }
}


ScanUtil


扫描出 IRouteRoot IInterceptorGroup 和 IProviderGroup 实现类


package com.camnter.gradle.plugin.arouter.utils

import com.camnter.gradle.plugin.arouter.core.RegisterTransform
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Opcodes

import java.util.jar.JarEntry
import java.util.jar.JarFile

/**
 * 扫描工具类
 *
 * Scan all class in the package: com/alibaba/android/arouter/
 * find out all routers,interceptors and providers
 * @author billy.qi email: qiyilike@163.com
 * @since 17/3/20 11:48
 */
class ScanUtil {

    /**
     * 扫描 jar 中的 class
     * 这些 class 是否是 IRouteRoot IInterceptorGroup 和 IProviderGroup 实现类
     * 是的话,记录 该 class
     *
     * scan jar file
     * @param jarFile All jar files that are compiled into apk
     * @param destFile dest file after this transform
     */
    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) {
                    // mark this jar file contains LogisticsCenter.class
                    // After the scan is complete, we will generate register code into this file
                    RegisterTransform.fileContainsInitClass = destFile
                }
            }
            file.close()
        }
    }

    /**
     * 根据不包含 "com.android.support" 或 "/android/m2repository"
     * 判断出是不是本项目 class,是的话扫描 jar 中的 class
     *
     * @param path path
     * @return boolean
     */
    static boolean shouldProcessPreDexJar(String path) {
        return !path.contains("com.android.support") && !path.contains("/android/m2repository")
    }

    /**
     * 是否是 APT 生成目录
     *
     * @param entryName entryName
     * @return boolean
     */
    static boolean shouldProcessClass(String entryName) {
        return entryName != null && entryName.startsWith(ScanSetting.ROUTER_CLASS_PACKAGE_NAME)
    }

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

    /**
     * ASM 扫描类
     *
     * @param inputStream
     */
    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()
    }

    /**
     * 扫描 class 归类
     * 归类到对应的 ScanSetting 内
     *
     * 比如 IRouteRoot class 就归类到对应 ScanSetting 内中的 List 内
     */
    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) {
                            ext.classList.add(name)
                        }
                    }
                }
            }
        }
    }

}


RegisterTransform


Jar 方面
根据不包含 "com.android.support" 或 "/android/m2repository"
判断出是不是本项目 class,是的话扫描 jar 中的 class
这些 class 是否是 IRouteRoot IInterceptorGroup 和 IProviderGroup 实现类
是的话,记录 该 class

File 方面
判断 File 是否是 IRouteRoot IInterceptorGroup 和 IProviderGroup 实现类
是的话,记录 该 class

如果存在 com/alibaba/android/arouter/core/LogisticsCenter.class
则开始生成 对应注册方法「用刚才记录的 class」


package com.camnter.gradle.plugin.arouter.core

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import com.camnter.gradle.plugin.arouter.utils.ScanSetting
import com.camnter.gradle.plugin.arouter.utils.ScanUtil
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project

/**
 * 扫描 ARouter 生成的 IRouteRoot IInterceptorGroup 和 IProviderGroup 实现类
 * 然后在 com/alibaba/android/arouter/core/LogisticsCenter.class 生成注册方法
 *
 * transform api
 * <p>
 *     1. Scan all classes to find which classes implement the specified interface
 *     2. Generate register code into class file: {@link ScanSetting#GENERATE_TO_CLASS_FILE_NAME}
 * @author billy.qi email: qiyilike@163.com
 * @since 17/3/21 11:48
 */
class RegisterTransform extends Transform {

    Project project
    static ArrayList<ScanSetting> registerList
    static File fileContainsInitClass

    RegisterTransform(Project project) {
        this.project = project
    }

    /**
     * name of this transform
     * @return
     */
    @Override
    String getName() {
        return ScanSetting.PLUGIN_NAME
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * The plugin will scan all classes in the project
     * @return
     */
    @Override
    Set<QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    /**
     * Jar 方面
     * 根据不包含 "com.android.support" 或 "/android/m2repository"
     * 判断出是不是本项目 class,是的话扫描 jar 中的 class
     * 这些 class 是否是 IRouteRoot IInterceptorGroup 和 IProviderGroup 实现类
     * 是的话,记录 该 class
     *
     * File 方面
     * 判断 File 是否是 IRouteRoot IInterceptorGroup 和 IProviderGroup 实现类
     * 是的话,记录 该 class
     *
     * 如果存在 com/alibaba/android/arouter/core/LogisticsCenter.class
     * 则开始生成 对应注册方法「用刚才记录的 class」
     *
     * @param context context
     * @param inputs inputs
     * @param referencedInputs referencedInputs
     * @param outputProvider outputProvider
     * @param isIncremental isIncremental
     * @throws IOException IOException
     * @throws TransformException TransformException
     * @throws InterruptedException InterruptedException
     */
    @Override
    void transform(Context context, Collection<TransformInput> inputs
            , Collection<TransformInput> referencedInputs
            , TransformOutputProvider outputProvider
            , boolean isIncremental) throws IOException, TransformException, InterruptedException {

        Logger.i('Start scan register info in jar file.')

        long startTime = System.currentTimeMillis()
        boolean leftSlash = File.separator == '/'


        inputs.each { TransformInput input ->

            /**
             * Jar 方面
             * 根据不包含 "com.android.support" 或 "/android/m2repository"
             * 判断出是不是本项目 class,是的话扫描 jar 中的 class
             * 这些 class 是否是 IRouteRoot IInterceptorGroup 和 IProviderGroup 实现类
             * 是的话,记录 该 class
             * */
            // 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)
            }

            /**
             * File 方面
             * 判断 File 是否是 IRouteRoot IInterceptorGroup 和 IProviderGroup 实现类
             * 是的话,记录 该 class
             * */
            // 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)
            }
        }

        Logger.i(
                'Scan finish, current cost time ' + (System.currentTimeMillis() - startTime) + "ms")

        /**
         * 如果存在 com/alibaba/android/arouter/core/LogisticsCenter.class
         * 则开始生成 对应注册方法「用刚才记录的 class」
         * */
        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 {
                    ext.classList.each {
                        Logger.i(it)
                    }
                    RegisterCodeGenerator.insertInitCodeTo(ext)
                }
            }
        }

        Logger.i("Generate code finish, current cost time: " + (System.currentTimeMillis() -
                startTime) + "ms")
    }
}


RegisterCodeGenerator


解 jar
拿出一个一个 class
然后得到对应的 InputStream
在 LogisticsCenter 的 loadRouterMap 方法内,进行 ASM 操作,添加注册代码


package com.camnter.gradle.plugin.arouter.core

import com.camnter.gradle.plugin.arouter.utils.ScanSetting
import org.apache.commons.io.IOUtils
import org.objectweb.asm.*

import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry

/**
 * 为 扫描 ARouter 生成的 IRouteRoot IInterceptorGroup 和 IProviderGroup 实现类
 * 生成对应的 注册代码
 *
 * generate register code into LogisticsCenter.class
 * @author billy.qi email: qiyilike@163.com
 */
class RegisterCodeGenerator {
    ScanSetting extension

    private RegisterCodeGenerator(ScanSetting extension) {
        this.extension = extension
    }

    /**
     * 找到 LogisticsCenter.class
     * 插入 注册代码
     *
     * @param registerSetting registerSetting
     */
    static void insertInitCodeTo(ScanSetting registerSetting) {
        if (registerSetting != null && !registerSetting.classList.isEmpty()) {
            RegisterCodeGenerator processor = new RegisterCodeGenerator(registerSetting)
            File file = RegisterTransform.fileContainsInitClass
            if (file.getName().endsWith('.jar')) processor.insertInitCodeIntoJarFile(file)
        }
    }

    /**
     * 解 jar
     * 拿出一个一个 class
     * 然后得到对应的 InputStream
     * 然后进行 ASM 操作
     * 在 LogisticsCenter 的 loadRouterMap 方法内添加 注册代码
     *
     * generate code into jar file
     * @param jarFile the jar file which contains LogisticsCenter.class
     * @return File
     */
    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) {

                    Logger.i('Insert init code to class >> ' + 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
    }

    /**
     * refer hack class when object init
     *
     * ASM 操作
     * 在 LogisticsCenter 的 loadRouterMap 方法内添加 注册代码
     *
     * @param inputStream inputStream
     * @return byte[]
     */
    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()
    }

    /**
     * 自定义 ClassVisitor
     * 主要覆写 visitMethod 方法
     *
     * 然后,当遇到 loadRouterMap 方法的时候
     * return 一个自定义 MethodVisitor
     */
    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
        }
    }

    /**
     * 自定义 MethodVisitor
     * 用来给 loadRouterMap 方法添加 注册代码
     */
    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) //类名
                    // 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)
        }
    }
}




小结


这个 ScanUtil 修一修,还是可以复用到下个项目的类似应用场景的。毕竟,都需要全 APK 代码,正好 transform 就可以完成这件事。