Android AOP(三):在Android中Plugin Transform Javassist操作Class文件
Javassist作用是在编译器间修改class文件,与之相似的ASM(热修复框架女娲)也有这个功能,可以让我们直接修改编译后的class二进制代码,首先我们得知道什么时候编译完成,并且我们要赶在class文件被转化为dex文件之前去修改。在Transfrom这个api出来之前,想要在项目被打包成dex之前对class进行操作,必须自定义一个Task,然后插入到predex或者dex之前,在自定义的Task中可以使用javassist或者asm对class进行操作。而Transform则更为方便,Transfrom会有他自己的执行时机,不需要我们插入到某个Task前面。Tranfrom一经注册便会自动添加到Task执行序列中,并且正好是项目被打包成dex之前。
1. Transfrom
Gradle是通过一个一个Task执行完成整个流程的,其中肯定也有将所有class打包成dex的task。
(在gradle plugin 1.5 以上和以下版本有些不同)
1.5以下,preDex这个task会将依赖的module编译后的class打包成jar,然后dex这个task则会将所有class打包成dex
1.5以上,preDex和Dex这两个task已经消失,取而代之的是TransfromClassesWithDexForDebug
2. 列表项
Transfrom是Gradle 1.5以上新出的一个api,其实它也是Task,不过定义方式和Task有点区别。
对于热补丁来说,Transfrom反而比原先的Task更好用。
在Transfrom这个api出来之前,想要在项目被打包成dex之前对class进行操作,必须自定义一个Task,然后插入到predex或者dex之前,在自定义的Task中可以使用javassist或者asm对class进行操作。
而Transform则更为方便,Transfrom会有他自己的执行时机,不需要我们插入到某个Task前面。Tranfrom一经注册便会自动添加到Task执行序列中,并且正好是项目被打包成dex之前。
3. Task的inputs和outputs
Gradle可以看做是一个脚本,包含一系列的Task,依次执行这些task后,项目就打包成功了。
而Task有一个重要的概念,那就是inputs和outputs。
Task通过inputs拿到一些东西,处理完毕之后就输出outputs,而下一个Task的inputs则是上一个Task的outputs。
例如:一个Task的作用是将java编译成class,这个Task的inputs就是java文件的保存目录,outputs这是编译后的class的输出目录,它的下一个Task的inputs就会是编译后的class的保存目录了。
4. Plugin
Gradle中除了Task这个重要的api,还有一个就是Plugin。
Plugin的作用是什么呢,这一两句话比较难以说明。
Gralde只能算是一个构建框架,里面的那么多Task是怎么来的呢,谁定义的呢?
是Plugin,细心的网友会发现,在module下的build.gradle文件中的第一行,往往会有apply plugin : ‘com.android.application’亦或者apply plugin : ‘com.android.library’。
com.android.application:这是app module下Build.gradle的
com.android.library:这是app依赖的module中的Builde.gradle的
就是这些Plugin为项目构建提供了Task,使用不同的plugin,module的功能也就不一样。
可以简单的理解为: Gradle只是一个框架,真正起作用的是plugin。而plugin的主要作用是往Gradle脚本中添加Task。 当然,实际上这些是很复杂的东西,plugin还有其他作用这里用不上。
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
compile gradleApi()
compile localGroovy()
compile 'com.android.tools.build:gradle:3.0.0'
compile 'org.javassist:javassist:3.22.0-GA'
compile 'org.aspectj:aspectjtools:1.8.1'
}
repositories {
mavenCentral()
}
//发布到本地
uploadArchives {
repositories.mavenDeployer {
repository(url: uri('../repo')) //仓库的路径,此处是项目根目录下的 repo 的文件夹
pom.groupId = 'com.example' //groupId ,自行定义,一般是包名
pom.artifactId = 'plugin' //artifactId ,自行定义
pom.version = '2.0.0' //version 版本号
}
}
package com.example
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
public class MyClassTransform extends Transform {
private Project mProject;
public MyClassTransform(Project p) {
this.mProject = p;
}
//transform的名称
//transformClassesWithMyClassTransformForDebug 运行时的名字
//transformClassesWith + getName() + For + Debug或Release
@Override
public String getName() {
return "MyClassTransform";
}
//需要处理的数据类型,有两种枚举类型
//CLASSES和RESOURCES,CLASSES代表处理的java的class文件,RESOURCES代表要处理java的资源
@Override
public Set getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
// 指Transform要操作内容的范围,官方文档Scope有7种类型:
//
// EXTERNAL_LIBRARIES 只有外部库
// PROJECT 只有项目内容
// PROJECT_LOCAL_DEPS 只有项目的本地依赖(本地jar)
// PROVIDED_ONLY 只提供本地或远程依赖项
// SUB_PROJECTS 只有子项目。
// SUB_PROJECTS_LOCAL_DEPS 只有子项目的本地依赖项(本地jar)。
// TESTED_CODE 由当前变量(包括依赖项)测试的代码
@Override
public Set getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
//指明当前Transform是否支持增量编译
@Override
public boolean isIncremental() {
return false;
}
// Transform中的核心方法,
// inputs中是传过来的输入流,其中有两种格式,一种是jar包格式一种是目录格式。
// outputProvider 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
@Override
public void transform(Context context,
Collection inputs,
Collection referencedInputs,
TransformOutputProvider outputProvider,
boolean isIncremental) throws IOException, TransformException, InterruptedException {
System.out.println("----------------进入transform了--------------")
//遍历input
inputs.each { TransformInput input ->
//遍历文件夹
input.directoryInputs.each { DirectoryInput directoryInput ->
//注入代码
MyInjects.inject(directoryInput.file.absolutePath, mProject)
// 获取output目录
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)//这里写代码片
// 将input的目录复制到output指定目录
FileUtils.copyDirectory(directoryInput.file, dest)
}
遍历jar文件 对jar不操作,但是要输出到out路径
input.jarInputs.each { JarInput jarInput ->
// 重命名输出文件(同目录copyFile会冲突)
def jarName = jarInput.name
println("jar = " + jarInput.file.getAbsolutePath())
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
}
System.out.println("--------------结束transform了----------------")
}
}
package com.example
import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project
public class MyPlugin implements Plugin<Project> {
void apply(Project project) {
System.out.println("------------------开始----------------------");
System.out.println("这是我们的自定义插件!");
//AppExtension就是build.gradle中android{...}这一块
def android = project.extensions.getByType(AppExtension)
//注册一个Transform
def classTransform = new MyClassTransform(project);
android.registerTransform(classTransform);
//创建一个Extension,名字叫做testCreatJavaConfig 里面可配置的属性参照MyPlguinTestClass
project.extensions.create("testCreatJavaConfig", MyPlguinTestClass)
//生产一个类
if (project.plugins.hasPlugin(AppPlugin)) {
//获取到Extension,Extension就是 build.gradle中的{}闭包
android.applicationVariants.all { variant ->
//获取到scope,作用域
def variantData = variant.variantData
def scope = variantData.scope
//拿到build.gradle中创建的Extension的值
def config = project.extensions.getByName("testCreatJavaConfig");
//创建一个task
def createTaskName = scope.getTaskName("CeShi", "MyTestPlugin")
def createTask = project.task(createTaskName)
//设置task要执行的任务
createTask.doLast {
//生成java类
createJavaTest(variant, config)
}
//设置task依赖于生成BuildConfig的task,然后在生成BuildConfig后生成我们的类
String generateBuildConfigTaskName = variant.getVariantData().getScope().getGenerateBuildConfigTask().name
def generateBuildConfigTask = project.tasks.getByName(generateBuildConfigTaskName)
if (generateBuildConfigTask) {
createTask.dependsOn generateBuildConfigTask
generateBuildConfigTask.finalizedBy createTask
}
}
}
System.out.println("------------------结束了吗----------------------");
}
static def void createJavaTest(variant, config) {
//要生成的内容
def content = """package tv.danmaku.ijk.media.sample;
public class MyPlguinTestClass {
public static final String str = "${config.str}";
}
""";
//获取到BuildConfig类的路径
File outputDir = variant.getVariantData().getScope().getBuildConfigSourceOutputDir()
def javaFile = new File(outputDir, "MyPlguinTestClass.java")
javaFile.write(content, 'UTF-8');
}
}
//
class MyPlguinTestClass {
def str = "默认值";
}
package com.example
import javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import org.gradle.api.Project
public class MyInjects {
//初始化类池
private final static ClassPool pool = ClassPool.getDefault();
public static void inject(String path,Project project) {
//将当前路径加入类池,不然找不到这个类
pool.appendClassPath(path);
//project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类
pool.appendClassPath(project.android.bootClasspath[0].toString());
//引入android.os.Bundle包,因为onCreate方法参数有Bundle
pool.importPackage("android.os.Bundle");
File dir = new File(path);
if (dir.isDirectory()) {
//遍历文件夹
dir.eachFileRecurse { File file ->
String filePath = file.absolutePath
println("filePath = " + filePath)
if (file.getName().equals("FileListActivity.class")) {
//获取MainActivity.class
CtClass ctClass = pool.getCtClass("tv.danmaku.ijk.media.sample.FileListActivity");
println("ctClass = " + ctClass)
//解冻
if (ctClass.isFrozen())
ctClass.defrost()
//获取到OnCreate方法
CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate")
println("方法名 = " + ctMethod)
String insetBeforeStr = """ android.widget.Toast.makeText(this,"我是被插入的Toast代码~!!",android.widget.Toast.LENGTH_SHORT).show();
"""
//在方法开头插入代码
ctMethod.insertBefore(insetBeforeStr);
ctClass.writeFile(path)
ctClass.detach()//释放
}
}
}
}
}
其实作用就是在找到tv.danmaku.ijk.media.sample.FileListActivity的oncreate方法插入一个toast。
然后新建main/resources/META-INF/gradle-plugins/plugin.test.properties
implementation-class=com.example.MyPlugin
此处文件名module里面会用到,class名对应plugin名。
执行gradle projects里面plugin的task upload,会在本地生成一个repo目录,保存生成的jar包
module 里同apply 插件名plugin.test
...
apply plugin: 'plugin.test'
...
dependencies {
...
classpath 'com.example:plugin:2.0.0'
}
sync project并make project后在module的build/intermediates/classes/debug…对应包名目录下生成
MyPlguinTestClass.java上面的如下代码实现。
File outputDir = variant.getVariantData().getScope().getBuildConfigSourceOutputDir()
def javaFile = new File(outputDir, "MyPlguinTestClass.java")
javaFile.write(content, 'UTF-8');
同时安装APP后,会在弹出toast上面的如下代码实现。
//获取MainActivity.class
CtClass ctClass = pool.getCtClass("tv.danmaku.ijk.media.sample.FileListActivity");
println("ctClass = " + ctClass)
//解冻
if (ctClass.isFrozen())
ctClass.defrost()
//获取到OnCreate方法
CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate")
println("方法名 = " + ctMethod)
String insetBeforeStr = """ android.widget.Toast.makeText(this,"我是被插入的Toast代码~!!",android.widget.Toast.LENGTH_SHORT).show();
"""
//在方法开头插入代码
ctMethod.insertBefore(insetBeforeStr);
ctClass.writeFile(path)
ctClass.detach()//释放
以上就是javaassit动态编译代码简单实现。
感谢,参考:
Android热补丁动态修复技术(三)—— 使用Javassist注入字节码,完成热补丁框架雏形(可使用)
Android动态编译技术:Plugin Transform Javassist操作Class文件
安卓AOP三剑客:APT,AspectJ,Javassist