一种更高效的组件自动注册方案(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

SigFit实战:从FEA到光学仿真的热力耦合分析全流程解析

在光机系统与芯片封装等精密工程领域,热与力的耦合效应是影响产品性能与可靠性的核心物理问题。有限元分析(FEA)是预测结构在热、力载荷下变形与应力的基础工具,但其结果通常无法直接用于评估光学性能。这需要一座桥梁,将物理场数据转化为光学设计语言。其核心原理在于数据映射与面形表征技术,例如通过泽尼克多项式拟合,将离散的节点位移分解为离焦、像散等标准光学像差。这项技术的工程价值在于,它实现了多物理场仿真的闭环,使工程师能在设计阶段精准预测热变形或应力对波前误差、调制传递函数等关键光学指标的影响。其典型应用场景包括高

weixin_28630185的博客 338

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

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

gitblog_00089的博客 926

基于任务的后勤智能体:三段构建与演进设计探究

这是系统智能化的核心难点。例如,对于一个“加强连级滩头阵地防御”子任务,系统会根据防御时长、预期敌军攻击强度、地形等因素,计算出所需的弹药(不同弹种的具体数量)、构筑工事的器材、人员的给养和饮水量、以及医疗救护的药品和血包数量。首先进行分词、词性标注等基础处理,然后利用在军事语料库上训练的命名实体识别(NER)模型,提取出关键实体,如任务类型(“夺控作战”)、作战对象(“Y岛”)、时间约束(“D日H时”、“24小时内”)、成功标准(“建立稳固滩头阵地”)、保障重点(“弹药充足”、“及时救治”)等。

weixin_49042442的博客 1088

完整的申请邓白氏编码的流程(手把手教你申请邓白氏编码)

申请公司的苹果开发者账号和企业级的苹果开发者账号时,都会用到邓白氏编码,也就是我们申请苹果开发者账号时需要填写的D-U-N-S Number。     申请这个邓白氏编码是免费的,但是从像华夏邓白氏公司提交申请到可以申请苹果开发者账号来用这个9位的D-U-N-S Number,需要一段时间,我申请得到D-U-N-S Number,用了8个工作日(不算周末哦)

wxw2011014003的博客 9万+

AutoRegister 项目常见问题解决方案

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

gitblog_00998的博客 1119

Orange Pi 3B+刷入EmuELEC全过程:手把手教程

手把手教你如何在Orange Pi 3B+上成功刷入emuelec系统,涵盖烧录工具、镜像下载到启动设置的完整流程,让复古游戏体验轻松实现。

weixin_35732273的博客 1091

Android:自动注册组件

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

BunnyCoffer的博客 836

uniapp 基于单选框多选框实现上拉触底多选/单选、回显

showPickerCheckbox弹层显示 label和value 类似下拉框的label和value一个意思 键和值。结果:可以传接口(接口只能post请求) 可以搜索 下面是结果图片展示 单选多选都可以。fieldValue回显传的值 传的是字符串 数组的话[1,2]转为1,2格式传入。mode是控制的单选还是多选 true为多选默认值 其余参数 单选多选一样。fieldName字段名 确定后会返回该字段名 根据字段名找对应的传入值。问题:在uniapp上 实现分页多选的操作。下面是组件的完整代码。

2301_76607997的博客 2936

[系统安全] 三十二.恶意代码检测(2)常用技术详解及总结

前文从产业界和学术界分别详细讲解恶意代码攻击溯源的相关知识,在学术界方面,用类似于综述来介绍攻击追踪溯源的不同方法;在产业界方面,主要参考李东宏老师从企业恶意样本分析的角度介绍溯源工作。这篇文章将详细总结恶意代码检测技术,包括恶意代码检测的对象和策略、特征值检测技术、校验和检测技术、启发式扫描技术、虚拟机检测技术和主动防御技术。希望这篇文章对您有所帮助,如果文章中存在错误、理解不到位或侵权的地方,还请告知作者与海涵。且看且珍惜,加油~

杨秀璋的专栏 1万+

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

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

gitblog_00106的博客 901

初识 QML

QML 是一种声明性语言,允许根据用户界面的可视组件以及它们如何相互交互和相互关联来描述用户界面。QML 是一种高度可读的语言,旨在使组件能够以动态方式互连,并且允许组件在用户界面中轻松重复使用和自定义。 使用 Qt Quick,开发人员可以轻松地在 QML 中构建流体动画用户界面,并且可以选择将这些用户界面连接到任何后端 C++ 库。

青春不老,奋斗不止! 1万+

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

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

weixin_33738555的博客 191

嵌入式软考必备知识点:嵌入式系统开发中的重要概念和示例代码

在嵌入式软考中,有一些重要的知识点需要掌握,包括嵌入式系统的基本概念、编程语言、实时操作系统和硬件接口等。实时操作系统是一种专门设计用于嵌入式系统的操作系统,可以提供对任务调度、中断处理和资源管理等方面的支持。GPIO(通用输入输出):GPIO是嵌入式系统中常用的硬件接口,用于连接外部设备和嵌入式系统。以上是嵌入式软考中的一些必备知识点,包括嵌入式系统的基本概念、编程语言、实时操作系统和硬件接口。通过配置串口的波特率、数据位数和校验位等参数,可以实现与其他设备的可靠通信。一、嵌入式系统的基本概念。

PixeGO的博客 313

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

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

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

weixin_34202952的博客 2092

Statistics and Machine Learning Toolbox

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

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

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

Linux技术宅 1131

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

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、付费专栏及课程。

余额充值