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.gradle
fill assembly configuration information registration code.
effect:
Before using the plug-in App Source:
After using the plug decompile App:
use:
- In Project
build.gradle
Addclasspath
buildscript {
dependencies {
// 组件注册插件
classpath 'com.owm.component:register:1.1.2'
}
}
复制代码
- Registration module
build.gradle
add 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.App
the class instanceModule
header adding the method componentMap.put("LoginInterface", new com.owm.module.login.LoginManager());
code.
- In
componentMain
creating a configuration classinstanceModule()
methods andcomponentMap
containers.
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:
- aapt (Android Asset Package Tool) R generated resource file documents;
- app source code;
- 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 src
directories and build.gradle
files. 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
-
src/main/groovy
Place the plug-in code -
src/main/resources
Place the plug-in configuration informationIn the
src/main/resources
followingresources/META-INF/gradle-plugins
storage 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 iscom.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/groovy
directory, 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/resources
Creating directory resources/META-INF/gradle-plugins
directory, and then create a com.owm.component.register.properties
configuration file. Configuration file as follows:
implementation-class=com.owm.component.register.plugin.RegisterPlugin
复制代码
Here is the configuration org.gradle.api.Plugin
interface 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 uploadArchives
Task
When writing plug-in is complete, double-click uploadArchives
Task 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.properties
the profile implementation-class
values 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.1
Skip 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