Android Studio gradle registered plug-in components Plug-in Development ----

Plug-in component registration is resolved not reflected in the development of modular, no new third-party framework, can be confused with demand . In the compilation phase Android Studio Module The host of build.gradlefill assembly configuration information registration code.

effect:

Before using the plug-in App Source:

After using the plug decompile App:

use:

  1. In Project build.gradleAddclasspath
buildscript {
    dependencies {
        // 组件注册插件
        classpath 'com.owm.component:register:1.1.2'
    }
}
复制代码
  1. Registration module build.gradleadd configuration parameters
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();
        ],
    ]
}

复制代码

In the above-described configuration represents com.owm.pluginset.application.Appthe class instanceModuleheader adding the method componentMap.put("LoginInterface", new com.owm.module.login.LoginManager());code.

  1. In componentMaincreating a configuration class instanceModule()methods and componentMapcontainers.
class App {
    public static final HashMap<String, Object> componentMap = new HashMap<>();
    public void instanceModule() {
    }
}
复制代码

Enter the following Gradle after synchronization rebuild the project was filled with success.

Detailed examples and source code plug-ins are welcome Star: github.com/trrying/Plu...

1. Background

Component-based development requires dynamic registration of the various components of the service, without the need to solve in the development of modular reflection, no third-party framework, can confuse needs.

2. Knowledge Point

  • Android Studio build process;
  • Android Gradle Plugin & Transform;
  • Groovy programming language;
  • javassist / asm bytecode modification;

Use Android Studio compilation process in the following figure:

If the entire build process compiler as a river, then there are three rivers into the java compiler stages, namely:

  1. aapt (Android Asset Package Tool) R generated resource file documents;
  2. app source code;
  3. aidl file generation interfaces;

After the above three rivers converge source code will be compiled into class files. To do now is to use a registered Gralde Plugin Transform, inserted after the Java Compileer, processing class files. After processing is complete to the next process to continue building.

3. Build Plug-in Module

3.1 Create a plug-in module

Created in the project Android Library Module (other modules, as long as the corresponding directory structure below), create a directory and delete unnecessary files after completion, leaving only the srcdirectories and build.gradlefiles. Directory structure is as follows:

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

复制代码

There are two main focus points

  1. src/main/groovy Place the plug-in code

  2. src/main/resources Place the plug-in configuration information

    In the src/main/resourcesfollowing resources/META-INF/gradle-pluginsstorage configuration information. A plurality of configuration information may be placed here, each configuration information is a plug. Configuration file name is the plug-in name , for example, I have here is com.owm.component.register.properties, when applications:apply plugin: 'com.owm.component.register'

3.2 plug-in code to create a table of contents

Create a src/main/groovydirectory, then create a path and package name correspond groovy class files in the directory.

3.3 to create a plug-in configuration file

In src/main/resourcesCreating directory resources/META-INF/gradle-pluginsdirectory, and then create a com.owm.component.register.propertiesconfiguration file. Configuration file as follows:

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

Here is the configuration org.gradle.api.Plugininterface implementation class, which is the core configuration entry plug.

//
// 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 Configuration gradle

ComponentRegister build.gradle plug-in module is configured as follows:

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'

复制代码

After gralde sync can be found in Android Studio Gradle panel uploadArchivesTask

When writing plug-in is complete, double-click uploadArchivesTask plug-in will generate local Maven repository configuration.

4. The component registration plug-in functions to achieve

4.1 interface to achieve Plugin

According to src/main/resources/resources/META-INF/gradle-plugins/com.owm.component.register.propertiesthe profile implementation-classvalues to create create a Plugin interface implementation class.

Complete the configuration parameters and get registered in the Transform Plugin implementation class.

Note that loading a configuration item to be delayed a little, for example, project.afterEvaluate{}which acquired.

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 Inheritance Transform

Transform implements the abstract method of integration;

getName(): Configuration name;

getInputTypes(): Configuration process content, content class, for example, jar content, resources, and other content, multi-optional

getScopes(): Configuration processing range, for example, the current module, sub-module, multiple choice;

isIncremental(): Whether to support incremental;

transform(transformInvocation): Conversion logic processing;

  • Determining whether jar need to package the guide, the guide is added to the list of packages
  • Determining whether the required class leader packet, is added to the list of packages guide
  • The configuration code injection config
  • Save Cache

** Note: ** library module can only be configured for the current range block;

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
    }
}
复制代码

In the transform(TransformInvocation transformInvocation)method parameters contains the data to be operated. Using the getInputs()acquired input class or jar contents to traverse the scan matching class, the component code into the instance of the class, and then copy the file to getOutputProvider()obtain the corresponding class or output path inside the 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 registration code injection assembly

Used as a code to javassist instrumentation tool, and then editing the bytecode skilled subsequent pile asm implemented;

  • Loading required jar and class path;
  • The method of injection and obtaining class code;
  • The configuration information code injection component instance;
  • ClassPool release occupied resources;
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 caching solution must UpToDate

Transform can be updated or is not configured to update option, Transform Task dependencies can not be obtained (by name, or promise to obtain).

This resulted in the need to determine whether the update conditional execution Transform is the built-defined, and can not be changed UpToDate conditions Gradle configuration is changed, it will lead to a revision of Gradle configuration options but to inject code has not changed.

4.4.1 Gralde 4.10.1Skip the task execution

Here to understand the conditions under Task cache judge

  • onlyif

    task.onlyif{ false } // return false 跳过任务执行
    复制代码
  • StopExecutionException

    task.doFirst{
        throw new StopExecutionException()
    }
    复制代码
  • enable

    task.enbale = false
    复制代码
  • input和output

    As part of incremental build, Gradle tests whether any of the task inputs or outputs have changed since the last build. If they haven’t, Gradle can consider the task up to date and therefore skip executing its actions. Also note that incremental build won’t work unless a task has at least one task output, although tasks usually have at least one input as well.

    Google Translation: As part of an incremental build, Gradle will test since the last mission to build if there are any input or output has changed. If they do not, Gradle can be considered that the task is up to date, so skip the implementation of its operations. Also note that, unless there is at least one task output task, otherwise incremental build will not work, even though the task usually have at least one input.

    Before the first mission, Gradle will enter snapshot. The path to this snapshot contains a hash of each input file and the contents of the file. Gradle then perform the task. If the task is completed successfully, Gradle will get a snapshot of output. This snapshot includes the output file and set the contents of each file's hash value. Gradle will retain two snapshots on the next mission.

    After each, before performing the task, Gradle will get a new snapshot of inputs and outputs. If the new snapshot with the previous snapshot of the same, Gradle assumes output current and to skip the task. If they are not the same, Gradle will perform the task. Gradle will retain two snapshots on the next mission.

Must solution method: Based on the first four inputs and outputs snapshot changes will Taask execution condition is true, so we can be in the need to re-inject code, the code output the contents of the file can be deleted injection to ensure normal execution of the task, but also can guarantee cache uses speed compilation.

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()
                }
            }
        }
    }

}

复制代码

Reference material

Reproduced in: https: //juejin.im/post/5cf500146fb9a07f0b03ad2f

Guess you like

Origin blog.csdn.net/weixin_34133829/article/details/91411962