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

DDComponentForAndroid:探索Android组件化方案 在现代Android应用开发中,随着应用规模的不断扩大,传统的单体应用架构已经无法满足快速迭代和维护的需求。组件化架构作为一种解决方案,可以将应用拆分成多个独立的模块,每个模块负责特定的功能,从而提高代码的可维护性和可扩展性。`DDComponentForAndroid`是一个实现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: "${artifactory_contextUrl}/${repoKey}") {
                    authentication(userName: artifactory_user, password: artifactory_password)
                }
            }

            pom.groupId = artifact_group
            pom.artifactId = artifact_id
            pom.version = artifact_version + (maven_type_snapshot ? '-SNAPSHOT' : '')

            pom.project {
                licenses {
                    license {
                        name 'The Apache Software License, Version 2.0'
                        url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    }
                }
            }
        }
    }
}

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

buildscript {

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

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

// 遍历输入文件
inputs.each { TransformInput input ->

    // 遍历jar
    input.jarInputs.each { JarInput jarInput ->
        String destName = jarInput.name
        // 重名名输出文件,因为可能同名,会覆盖
        def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath)
        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.JAR)

        //遍历jar的字节码类文件,找到需要自动注册的component
        if (CodeScanProcessor.shouldProcessPreDexJar(src.absolutePath)) {
            CodeScanProcessor.scanJar(src, dest)
        }
        FileUtils.copyFile(src, dest)

        project.logger.info "Copying\t${src.absolutePath} \nto\t\t${dest.absolutePath}"
    }
    // 遍历目录
    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(file.isFile()){
                CodeScanProcessor.checkInitClass(path, new File(dest.absolutePath + File.separator + path))
                if (CodeScanProcessor.shouldProcessClass(path)) {
                    CodeScanProcessor.scanClass(file)
                }
            }
        }
        project.logger.info "Copying\t${directoryInput.file.absolutePath} \nto\t\t${dest.absolutePath}"
        // 处理完后拷到目标文件
        FileUtils.copyDirectory(directoryInput.file, 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))

            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 (isInitClass(entryName)) {
                    println('codeInsertToClassName:' + 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
    }

    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) {
            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)
            if (name == extension.initMethodName) { //注入代码到指定的方法之中
                boolean _static = (access & Opcodes.ACC_STATIC) > 0
                mv = new MyMethodVisitor(Opcodes.ASM5, mv, _static)
            }
            return mv
        }
    }

    class MyMethodVisitor extends MethodVisitor {
        boolean _static;

        MyMethodVisitor(int api, MethodVisitor mv, boolean _static) {
            super(api, mv)
            this._static = _static;
        }

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

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

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

import org.gradle.api.Project
/**
 * aop的配置信息
 * @author billy.qi
 * @since 17/3/28 11:48
 */
class AutoRegisterConfig {

    public ArrayList<Map<String, Object>> registerInfo = []

    ArrayList<RegisterInfo> list = new ArrayList<>()

    Project project

    AutoRegisterConfig(){}

    void convertConfig() {
        registerInfo.each { map ->
            RegisterInfo info = new RegisterInfo()
            info.interfaceName = map.get('scanInterface')
            def superClasses = map.get('scanSuperClasses')
            if (!superClasses) {
                superClasses = new ArrayList<String>()
            } else if (superClasses instanceof String) {
                ArrayList<String> superList = new ArrayList<>()
                superList.add(superClasses)
                superClasses = superList
            }
            info.superClassNames = superClasses
            info.initClassName = map.get('codeInsertToClassName') //代码注入的类
            info.initMethodName = map.get('codeInsertToMethodName') //代码注入的方法(默认为static块)
            info.registerMethodName = map.get('registerMethodName') //生成的代码所调用的方法
            info.registerClassName = map.get('registerClassName') //注册方法所在的类
            info.include = map.get('include')
            info.exclude = map.get('exclude')
            info.init()
            if (info.validate())
                list.add(info)
            else {
                project.logger.error('auto register config error: scanInterface, codeInsertToClassName and registerMethodName should not be null\n' + 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.youkuaiyun.com/cdecde111/article/details/78074692

Android组件化开发模块间通信实战 组件化开发是当前Android大型项目中广泛应用的架构模式,旨在通过模块拆分实现高内聚、低耦合的系统设计。随着App功能日益复杂,传统单体架构在代码维护、功能扩展及团队协作方面逐渐暴露出诸多问题。组件化通过将业务模块拆分为独立组件,支持独立编译、运行与调试,极大提升了开发效率与系统可维护性。本章将从组件化的基本概念入手,分析其相较于模块化的差异,并阐述其在现代Android架构中的核心价值。 阅读详情

相关推荐

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

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

2024年安卓最新AutoRegister框架分析,美团三面面试什么

各行各样都会淘汰一些能力差的,不仅仅是IT这个行业,所以,不要被程序猿是吃青春饭等等这类话题所吓倒,也不要觉得,找到一份工作,就享受安逸的生活,你在安逸的同时,别人正在奋力的向前跑,这样与别人的差距也就会越来越遥远,加油,希望,我们每一个人,成为更好的自己。BAT大厂面试题、独家面试工具包,资料包括 数据结构、Kotlin、计算机网络、Framework源码、数据结构与算法、小程序、NDK、Flutter,

2401_84544434的博客 1013

基因家族的鉴定-基于Windows系统上的HMMER

基因家族的鉴定-基于Windows系统上的HMMER。

liangjinghui123的博客 3246

探索 AutoRegister:自动化注册神器的技术解析与应用

探索 AutoRegister:自动化注册神器的技术解析与应用 项目地址:https://gitcode.com/gh_mirrors/au/AutoRegister 在这个数字化的时代,我们常常需要注册各种在线服务,而手动填写信息的过程既耗时又容易出错。这就是AutoRegister项目的诞生背景,它是一个开源的自动化注册工具,借助于Python和Selenium库,能够帮助用户快速、准确地完成...

gitblog_00089的博客 926

手把手教你用RTL8306MB搭建小型网络交换机:从原理到实战

本文详细介绍了如何使用RTL8306MB交换芯片从零搭建小型网络交换机的全过程。内容涵盖芯片原理分析、核心硬件电路设计(包括电源、时钟、RMII接口连接要点)、驱动开发与SMI通信实现,以及从物理链路到交换功能的系统调试实战。重点解析了将MAC5配置为RMII PHY模式与CPU对接的关键步骤和常见错误,帮助嵌入式开发者深入理解网络交换原理并完成实践。

weixin_29274969的博客 192

为什么需要知识图谱?什么是知识图谱?——KG的前世今生

我的知乎专栏,对知识图谱感兴趣的朋友可以关注。标题的命名顺序可能让有的读者不太习惯。通常在介绍一个陌生事物的应用前,我们先给出其定义。之所以换个顺序,是为了不让读者一开始就接触比较冰冷生硬的概念刻板描述(后面我尽量用更具体、准确的例子来表达),另一方面也是为了通过现实生活中的例子自然的引入知识图谱的概念。希望通过这种方式加深读者的印象和理解。为了减轻读者理解的负担,我尽可能地避免引入过多的概念和技术

SimmerChan的专栏 8万+

AutoRegister 项目常见问题解决方案

AutoRegister 是一个基于字节码插桩的 Gradle 插件,主要用于在 Android 项目中实现跨模块的自动注册功能。该项目的主要编程语言是 Java 和 Groovy。通过字节码插桩技术,AutoRegister 能够在编译期间扫描所有类,并将符合条件的类自动注册到指定的管理类中,从而实现模块解耦和自动化注册。 ## 新手使用注意事项及解决方案 ### 1. 配置文件路径错误 ...

gitblog_00998的博客 1119

Nvidia Isaac Sim图形界面 入门教程 2024(3)

在使用Isaac Sim时,最先接触到的应该是它的图形界面,此处将简单介绍图形界面中常用的一些组件:Isaac Sim的界面的常用内容可以分为几个板块。

m0_56661101的博客 4547

Android:自动注册组件

参考文章:https://github.com/luckybilly/AutoRegister/ 什么叫自动注册组件?一开始我也没有明白,现在我来解释下。 组件化完成后,为了减少代码耦合,我们可以使用动态生成代码的方式写一些重复代码。 什么是自动注册: 当我们项目有多个moudle的时候,app作为主工程,会有多个业务moudle。我们想要将每一个业务moudle下的 XXInterface 的实现类放到一个全局的数组之中,我们应该怎么处理呢?第一个办法就是在app module下创建全局数组,.

BunnyCoffer的博客 836

从仿真到真机:如何将XTDrone的VINS-Fusion+RTABMap建图方案部署到真实无人机上?

本文详细介绍了如何将XTDrone的VINS-Fusion+RTABMap建图方案从仿真环境迁移到真实无人机上。内容涵盖硬件选型、传感器配置、VINS-Fusion调优、RTABMap性能优化以及系统集成等关键步骤,特别针对三维稠密重建在真实环境中的挑战提供了实用解决方案。

weixin_29056781的博客 242

4、机器人建模、规划与控制及运动学知识解析

本博客深入解析了机器人建模、规划与控制的核心内容,涵盖运动学与动力学的基本原理,并结合典型应用场景探讨了移动机器人与操作器的控制策略。同时,博客还介绍了机器人技术的发展趋势及其在工业和服务领域的广泛应用。

desk3的博客 123

终极指南:如何用AutoRegister彻底解决Android模块自动注册难题

AutoRegister是一款基于字节码插桩技术的Gradle插件,专为Android开发者设计,可实现跨模块自动注册功能,有效解决组件化开发中的模块解耦问题。该插件已成功应用于ARouter和CC等知名框架,是Android组件化开发的必备工具。 ## 🤔 为什么需要AutoRegister? 在Android开发中,我们经常面临以下痛点: - 工厂模式中,新增产品需手动修改工厂类代码 -

gitblog_00106的博客 901

电脑硬件:电脑主板上的接口介绍,看完你就懂了

它们主要用于连接老式的键盘和鼠标,虽然速度和兼容性不如USB接口,但在某些特定场合依然发挥着重要作用。PCIe接口是主板上的扩展能力象征,可以连接各种扩展卡,如显卡、声卡、网卡等。电脑主板作为电脑硬件组成关键的部分,今天给大家分享一下电脑主板上有哪些接口,具体每一个接口的特点、使用场景,有需要的朋友可以来了解一下!它们不仅能连接鼠标、键盘、U盘、打印机,还能连接各种外设,如移动硬盘、游戏手柄等。:虽然现在用得越来越少,但在一些特定场合,如工业控制、老旧设备上,仍然需要使用PS/2接口。

IT技术分享社区 2787

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

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

weixin_33738555的博客 191

详解AUTOSAR:AUTOSAR架构中的配置文件(拓展篇—7)

查看AUTOSAR代码包时会发现一些这样格式的文件:xxx_PBcfg.c、xxx_Lcfg.c和xxx_Cfg.h,本篇博文讲一讲这几个配置文件的作用和特点。

不脱发的程序猿 2146

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

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

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 摘要: 在编译时,扫描即将打包到apk中的所有类,将所有组件类收集起来,通过修改字节码的方式生成注册代码到组件管理类中,从而实现编译时自动注册的功能,不用再关心项目中有哪些组件类了。 特点:不需要注解,不会增加新的类;性能高,不需要反射,运行时直接调用组件的构造方法;能扫描到所有类,不会出现遗漏;支持分级按需加载功能的实现。 前...

weixin_34202952的博客 2092

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

Android组件化框架CC:高效解耦开发利器

(ComponentCaller)是一个专为 Android 设计的组件化开发框架,旨在解决大型应用开发中的模块解耦、独立调试和协作效率问题。通过将应用拆分为多个独立模块(组件),开发者可以单独编译、测试和调试特定功能,同时保持模块间的灵活通信能力。

Linux技术宅 1131

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

Android组件化

Android组件化本文由5个模块组成: 1.组件化与模块化区别 2.组件化家构图 3.各组件之间的跳转 4.各组件之间的通讯 5.组件化优点 6.Android Studio上面实现组件化1.组件化与模块化区别组件化就是基于可重用的目的,将一个大的软件系统按照分离关注点的形式,拆分成多个独立的组件,已较少耦合。2.组件化家构图 说明:Library是各组件需要依赖的一些公用库,Router(负责

黑白键的奔腾 1280

Android彻底组件化—UI跳转升级改造

一线互联网Android面试题含详解(初级到高级专题)这些题目是今年群友去腾讯、百度、小米、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。并且大多数都整理了答案,熟悉这些知识点会大大增加通过前两轮技术面试的几率如果设置门槛,很多开发者朋友会因此错过这套高级架构资料,错过提升成为架构师的可能。这就失去了我们的初衷;让更多人都能通过高效高质量的学习,提升自己的技术和格局,升职加薪。最后送给大家一句话,望共勉,永远不要放弃自己的梦想和追求;

2401_84010784的博客 1052

安卓APT技术讲解(下)-实现安卓组件化的路由功能

安卓APT技术讲解(下)-实现安卓组件化的路由功能

分享+记录 1039

为什么选择AndroidComponentizeLibs?组件化开发效率提升指南

AndroidComponentizeLibs是一个专注于Android组件化开发的开源项目,旨在帮助开发者快速了解和选择适合的组件化方案,提升开发效率和项目架构质量。对于Android开发新手和普通开发者来说,面对众多的组件化框架和方案往往感到困惑,而AndroidComponentizeLibs通过全面的对比分析,为你提供清晰的选择指引。 ## 📊 组件化方案对比:为什么AndroidCo

gitblog_00304的博客 766

Android Jetpack组件化开发实践

Android Jetpack中的Lifecycle组件可以帮助开发者更好地管理应用的生命周期。通过观察者模式,Lifecycle组件可以在Activity或Fragment的生命周期发生变化时通知相关的Observer,从而实现更好的资源管理和错误处理。Android Jetpack组件化开发实践为开发者提供了一种高效、稳定的开发模式。通过组件化开发,开发者可以提高开发效率、降低维护成本,同时应对项目发展的挑战。

2501_92487643的博客 799

Android彻底组件化方案实践

转载自: https://www.jianshu.com/p/1b1d77f58e84 得到Android组件化方案已经开源,参见Android组件化方案开源。方案的解读文章是一个小的系列,这是系列的第一篇文章: 1、Android彻底组件化方案实践 2、Android彻底组件化demo发布 3、Android彻底组件化-代码和资源隔离 4、Android彻底组件化—UI...

俊墨客 371

快速上手Android组件化:AndroidComponentizeLibs框架选型与实战指南

Android组件化开发是当前移动端架构演进的重要方向,它能显著提升大型应用的开发效率和维护性。然而面对众多的组件化框架,开发者往往感到迷茫:哪个框架最适合我的项目?各方案有何差异?如何快速上手?本文将基于AndroidComponentizeLibs项目,为你提供一份完整的组件化框架选型与实战指南。 ## 📊 Android组件化框架全景对比 AndroidComponentizeLibs

gitblog_00779的博客 873
上一篇: 零代码侵入给app添加应用外debug工具
下一篇: android发布maven报错:Could not write to file '*****/build/poms/pom-default.xml'
齐翊
博客等级 码龄17年 85粉丝 24原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值