一种更高效的组件自动注册方案(android组件化开发)

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

摘要:

在编译时,扫描即将打包到apk中的所有类,将所有组件类收集起来,通过修改字节码的方式生成注册代码到组件管理类中,从而实现编译时自动注册的功能,不用再关心项目中有哪些组件类了。
特点:不需要注解,不会增加新的类;性能高,不需要反射,运行时直接调用组件的构造方法;能扫描到所有类,不会出现遗漏;支持分级按需加载功能的实现。

前言


最近在公司做android组件化开发框架的搭建,采用组件总线的方式进行通信:提供一个基础库,各组件(IComponent接口的实现类)都注册到组件管理类(组件总线:ComponentManager)中,组件之间在同一个app内时,通过ComponentManager转发调用请求来实现通信(不同app之间的通信方式不是本文的主题,暂且略去)。但在实现过程中遇到了一个问题:

如何将不同module中的组件类自动注册到ComponentManager中

目前市面上比较常用的解决方案是使用annotationProcessor:通过编译时注解动态生成组件映射表代码的方式来实现。但尝试过后发现有问题,因为编译时注解的特性只在源码编译时生效,无法扫描到aar包里的注解(project依赖、maven依赖均无效),也就是说必须每个module编译时生成自己的代码,然后要想办法将这些分散在各aar种的类找出来进行集中注册。

ARouter的解决方案是:

  • 每个module都生成自己的java类,这些类的包名都是’com.alibaba.android.arouter.routes’
  • 然后在运行时通过读取每个dex文件中的这个包下的所有类通过反射来完成映射表的注册,详见ClassUtils.java源码

运行时通过读取所有dex文件遍历每个entry查找指定包内的所有类名,然后反射获取类对象。这种效率看起来并不高。

ActivityRouter的解决方案是(demo中有2个组件名为’app’和’sdk’):

  • 在主app module中有一个@Modules({"app", "sdk"})注解用来标记当前app内有多少组件,根据这个注解生成一个RouterInit类
  • 在RouterInit类的init方法中生成调用同一个包内的RouterMapping_app.map
  • 每个module生成的类(RouterMapping_app.java 和 RouterMapping_sdk.java)都放在com.github.mzule.activityrouter.router包内(在不同的aar中,但包名相同)
  • 在RouterMapping_sdk类的map()方法中根据扫描到的当前module内所有路由注解,生成了调用Routers.map(…)方法来注册路由的代码
  • 在Routers的所有api接口中最终都会触发RouterInit.init()方法,从而实现所有路由的映射表注册

这种方式用一个RouterInit类组合了所有module中的路由映射表类,运行时效率比扫描所有dex文件的方式要高,但需要额外在主工程代码中维护一个组件名称列表注解: @Modules({“app”, “sdk”})

有没有一种方式可以更高效地管理这个列表呢?

联想到之前用ASM框架自动生成代码的方式做了个AndAop插件用于自动插入指定代码到任意类的任意方法中,于是写了一个自动生成注册组件的gradle插件。
大致思路是:在编译时,扫描所有类,将符合条件的类收集起来,并通过修改字节码生成注册代码到指定的管理类中,从而实现编译时自动注册的功能,不用再关心项目中有哪些组件类了。不会增加新的class,不需要反射,运行时直接调用组件的构造方法。

性能方面:由于使用效率更高的ASM框架来进行字节码分析和修改,并过滤掉android/support包中的所有类(还支持设置自定义的扫描范围),经公司项目实测,未代码混淆前所有dex文件总计12MB左右,扫描及代码插入的总耗时在2s-3s之间,相对于整个apk打包所花3分钟左右的时间来说可以忽略不计(运行环境:MacBookPro 15吋高配 Mid 2015)。

开发完成后,考虑到这个功能的通用性,于是升级组件扫描注册插件为通用的自动注册插件AutoRegister,支持配置多种类型的扫描注册,使用方式见github中的README文档。此插件现已用到组件化开发框架: CC

升级后,AutoRegister插件的完整功能描述是:

在编译期扫描即将打包到apk中的所有类,并将指定接口的实现类(或指定类的子类)通过字节码操作自动注册到对应的管理类中。尤其适用于命令模式或策略模式下的映射表生成。

在组件化开发框架中,可有助于实现分级按需加载的功能:

  • 在组件管理类中生成组件自动注册的代码
  • 在组件框架第一次被调用时加载此注册表
  • 若组件中有很多功能提供给外部调用,可以将这些功能包装成多个Processor,并将它们自动注册到组件中进行管理
  • 组件被初次调用时再加载这些Processor

实现过程


第一步:准备工作

  1. 首先要知道如何使用Android Studio开发Gradle插件
  2. 了解TransformAPI:Transform API是从Gradle 1.5.0版本之后提供的,它允许第三方在打包Dex文件之前的编译过程中修改java字节码(自定义插件注册的transform会在ProguardTransform和DexTransform之前执行,所以自动注册的类不需要考虑混淆的情况).参考文章有:
  3. 字节码修改框架(相比于Javassist框架ASM较难上手,但性能更高,但相学习难度阻挡不了我们对性能的追求):

第二步:构建插件工程

  1. 按照如何使用Android Studio开发Gradle插件文章中的方法创建好插件工程并发布到本地maven仓库(我是放在工程根目录下的一个文件夹中),这样我们就可以在本地快速调试了

build.gradle文件的部分内容如下:

apply plugin: 'groovy'
apply plugin: 'maven'

dependencies {
compile gradleApi()
compile localGroovy()
}

repositories {
mavenCentral()
}
dependencies {
compile ‘com.android.tools.build:gradle:2.2.0’
}

//加载本地maven私服配置(在工程根目录中的local.properties文件中进行配置)
Properties properties = new Properties()
properties.load(project.rootProject.file(‘local.properties’).newDataInputStream())
def artifactory_user = properties.getProperty(“artifactory_user”)
def artifactory_password = properties.getProperty(“artifactory_password”)
def artifactory_contextUrl = properties.getProperty(“artifactory_contextUrl”)
def artifactory_snapshot_repoKey = properties.getProperty(“artifactory_snapshot_repoKey”)
def artifactory_release_repoKey = properties.getProperty(“artifactory_release_repoKey”)

def maven_type_snapshot = true
// 项目引用的版本号,比如compile 'com.yanzhenjie:andserver:1.0.1’中的1.0.1就是这里配置的。
def artifact_version=‘1.0.1’
// 唯一包名,比如compile 'com.yanzhenjie:andserver:1.0.1’中的com.yanzhenjie就是这里配置的。
def artifact_group = ‘com.billy.android’
def artifact_id = ‘autoregister’
def debug_flag = true //true: 发布到本地maven仓库, false: 发布到maven私服

task sourcesJar(type: Jar) {
from project.file(‘src/main/groovy’)
classifier = ‘sources’
}

artifacts {
archives sourcesJar
}
uploadArchives {
repositories {
mavenDeployer {
//deploy到maven仓库
if (debug_flag) {
repository(url: uri(’…/repo-local’)) //deploy到本地仓库
} else { //deploy到maven私服中
def repoKey = maven_type_snapshot ? artifactory_snapshot_repoKey : artifactory_release_repoKey
repository(url: a r t i f a c t o r y c o n t e x t U r l / {artifactory_contextUrl}/ artifactorycontextUrl/{repoKey}”) {
authentication(userName: artifactory_user, password: artifactory_password)
}
}

        pom.groupId = artifact_group
        pom.artifactId = artifact_id
        pom.version = artifact_version + (maven_type_snapshot ? <span class="hljs-string">'-SNAPSHOT'</span> : <span class="hljs-string">''</span>)

        pom.project {
            licenses {
                license {
                    name <span class="hljs-string">'The Apache Software License, Version 2.0'</span>
                    url <span class="hljs-string">'http://www.apache.org/licenses/LICENSE-2.0.txt'</span>
                }
            }
        }
    }
}

}

    根目录的build.gradle文件中要添加本地仓库的地址及dependencies

    buildscript {
    
    repositories {
        maven{ url rootProject.file("repo-local") }</span>
        maven <span class="hljs-cell">{ url <span class="hljs-string">'http://maven.aliyun.com/nexus/content/groups/public/'</span> }</span>
        google()
        jcenter()
    }
    dependencies <span class="hljs-cell">{
        classpath <span class="hljs-string">'com.android.tools.build:gradle:3.0.0-beta6'</span>
        classpath <span class="hljs-string">'com.github.dcendents:android-maven-gradle-plugin:1.4.1'</span>
        classpath <span class="hljs-string">'com.billy.android:autoregister:1.0.1'</span>
    }</span>
    

    }

    2.在Transform类的transform方法中添加类扫描相关的代码

    // 遍历输入文件
    inputs.each { TransformInput input ->
    
    <span class="hljs-comment">// 遍历jar</span>
    input.jarInputs.each { JarInput jarInput -&gt;
        String destName = jarInput.name
        <span class="hljs-comment">// 重名名输出文件,因为可能同名,会覆盖</span>
        def hexName = DigestUtils.md5Hex(jarInput.<span class="hljs-keyword">file</span>.absolutePath)
        <span class="hljs-keyword">if</span> (destName.<span class="hljs-keyword">endsWith</span>(<span class="hljs-string">".jar"</span>)) {
            destName = destName.<span class="hljs-keyword">substring</span>(<span class="hljs-number">0</span>, destName.length() - <span class="hljs-number">4</span>)
        }
        <span class="hljs-comment">// 获得输入文件</span>
        File src = jarInput.<span class="hljs-keyword">file</span>
        <span class="hljs-comment">// 获得输出文件</span>
        File dest = outputProvider.getContentLocation(destName + <span class="hljs-string">"_"</span> + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
    
        <span class="hljs-comment">//遍历jar的字节码类文件,找到需要自动注册的component</span>
        <span class="hljs-keyword">if</span> (CodeScanProcessor.shouldProcessPreDexJar(src.absolutePath)) {
            CodeScanProcessor.scanJar(src, dest)
        }
        FileUtils.copyFile(src, dest)
    
        project.logger.info <span class="hljs-string">"Copying\t${src.absolutePath} \nto\t\t${dest.absolutePath}"</span>
    }
    <span class="hljs-comment">// 遍历目录</span>
    input.directoryInputs.each { DirectoryInput directoryInput -&gt;
        <span class="hljs-comment">// 获得产物的目录</span>
        File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
        String root = directoryInput.<span class="hljs-keyword">file</span>.absolutePath
        <span class="hljs-keyword">if</span> (!root.<span class="hljs-keyword">endsWith</span>(File.<span class="hljs-keyword">separator</span>))
            root += File.<span class="hljs-keyword">separator</span>
        <span class="hljs-comment">//遍历目录下的每个文件</span>
        directoryInput.<span class="hljs-keyword">file</span>.eachFileRecurse { File <span class="hljs-keyword">file</span> -&gt;
            def path = <span class="hljs-keyword">file</span>.absolutePath.replace(root, <span class="hljs-string">''</span>)
            <span class="hljs-keyword">if</span>(<span class="hljs-keyword">file</span>.isFile()){
                CodeScanProcessor.checkInitClass(path, new File(dest.absolutePath + File.<span class="hljs-keyword">separator</span> + path))
                <span class="hljs-keyword">if</span> (CodeScanProcessor.shouldProcessClass(path)) {
                    CodeScanProcessor.scanClass(<span class="hljs-keyword">file</span>)
                }
            }
        }
        project.logger.info <span class="hljs-string">"Copying\t${directoryInput.file.absolutePath} \nto\t\t${dest.absolutePath}"</span>
        <span class="hljs-comment">// 处理完后拷到目标文件</span>
        FileUtils.copyDirectory(directoryInput.<span class="hljs-keyword">file</span>, dest)
    }
    

    }

    CodeScanProcessor是一个工具类,其中CodeScanProcessor.scanJar(src, dest)CodeScanProcessor.scanClass(file)分别是用来扫描jar包和class文件的
    扫描的原理是利用ASM的ClassVisitor来查看每个类的父类类名及所实现的接口名称,与配置的信息进行比较,如果符合我们的过滤条件,则记录下来,在全部扫描完成后将调用这些类的无参构造方法进行注册

    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.infoList.each { ext ->
    if (shouldProcessThisClassForRegister(ext, name)) {
    if (superName != ‘java/lang/Object’ && !ext.superClassNames.isEmpty()) {
    for (int i = 0; i < ext.superClassNames.size(); i++) {
    if (ext.superClassNames.get(i) == superName) {
    ext.classList.add(name)
    return
    }
    }
    }
    if (ext.interfaceName && interfaces != null) {
    interfaces.each { itName ->
    if (itName == ext.interfaceName) {
    ext.classList.add(name)
    }
    }
    }
    }
    }

    }
    

    }

      3.记录目标类所在的文件,因为我们接下来要修改其字节码,将注册代码插入进去

       static void checkInitClass(String entryName, File file) {
           if (entryName == null || !entryName.endsWith(".class"))
               return
           entryName = entryName.substring(0, entryName.lastIndexOf('.'))
           RegisterTransform.infoList.each { ext ->
               if (ext.initClassName == entryName)
                   ext.fileContainsInitClass = file
           }
       }
       
       

      4.扫描完成后,开始修改目标类的字节码(使用ASM的MethodVisitor来修改目标类指定方法,若未指定则默认为static块,即<clinit>方法),生成的代码是直接调用扫描到的类的无参构造方法,并非通过反射

      • class文件: 直接修改此字节码文件(其实是重新生成一个class文件并替换掉原来的文件)
      • jar文件:复制此jar文件,找到jar包中目标类所对应的JarEntry,修改其字节码,然后替换原来的jar文件
      
      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
      /**
      *

      • @author billy.qi

      • @since 17/3/20 11:48
        */
        class CodeInsertProcessor {
        RegisterInfo extension

        private CodeInsertProcessor(RegisterInfo extension) {
        this.extension = extension
        }

        static void insertInitCodeTo(RegisterInfo extension) {
        if (extension != null && !extension.classList.isEmpty()) {
        CodeInsertProcessor processor = new CodeInsertProcessor(extension)
        File file = extension.fileContainsInitClass
        if (file.getName().endsWith(’.jar’))
        processor.insertInitCodeIntoJarFile(file)
        else
        processor.insertInitCodeIntoClassFile(file)
        }
        }

        //处理jar包中的class代码注入
        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))

             <span class="hljs-keyword">while</span> (enumeration.hasMoreElements()) {
                 JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                 String entryName = jarEntry.getName()
                 ZipEntry zipEntry = <span class="hljs-keyword">new</span> ZipEntry(entryName)
                 InputStream inputStream = file.getInputStream(jarEntry)
                 jarOutputStream.putNextEntry(zipEntry)
                 <span class="hljs-keyword">if</span> (isInitClass(entryName)) {
                     println(<span class="hljs-string">'codeInsertToClassName:'</span> + entryName)
                     <span class="hljs-keyword">def</span> bytes = referHackWhenInit(inputStream)
                     jarOutputStream.write(bytes)
                 } <span class="hljs-keyword">else</span> {
                     jarOutputStream.write(IOUtils.toByteArray(inputStream))
                 }
                 inputStream.close()
                 jarOutputStream.closeEntry()
             }
             jarOutputStream.close()
             file.close()
        
             <span class="hljs-keyword">if</span> (jarFile.exists()) {
                 jarFile.delete()
             }
             optJar.renameTo(jarFile)
         }
         <span class="hljs-keyword">return</span> jarFile
        

        }

        boolean isInitClass(String entryName) {
        if (entryName == null || !entryName.endsWith(".class"))
        return false
        if (extension.initClassName) {
        entryName = entryName.substring(0, entryName.lastIndexOf(’.’))
        return extension.initClassName == entryName
        }
        return false
        }
        /**

        • 处理class的注入

        • @param file class文件

        • @return 修改后的字节码文件内容
          */
          private byte[] insertInitCodeIntoClassFile(File file) {
          def optClass = new File(file.getParent(), file.name + “.opt”)

          FileInputStream inputStream = new FileInputStream(file)
          FileOutputStream outputStream = new FileOutputStream(optClass)

          def bytes = referHackWhenInit(inputStream)
          outputStream.write(bytes)
          inputStream.close()
          outputStream.close()
          if (file.exists()) {
          file.delete()
          }
          optClass.renameTo(file)
          return bytes
          }

        //refer hack class when object init
        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()
        }

        class MyClassVisitor extends ClassVisitor {

         MyClassVisitor(int api, ClassVisitor cv) {
             <span class="hljs-keyword">super</span>(api, cv)
         }
        
         void visit(int version, int access, String name, String signature,
                    String superName, String[] interfaces) {
             <span class="hljs-keyword">super</span>.visit(version, access, name, signature, superName, interfaces)
         }
         <span class="hljs-annotation">@Override</span>
         MethodVisitor visitMethod(int access, String name, String desc,
                                   String signature, String[] exceptions) {
             MethodVisitor mv = <span class="hljs-keyword">super</span>.visitMethod(access, name, desc, signature, exceptions)
             <span class="hljs-keyword">if</span> (name == extension.initMethodName) { <span class="hljs-comment">//注入代码到指定的方法之中</span>
                 boolean _static = (access &amp; Opcodes.ACC_STATIC) &gt; <span class="hljs-number">0</span>
                 mv = <span class="hljs-keyword">new</span> MyMethodVisitor(Opcodes.ASM5, mv, _static)
             }
             <span class="hljs-keyword">return</span> mv
         }
        

        }

        class MyMethodVisitor extends MethodVisitor {
        boolean _static;

         MyMethodVisitor(int api, MethodVisitor mv, boolean _static) {
             <span class="hljs-keyword">super</span>(api, mv)
             <span class="hljs-keyword">this</span>._static = _static;
         }
        
         <span class="hljs-annotation">@Override</span>
         void visitInsn(int opcode) {
             <span class="hljs-keyword">if</span> ((opcode &gt;= Opcodes.IRETURN &amp;&amp; opcode &lt;= Opcodes.RETURN)) {
                 extension.classList.each { name -&gt;
                     <span class="hljs-keyword">if</span> (!_static) {
                         <span class="hljs-comment">//加载this</span>
                         mv.visitVarInsn(Opcodes.ALOAD, <span class="hljs-number">0</span>)
                     }
                     <span class="hljs-comment">//用无参构造方法创建一个组件实例</span>
                     mv.visitTypeInsn(Opcodes.NEW, name)
                     mv.visitInsn(Opcodes.DUP)
                     mv.visitMethodInsn(Opcodes.INVOKESPECIAL, name, <span class="hljs-string">"&lt;init&gt;"</span>, <span class="hljs-string">"()V"</span>, <span class="hljs-keyword">false</span>)
                     <span class="hljs-comment">//调用注册方法将组件实例注册到组件库中</span>
                     <span class="hljs-keyword">if</span> (_static) {
                         mv.visitMethodInsn(Opcodes.INVOKESTATIC
                                 , extension.registerClassName
                                 , extension.registerMethodName
                                 , <span class="hljs-string">"(L${extension.interfaceName};)V"</span>
                                 , <span class="hljs-keyword">false</span>)
                     } <span class="hljs-keyword">else</span> {
                         mv.visitMethodInsn(Opcodes.INVOKESPECIAL
                                 , extension.registerClassName
                                 , extension.registerMethodName
                                 , <span class="hljs-string">"(L${extension.interfaceName};)V"</span>
                                 , <span class="hljs-keyword">false</span>)
                     }
                 }
             }
             <span class="hljs-keyword">super</span>.visitInsn(opcode)
         }
         <span class="hljs-annotation">@Override</span>
         void visitMaxs(int maxStack, int maxLocals) {
             <span class="hljs-keyword">super</span>.visitMaxs(maxStack + <span class="hljs-number">4</span>, maxLocals)
         }
        

        }
        }

        5.接收扩展参数,获取需要扫描类的特征及需要插入的代码

        找了很久没找到gradle插件接收自定义对象数组扩展参数的方法,于是退一步改用List<Map>接收后再进行转换的方式来实现,以此来接收多个扫描任务的扩展参数

        import org.gradle.api.Project
        /**
         * aop的配置信息
         * @author billy.qi
         * @since 17/3/28 11:48
         */
        class AutoRegisterConfig {
        
        <span class="hljs-keyword">public</span> ArrayList&lt;Map&lt;String, Object&gt;&gt; registerInfo = []
        
        ArrayList&lt;RegisterInfo&gt; list = <span class="hljs-keyword">new</span> ArrayList&lt;&gt;()
        
        Project project
        
        AutoRegisterConfig(){}
        
        <span class="hljs-keyword">void</span> convertConfig() {
            registerInfo.each { map -&gt;
                RegisterInfo info = <span class="hljs-keyword">new</span> RegisterInfo()
                info.interfaceName = map.get(<span class="hljs-string">'scanInterface'</span>)
                def superClasses = map.get(<span class="hljs-string">'scanSuperClasses'</span>)
                <span class="hljs-keyword">if</span> (!superClasses) {
                    superClasses = <span class="hljs-keyword">new</span> ArrayList&lt;String&gt;()
                } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (superClasses <span class="hljs-keyword">instanceof</span> String) {
                    ArrayList&lt;String&gt; superList = <span class="hljs-keyword">new</span> ArrayList&lt;&gt;()
                    superList.add(superClasses)
                    superClasses = superList
                }
                info.superClassNames = superClasses
                info.initClassName = map.get(<span class="hljs-string">'codeInsertToClassName'</span>) <span class="hljs-comment">//代码注入的类</span>
                info.initMethodName = map.get(<span class="hljs-string">'codeInsertToMethodName'</span>) <span class="hljs-comment">//代码注入的方法(默认为static块)</span>
                info.registerMethodName = map.get(<span class="hljs-string">'registerMethodName'</span>) <span class="hljs-comment">//生成的代码所调用的方法</span>
                info.registerClassName = map.get(<span class="hljs-string">'registerClassName'</span>) <span class="hljs-comment">//注册方法所在的类</span>
                info.include = map.get(<span class="hljs-string">'include'</span>)
                info.exclude = map.get(<span class="hljs-string">'exclude'</span>)
                info.init()
                <span class="hljs-keyword">if</span> (info.validate())
                    list.add(info)
                <span class="hljs-keyword">else</span> {
                    project.logger.error(<span class="hljs-string">'auto register config error: scanInterface, codeInsertToClassName and registerMethodName should not be null\n'</span> + info.toString())
                }
        
            }
        }
        

        }

        import java.util.regex.Pattern
        /**
         * aop的配置信息
         * @author billy.qi
         * @since 17/3/28 11:48
         */
        class RegisterInfo {
            static final DEFAULT_EXCLUDE = [
                    '.*/R(\\$[^/]*)?'
                    , '.*/BuildConfig$'
            ]
            //以下是可配置参数
            String interfaceName = ''
            ArrayList<String> superClassNames = []
            String initClassName = ''
            String initMethodName = ''
            String registerClassName = ''
            String registerMethodName = ''
            ArrayList<String> include = []
            ArrayList<String> exclude = []
        
            //以下不是可配置参数
            ArrayList<Pattern> includePatterns = []
            ArrayList<Pattern> excludePatterns = []
            File fileContainsInitClass //initClassName的class文件或含有initClassName类的jar文件
            ArrayList<String> classList = new ArrayList<>()
        
            RegisterInfo(){}
        
            boolean validate() {
                return interfaceName && registerClassName && registerMethodName
            }
        
            //用于在console中输出日志
            @Override
            String toString() {
                StringBuilder sb = new StringBuilder('{')
                sb.append('\n\t').append('scanInterface').append('\t\t\t=\t').append(interfaceName)
                sb.append('\n\t').append('scanSuperClasses').append('\t\t=\t[')
                for (int i = 0; i < superClassNames.size(); i++) {
                    if (i > 0) sb.append(',')
                    sb.append(' \'').append(superClassNames.get(i)).append('\'')
                }
                sb.append(' ]')
                sb.append('\n\t').append('codeInsertToClassName').append('\t=\t').append(initClassName)
                sb.append('\n\t').append('codeInsertToMethodName').append('\t=\t').append(initMethodName)
                sb.append('\n\t').append('registerMethodName').append('\t\t=\tpublic static void ')
                        .append(registerClassName).append('.').append(registerMethodName)
                sb.append('\n\t').append('include').append(' = [')
                include.each { i ->
                    sb.append('\n\t\t\'').append(i).append('\'')
                }
                sb.append('\n\t]')
                sb.append('\n\t').append('exclude').append(' = [')
                exclude.each { i ->
                    sb.append('\n\t\t\'').append(i).append('\'')
                }
                sb.append('\n\t]\n}')
                return sb.toString()
            }
        
            void init() {
                if (include == null) include = new ArrayList<>()
                if (include.empty) include.add(".*") //如果没有设置则默认为include所有
                if (exclude == null) exclude = new ArrayList<>()
                if (!registerClassName)
                    registerClassName = initClassName
        
                //将interfaceName中的'.'转换为'/'
                if (interfaceName)
                    interfaceName = convertDotToSlash(interfaceName)
                //将superClassName中的'.'转换为'/'
                if (superClassNames == null) superClassNames = new ArrayList<>()
                for (int i = 0; i < superClassNames.size(); i++) {
                    def superClass = convertDotToSlash(superClassNames.get(i))
                    superClassNames.set(i, superClass)
                    if (!exclude.contains(superClass))
                        exclude.add(superClass)
                }
                //interfaceName添加到排除项
                if (!exclude.contains(interfaceName))
                    exclude.add(interfaceName)
                //注册和初始化的方法所在的类默认为同一个类
                initClassName = convertDotToSlash(initClassName)
                //默认插入到static块中
                if (!initMethodName)
                    initMethodName = "<clinit>"
                registerClassName = convertDotToSlash(registerClassName)
                //添加默认的排除项
                DEFAULT_EXCLUDE.each { e ->
                    if (!exclude.contains(e))
                        exclude.add(e)
                }
                initPattern(include, includePatterns)
                initPattern(exclude, excludePatterns)
            }
        
            private static String convertDotToSlash(String str) {
                return str ? str.replaceAll('\\.', '/').intern() : str
            }
        
            private static void initPattern(ArrayList<String> list, ArrayList<Pattern> patterns) {
                list.each { s ->
                    patterns.add(Pattern.compile(s))
                }
            }
        }
         
         

          第三步: 在application中配置自动注册插件所需的相关扩展参数

          在主app module的build.gradle文件中添加扩展参数,示例如下:

          //auto register extension
          // 功能介绍:
          //  在编译期扫描将打到apk包中的所有类
          //  将 scanInterface的实现类 或 scanSuperClasses的子类
          //  并在 codeInsertToClassName 类的 codeInsertToMethodName 方法中生成如下代码:
          //  codeInsertToClassName.registerMethodName(scanInterface)
          // 要点:
          //  1. codeInsertToMethodName 若未指定,则默认为static块
          //  2. codeInsertToMethodName 与 registerMethodName 需要同为static或非static
          // 自动生成的代码示例:
          /*
            在com.billy.app_lib_interface.CategoryManager.class文件中
            static
            {
              register(new CategoryA()); //scanInterface的实现类
              register(new CategoryB()); //scanSuperClass的子类
            }
           */
          apply plugin: 'auto-register'
          autoregister {
              registerInfo = [
                  [
                      'scanInterface'             : 'com.billy.app_lib_interface.ICategory'
                      // scanSuperClasses 会自动被加入到exclude中,下面的exclude只作为演示,其实可以不用手动添加
                      , 'scanSuperClasses'        : ['com.billy.android.autoregister.demo.BaseCategory']
                      , 'codeInsertToClassName'   : 'com.billy.app_lib_interface.CategoryManager'
                      //未指定codeInsertToMethodName,默认插入到static块中,故此处register必须为static方法
                      , 'registerMethodName'      : 'register' //
                      , 'exclude'                 : [
                          //排除的类,支持正则表达式(包分隔符需要用/表示,不能用.)
                          'com.billy.android.autoregister.demo.BaseCategory'.replaceAll('\\.', '/') //排除这个基类
                      ]
                  ],
                  [
                      'scanInterface'             : 'com.billy.app_lib.IOther'
                      , 'codeInsertToClassName'   : 'com.billy.app_lib.OtherManager'
                      , 'codeInsertToMethodName'  : 'init' //非static方法
                      , 'registerMethodName'      : 'registerOther' //非static方法
                  ]
              ]
          }
           
           

          总结


          本文介绍了AutoRegister插件的功能及其在组件化开发框架中的应用。重点对其原理做了说明,主要介绍了此插件的实现过程,其中涉及到的技术点有TransformAPI、ASM、groovy相关语法、gradle机制。

          本插件的所有代码及其用法demo已开源到github上,欢迎fork、start

          接下来就用这个插件来为我们自动管理注册表吧!

          转载请注明出处: http://blog.csdn.net/cdecde111/article/details/78074692

          评论
          添加红包

          请填写红包祝福语或标题

          红包个数最小为10个

          红包金额最低5元

          当前余额3.43前往充值 >
          需支付:10.00
          成就一亿技术人!
          领取后你会自动成为博主和红包主的粉丝 规则
          hope_wisdom
          发出的红包
          实付
          使用余额支付
          点击重新获取
          扫码支付
          钱包余额 0

          抵扣说明:

          1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
          2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

          余额充值