Android Studio gradle插件开发----组件注

2021-03-16  本文已影响0人  今日Android

组件注册插件是解决在模块化开发中无反射、无新增第三方框架、可混淆的需求。在Android Studio编译阶段根据宿主Module的build.gradle中的配置信息注入组件注册代码。

详细例子和插件源码,欢迎star:github.com/trrying/Plu…

效果:

使用插件前App源码:

[图片上传中...(image-6d9f08-1615864625688-7)]

使用插件后反编译App:

[图片上传中...(image-81caff-1615864625688-6)]

使用:

  1. 在项目根目录 build.gradle添加classpath
buildscript {
    dependencies {
        // 组件注册插件
        classpath 'com.owm.component:register:1.1.2'
    }
}
复制代码
  1. 在宿主模块build.gradle添加配置参数
apply plugin: 'com.android.application'
android {
    ...
}
dependencies {
    ...
}

apply plugin: 'com.owm.component.register'
componentRegister {
    // 是否开启debug模式,输出详细日志
    isDebug = false
    // 是否启动组件注册
    componentRegisterEnable = true
    // 组件注册代码注入类
    componentMain = "com.owm.pluginset.application.App"
    // 注册代码注入类的方法
    componentMethod = "instanceModule"
    // 注册组件容器 HashMap,如果没有该字段则创建一个 public static final HashMap<String, Object> componentMap = new HashMap<>();
    componentContainer = "componentMap"
    // 注册组件配置
    componentRegisterList = [
        [
            "componentName": "LoginInterface",
            "instanceClass": "com.owm.module.login.LoginManager",
            "enable"       : true, // 默认为:true
            "singleton"    : false, // 默认为:false,是否单例实现,为true调用Xxx.getInstance(),否则调用new Xxx();
        ],
    ]
}

复制代码

上述配置表示在com.owm.pluginset.application.App类中instanceModule方法内首部添加 componentMap.put("LoginInterface", new com.owm.module.login.LoginManager());代码。

  1. componentMain配置的类中创建instanceModule()方法和componentMap容器。
class App {
    public static final HashMap<String, Object> componentMap = new HashMap<>();
    public void instanceModule() {
    }
}
复制代码

Gradle同步重新构建项目后出现下列输入表示注入成功。

[图片上传中...(image-b07825-1615864625687-5)]

1. 背景

组件化开发需要动态注册各个组件服务,解决在模块化化开发中无需反射、无需第三方框架、可混淆的需求。

2. 知识点

使用Android Studio编译流程如下图:

[图片上传中...(image-1305c5-1615864625687-4)]

如果把整个构建编译流程看成是河流的话,在java 编译阶段有3条河流入,分别是:

  1. aapt(Android Asset Package Tool)根据资源文件生成的R文件;
  2. app 源码;
  3. aidl文件生成接口;

上面3条河流汇集后源码将被编译成class文件。 现在要做的就是使用 Gralde Plugin 注册一个Transform,在Java Compileer 之后插入,处理class文件。处理完成后交给下一步流程去继续构建。

3. 构建插件模块

3.1 创建插件模块

在项目中创建Android Library Module(其他模块也行,只要目录结构对应下面),创建完成后删除多余目录和文件,只保留src目录和build.gradle文件。 目录结构如下:

PluginSet
│
├─ComponentRegister
│  │  .gitignore
│  │  build.gradle
│  │  ComponentRegister.iml
│  │
│  └─src
│      └─main
│          ├─groovy //
│          │  └─com
│          │      └─owm
│          │          └─component
│          │              └─register
│          │                  ├─plugin
│          │                  │      RegisterPlugin.groovy
│          │
│          └─resources
│              └─META-INF
│                  └─gradle-plugins
│                          com.owm.component.register.properties

复制代码

主要关注有两个点

  1. src/main/groovy 放置插件代码

  2. src/main/resources 放置插件配置信息

    src/main/resources下面的 resources/META-INF/gradle-plugins 存放配置信息。这里可以放置多个配置信息,每个配置信息是一个插件。 配置文件名就是插件名,例如我这里是com.owm.component.register.properties,应用时:apply plugin: 'com.owm.component.register'

3.2 创建插件代码目录

创建src/main/groovy 目录,再在该目录下创建包名路径和对应groovy类文件。

3.3 创建插件配置文件

src/main/resources目录下创建 resources/META-INF/gradle-plugins 目录,再创建com.owm.component.register.properties配置文件。 配置文件内容如下:

implementation-class=com.owm.component.register.plugin.RegisterPlugin
复制代码

这里是配置org.gradle.api.Plugin 接口的实现类,也就是配置插件的核心入口。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.gradle.api;

public interface Plugin<T> {
    void apply(T t);
}
复制代码

3.4 配置gradle

ComponentRegister插件模块build.gradle配置如下:

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

dependencies {
    //gradle sdk
    implementation gradleApi()
    //groovy sdk
    implementation localGroovy()

    //noinspection GradleDependency
    implementation "com.android.tools.build:gradle:3.2.0"
    implementation "javassist:javassist:3.12.1.GA"
    implementation "commons-io:commons-io:2.6"
}

// 发布到 plugins.gradle.org 双击Gradle面板 PluginSet:ComponentRegister -> Tasks -> plugin portal -> publishPlugins
apply from: "../script/gradlePlugins.gradle"

// 发布到本地maven仓库 双击Gradle面板 PluginSet:ComponentRegister -> Tasks -> upload -> uploadArchives
apply from: "../script/localeMaven.gradle"

//发布 Jcenter 双击Gradle面板 PluginSet:ComponentRegister -> Tasks -> publishing -> bintrayUpload
apply from: '../script/bintray.gradle'

复制代码

gralde sync 后可以在Android Studio Gradle面板找到uploadArchives Task

[图片上传中...(image-73141d-1615864625686-3)]

当插件编写完成,双击运行uploadArchivesTask会在配置的本地Maven仓库中生成插件。

[图片上传中...(image-3ad5c-1615864625686-2)]

4. 组件注册插件功能实现

4.1 实现Plugin接口

按照src/main/resources/resources/META-INF/gradle-plugins/com.owm.component.register.properties配置文件中implementation-class的值来创建创建Plugin接口实现类。

在Plugin实现类中完成配置参数获取和注册Transform。

注意加载配置项需要延迟一点,例如project.afterEvaluate{}里面获取。

class RegisterPlugin implements Plugin<Project> {

    // 定义gradle配置名称
    static final String EXT_CONFIG_NAME = 'componentRegister'

    @Override
    void apply(Project project) {
        LogUtils.i("RegisterPlugin 1.1.0 $project.name")

        // 注册Transform
        def transform = registerTransform(project)

        // 创建配置项
        project.extensions.create(EXT_CONFIG_NAME, ComponentRegisterConfig)

        project.afterEvaluate {
            // 获取配置项
            ComponentRegisterConfig config = project.extensions.findByName(EXT_CONFIG_NAME)
            // 配置项设置设置默认值
            config.setDefaultValue()

            LogUtils.logEnable = config.isDebug
            LogUtils.i("RegisterPlugin apply config = ${config}")

            transform.setConfig(config)

            // 保存配置缓存,判断改动设置UpToDate状态
            CacheUtils.handleUpToDate(project, config)
        }
    }

    // 注册Transform
    static registerTransform(Project project) {
        LogUtils.i("RegisterPlugin-registerTransform :" + " project = " + project)

        // 初始化Transform
        def extension = null, transform = null
        if (project.plugins.hasPlugin(AppPlugin)) {
            extension = project.extensions.getByType(AppExtension)
            transform = new ComponentRegisterAppTransform(project)
        } else if (project.plugins.hasPlugin(LibraryPlugin)) {
            extension = project.extensions.getByType(LibraryExtension)
            transform = new ComponentRegisterLibTransform(project)
        }

        LogUtils.i("extension = ${extension} \ntransform = $transform")

        if (extension != null && transform != null) {
            // 注册Transform
            extension.registerTransform(transform)
            LogUtils.i("register transform")
        } else {
            throw new RuntimeException("can not register transform")
        }
        return transform
    }

}

复制代码

4.2 继承Transform

集成Transform实现抽象方法;

getName():配置名字;

getInputTypes():配置处理内容,例如class内容,jar内容,资源内容等,可多选

getScopes():配置处理范围,例如当前模块,子模块等,可多选;

isIncremental():是否支持增量;

transform(transformInvocation):转换逻辑处理;

注意:library模块范围只能配置为当前模块;

class BaseComponentRegisterTransform extends Transform {

    // 组件注册配置
    protected ComponentRegisterConfig config

    // Project
    protected Project project

    // Transform 显示名字,只是部分,真实显示还有前缀和后缀
    protected String name = this.class.simpleName

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

    @Override
    String getName() {
        return name
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        // 处理的类型,这里是要处理class文件
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<QualifiedContent.Scope> getScopes() {
        // 处理范围,这里是整个项目所有资源,library只能处理本模块
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        // 是否支持增量
        return true
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        LogUtils.i("${this.class.name} start ")

        if (!config.componentRegisterEnable) {
            LogUtils.r("componentRegisterEnable = false")
            return
        }

        // 缓存信息,决解UpTpDate缓存无法控制问题
        ConfigCache configInfo = new ConfigCache()
        configInfo.configString = config.configString()

        // 遍历输入文件
        transformInvocation.getInputs().each { TransformInput input ->
            // 遍历jar
            input.jarInputs.each { JarInput jarInput ->
                File dest = transformInvocation.outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                // 复制jar到目标目录
                FileUtils.copyFile(jarInput.file, dest)
                // 查看是否需要导包,是则加入导包列表
                InsertCodeUtils.scanImportClass(dest.toString(), config)
            }

            // 遍历源码目录文件
            input.directoryInputs.each { DirectoryInput directoryInput ->
                // 获得输出的目录
                File dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                // 复制文件夹到目标目录
                FileUtils.copyDirectory(directoryInput.file, dest)
                // 查看是否需要导包,是则加入导包列表
                InsertCodeUtils.scanImportClass(dest.toString(), config)
            }
        }

        // 代码注入
        def result = InsertCodeUtils.insertCode(config)
        LogUtils.i("insertCode result = ${result}")
        LogUtils.r("${result.message}")
        if (!result.state) {
            // 插入代码异常,终止编译打包
            throw new Exception(result.message)
        }
        // 缓存-记录路径
        configInfo.destList.add(config.mainClassPath)
        // 保存缓存文件
        CacheUtils.saveConfigInfo(project, configInfo)
    }

    ComponentRegisterConfig getConfig() {
        return config
    }

    void setConfig(ComponentRegisterConfig config) {
        this.config = config
    }
}
复制代码

transform(TransformInvocation transformInvocation)方法中的参数包含需要操作的数据。使用getInputs()获取输入的class或者jar内容,遍历扫描到匹配的类,将的组件实例代码注入到类中,再将文件复制到getOutputProvider()获取class或者jar对应的输出路径里面。

package com.android.build.api.transform;

import java.util.Collection;

public interface TransformInvocation {
    Context getContext();

    // 输入内容
    Collection<TransformInput> getInputs();

    Collection<TransformInput> getReferencedInputs();

    Collection<SecondaryInput> getSecondaryInputs();

    // 输出内容提供者
    TransformOutputProvider getOutputProvider();

    boolean isIncremental();
}
复制代码

4.3 使用javassist注入组件注册代码

使用javassist来作为代码插桩工具,后续熟练字节码编辑再桩位asm实现;

class InsertCodeUtils {

    /**
     * 注入组件实例代码
     * @param config 组件注入配置
     * @return 注入状态["state":true/false]
     */
    static insertCode(ComponentRegisterConfig config) {
        def result = ["state": false, "message":"component insert cant insert"]
        def classPathCache = []
        LogUtils.i("InsertCodeUtils config = ${config}")

        // 实例类池
        ClassPool classPool = new ClassPool()
        classPool.appendSystemPath()

        // 添加类路径
        config.classPathList.each { jarPath ->
            appendClassPath(classPool, classPathCache, jarPath)
        }

        CtClass ctClass = null
        try {
            // 获取注入注册代码的类
            ctClass = classPool.getCtClass(config.componentMain)
            LogUtils.i("ctClass ${ctClass}")

            if (ctClass.isFrozen()) {
                // 如果冻结就解冻
                ctClass.deFrost()
            }

            // 获取注入方法
            CtMethod ctMethod = ctClass.getDeclaredMethod(config.componentMethod)
            LogUtils.i("ctMethod = $ctMethod")

            // 判断是否有组件容器
            boolean hasComponentContainer = false
            ctClass.fields.each { field ->
                if (field.name == config.componentContainer) {
                    hasComponentContainer = true
                }
            }
            if (!hasComponentContainer) {
                CtField componentContainerField = new CtField(classPool.get("java.util.HashMap"), config.componentContainer, ctClass)
                componentContainerField.setModifiers(Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL)
                ctClass.addField(componentContainerField, "new java.util.HashMap();")
            }

            // 注入组件实例代码
            String insertCode = ""
            // 记录组件注入情况,用于日志输出
            def componentInsertSuccessList = []
            def errorComponent = config.componentRegisterList.find { component ->
                LogUtils.i("component = ${component}")
                if (component.enable) {
                    String instanceCode = component.singleton ? "${component.instanceClass}.getInstance()" : "new ${component.instanceClass}()"
                    insertCode = """${config.componentContainer}.put("${component.componentName}", ${instanceCode});"""
                    LogUtils.i("insertCode = ${insertCode}")
                    try {
                        ctMethod.insertBefore(insertCode)
                        componentInsertSuccessList.add(component.componentName)
                        return false
                    } catch (Exception e) {
                        if (LogUtils.logEnable) { e.printStackTrace() }
                        result = ["state": false, "message":"""insert "${insertCode}" error : ${e.getMessage()}"""]
                        return true
                    }
                }
            }
            LogUtils.i("errorComponent = ${errorComponent}")
            if (errorComponent == null) {
                File mainClassPathFile = new File(config.mainClassPath)
                if (mainClassPathFile.name.endsWith('.jar')) {
                    // 将修改的类保存到jar中
                    saveToJar(config, mainClassPathFile, ctClass.toBytecode())
                } else {
                    ctClass.writeFile(config.mainClassPath)
                }
                result = ["state": true, "message": "component register ${componentInsertSuccessList}"]
            }
        } catch (Exception e) {
            LogUtils.r("""error : ${e.getMessage()}""")
            if (LogUtils.logEnable) { e.printStackTrace() }
        } finally {
            // 需要释放资源,否则会io占用
            if (ctClass != null) {
                ctClass.detach()
            }
            if (classPool != null) {
                classPathCache.each { classPool.removeClassPath(it) }
                classPool = null
            }
        }
        return result
    }

    static saveToJar(ComponentRegisterConfig config, File jarFile, byte[] codeBytes) {
        if (!jarFile) {
            return
        }
        def mainJarFile = null
        JarOutputStream jarOutputStream = null
        InputStream inputStream = null

        try {
            String mainClass = "${config.componentMain.replace(".", "/")}.class"

            def tempJarFile = new File(config.mainJarFilePath)
            if (tempJarFile.exists()) {
                tempJarFile.delete()
            }

            mainJarFile = new JarFile(jarFile)
            jarOutputStream = new JarOutputStream(new FileOutputStream(tempJarFile))
            Enumeration enumeration = mainJarFile.entries()

            while (enumeration.hasMoreElements()) {
                try {
                    JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                    String entryName = jarEntry.getName()
                    ZipEntry zipEntry = new ZipEntry(entryName)
                    inputStream = mainJarFile.getInputStream(jarEntry)
                    jarOutputStream.putNextEntry(zipEntry)
                    if (entryName == mainClass) {
                        jarOutputStream.write(codeBytes)
                    } else {
                        jarOutputStream.write(IOUtils.toByteArray(inputStream))
                    }
                } catch (Exception e) {
                    LogUtils.r("""error : ${e.getMessage()}""")
                    if (LogUtils.logEnable) { e.printStackTrace() }
                } finally {
                    FileUtils.close(inputStream)
                    if (jarOutputStream != null) {
                        jarOutputStream.closeEntry()
                    }
                }
            }
        } catch (Exception e) {
            LogUtils.r("""error : ${e.getMessage()}""")
            if (LogUtils.logEnable) { e.printStackTrace() }
        } finally {
            FileUtils.close(jarOutputStream, mainJarFile)
        }
    }

    /**
     * 缓存添加类路径
     * @param classPool 类池
     * @param classPathCache 类路径缓存
     * @param classPath 类路径
     */
    static void appendClassPath(ClassPool classPool, classPathCache, classPath) {
        classPathCache.add(classPool.appendClassPath(classPath))
    }

    // 检测classPath是否包含任意一个classList类
    static scanImportClass(String classPath, ComponentRegisterConfig config) {
        ClassPool classPool = null
        def classPathCache = null
        try {
            classPool = new ClassPool()
            classPathCache = classPool.appendClassPath(classPath)
            def clazz = config.classNameList.find {
                classPool.getOrNull(it) != null
            }
            if (clazz != null) {
                config.classPathList.add(classPath)
            }
            if (clazz == config.componentMain) {
                if (classPath.endsWith(".jar")) {
                    File src = new File(classPath)
                    File dest = new File(src.getParent(), "temp_${src.getName()}")
                    org.apache.commons.io.FileUtils.copyFile(src, dest)
                    config.mainClassPath = dest.toString()
                    config.mainJarFilePath = classPath
                } else {
                    config.mainClassPath = classPath
                }
            }
        }  catch (Exception e) {
            LogUtils.r("""error : ${e.getMessage()}""")
            if (LogUtils.logEnable) { e.printStackTrace() }
        } finally {
            if (classPool != null && classPathCache != null) classPool.removeClassPath(classPathCache)
        }
    }
}
复制代码

4.4 使用缓存决解UpToDate

Transform没有配置可更新或者强制更新选项,Transform依赖的Task无法获取(或者许能通过名字获取)。

这样就造成判断是否需要更新执行Transform的条件是内置定义好的,而Gradle配置改变时无法改变UpToDate条件,所以会导致修改了Gradle配置选项但是注入代码没有改变。

[图片上传中...(image-c3c5a6-1615864625685-1)]

4.4.1Gralde 4.10.1跳过任务执行

这里来了解下Task缓存判断条件

决解方法: 基于第4点输入和输出快照变化会使Taask执行条件为true,所以我们可以在需要重新注入代码时,把输出内容的代码注入文件删除即可保证任务正常执行,同时也可以保证缓存使用加快编译速度。

class CacheUtils {
    // 缓存文件夹,在构建目录下
    final static String CACHE_INFO_DIR = "component_register"
    // 缓存文件
    final static String CACHE_CONFIG_FILE_NAME = "config.txt"

    /**
     * 保存配置信息
     * @param project project
     * @param configInfo 配置信息
     */
    static void saveConfigInfo(Project project, ConfigCache configInfo) {
        saveConfigCache(project, new Gson().toJson(configInfo))
    }

    /**
     * 保存配置信息
     * @param project project
     * @param config 配置信息
     */
    static void saveConfigCache(Project project, String config) {
        LogUtils.i("HelperUtils-saveConfigCache :" + " project = " + project + " config = " + config)
        try {
            FileUtils.writeStringToFile(getRegisterInfoCacheFile(project), config, Charset.defaultCharset())
        } catch (Exception e) {
            LogUtils.i("saveConfigCache error ${e.message}")
        }
    }

    /**
     * 读取配置缓存信息
     * @param project project
     * @return 配置信息
     */
    static String readConfigCache(Project project) {
        try {
            return FileUtils.readFileToString(getRegisterInfoCacheFile(project), Charset.defaultCharset())
        } catch (Exception e) {
            LogUtils.i("readConfigCache error ${e.message}")
        }
        return ""
    }

    /**
     * 缓存自动注册配置的文件
     * @param project
     * @return file
     */
    static File getRegisterInfoCacheFile(Project project) {
        File baseFile = new File(getCacheFileDir(project))
        if (baseFile.exists() || baseFile.mkdirs()) {
            File cacheFile = new File(baseFile, CACHE_CONFIG_FILE_NAME)
            if (!cacheFile.exists()) cacheFile.createNewFile()
            return cacheFile
        } else {
            throw new FileNotFoundException("Not found  path:" + baseFile)
        }
    }

    /**
     * 获取缓存文件夹路径
     * @param project project
     * @return 缓存文件夹路径
     */
    static String getCacheFileDir(Project project) {
        return project.getBuildDir().absolutePath + File.separator + AndroidProject.FD_INTERMEDIATES + File.separator + CACHE_INFO_DIR
    }

    /**
     * 判断是否需要强制执行Task
     * @param project project
     * @param config 配置信息
     * @return true:强制执行
     */
    static boolean handleUpToDate(Project project, ComponentRegisterConfig config) {
        LogUtils.i("HelperUtils-handleUpToDate :" + " project = " + project + " config = " + config)
        Gson gson = new Gson()
        String configInfoText = getRegisterInfoCacheFile(project).text
        LogUtils.i("configInfoText = ${configInfoText}")
        ConfigCache configInfo = gson.fromJson(configInfoText, ConfigCache.class)
        LogUtils.i("configInfo = ${configInfo}")
        if (configInfo != null && configInfo.configString != config.toString()) {
            configInfo.destList.each {
                LogUtils.i("delete ${it}")
                File handleFile = new File(it)
                if (handleFile.isDirectory()) {
                    FileUtils.deleteDirectory(handleFile)
                } else {
                    handleFile.delete()
                }
            }
        }
    }

}

复制代码

参考资料

本文在开源项目:https://github.com/Android-Alvin/Android-LearningNotes 中已收录,里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中...

上一篇下一篇

猜你喜欢

热点阅读