前言
Javassist
是一个执行字节码操作的库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。
Javassist 可以绕过编译,直接操作字节码,从而实现代码注入,所以使用Javassist的时机就是在构建工具Gradle将源文件编译成.class文件之后,在将.class打包成dex文件之前
。
具体实现
- 新建
javassist
groovy模块,对应build.gradle配置文件如下:
plugins {
id 'groovy'
id 'maven-publish'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
implementation gradleApi()
implementation 'org.javassist:javassist:3.20.0-GA' //引入javassist依赖
implementation 'com.android.tools.build:gradle:4.0.2'
}
publishing {
publications {
maven(MavenPublication) {
groupId = 'com.javassist' //自定义 pom.groupId一般为包名
artifactId = 'plugin' //自定义 pom.artifactId 一般为项目名称
version = '1.0.3' //版本号
from components.java //生成的类型,一定要制定类型
}
}
repositories {
maven {
name = 'repo'
url = "../repo" //表示发布到本地repo目录下,目录位于根目录下
}
}
}
- 自定义
ModifyPlugin
并注册自定义Transform
class ModifyPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
println("=========ModifyPlugin===========")
project.android.registerTransform(new ModifyTransform(project))
}
}
- 自定义
ModifyTransform
并使用javassist
进行代码插入
import com.android.build.api.transform.Format
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.internal.pipeline.TransformManager
import javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
class ModifyTransform extends Transform {
def project
def pool = ClassPool.default
ModifyTransform(Project project) {
this.project = project
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println("start ModifyTransform..........")
//1.先拿到需要处理的class文件
transformInvocation.inputs.each { allInput ->
//类最终生成为两种形式;1.文件夹(包含包名)2.jar包
//1.1先从文件夹中找到我们需要的Class文件
allInput.directoryInputs.each { directoryInput ->
String preClassNamePath = directoryInput.file.absolutePath
println("Class 文件路径$preClassNamePath") //找到class文件路径
//插入文件路径到Pool内存池中
pool.insertClassPath(preClassNamePath)
//找到目标class文件,进行改写
findTarget(directoryInput.file, preClassNamePath)
//1.2获取输出的文件夹
def dest = transformInvocation.outputProvider.getContentLocation(
directoryInput.name,
directoryInput.contentTypes,
directoryInput.scopes,
Format.DIRECTORY
)
println("文件夹输出路径为:$dest")
// 需要把文件复制给下一个transform使用,不然下一个transform任务拿不到,也就无法编译出apk文件
FileUtils.copyDirectory(directoryInput.file, dest)
}
//1.3 从jar包拿到需要处理的class文件(如果工程中没有jar包,则不需要此步骤)
allInput.jarInputs.each {
//1.4获取输出的文件夹
def dest = transformInvocation.outputProvider.getContentLocation(it.name
, it.contentTypes, it.scopes, Format.JAR)
println("Jar包输出文件的路径:$dest")
FileUtils.copyFile(it.file, dest)
}
}
}
/**
* fileNamePath:build/tmp/kotlin-classes/debug
*/
private void findTarget(File dir, String fileNamePath) {
//如果当前是文件夹,不断遍历查找
if (dir.isDirectory()) {
dir.listFiles().each {
findTarget(it, fileNamePath)
}
} else {
String targetPath = dir.absolutePath
if (targetPath.endsWith(".class")) {
//进行文件修改
modify(targetPath, fileNamePath)
}
}
}
/**
* 使用javassist进行文件修改
*/
private void modify(String targetPath, String fileNamePath) {
//过滤无用的class文件
if (targetPath.contains("R\$") || targetPath.contains("R.class") || targetPath.contains("BuildConfig.class")) {
return
}
//javassist 需要对应的class名称
String classPackageName =
targetPath.replace(fileNamePath, "").replace("\\", ".").replace("/", ".")
String className = classPackageName.replace(".class", "").substring(1)
println("需要修改的className:$className")
pool.appendClassPath(project.android.bootClasspath[0].toString())
CtClass ctClass = pool[className]
//插入代码
addCode(ctClass, fileNamePath)
}
private void addCode(CtClass ctClass, String fileNamePath) {
//设置class可修改
if (ctClass.isFrozen()) {
ctClass.defrost()
}
//类似于反射获取class所有的方法
CtMethod[] declaredMethods = ctClass.declaredMethods
for (method in declaredMethods) {
if (method.name == "onCreate") {
method.addLocalVariable("startTime", CtClass.longType)
method.insertBefore("{startTime = System.currentTimeMillis();}")
method.insertAfter("{ long totalTime = System.currentTimeMillis() - startTime;\n" + " System.out.println(\"方法耗时:\" + totalTime);" + "}")
}
}
//把修改的内容写入文件
ctClass.writeFile(fileNamePath)
//释放内存
ctClass.detach()
}
//任务名称
@Override
String getName() {
return "ModifyTransform"
}
//想要处理的文件类型 .class文件
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
//想要处理的范围 这里为整个project
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
//是否是增量编译
@Override
boolean isIncremental() {
return false
}
}
- 定义插件配置文件
resources
目录下新建META-INF
目录,META-INF目录下
新建gradle-plugins
,新增插件配置文件com.javassist.properties
,其中com.javassist
为最终插件名称;
文件对应配置信息如下:
implementation-class=com.crystal.javassist.ModifyPlugin
整体javassist
插件目录模块结构如下:
- 编译
javassist
模块,并上传至本地repo仓库;
会在根目录下生成repo
文件夹内容如下:
- 引用插件
- 根目录
build.gradle
新增配置:
repositories {
maven {
allowInsecureProtocol(true)
url uri('./repo')
}
gradlePluginPortal()
google()
mavenCentral()
}
dependencies {
classpath 'com.javassist:plugin:1.0.3' //自定义的插件
classpath "org.javassist:javassist:3.20.0-GA" //javassist配置
}
allprojects {
repositories {
maven {
allowInsecureProtocol(true)
url uri('./repo')
}
gradlePluginPortal()
google()
mavenCentral()
}
}
- app模块引入插件
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.javassist' -- 引入javassist插件
}
最终效果
public void onCreate(Bundle savedInstanceState) {
long startTime = System.currentTimeMillis(); //javassist插入的代码段
super.onCreate(savedInstanceState);
setContentView(C0589R.layout.activity_main);
InjectUtils.INSTANCE.inject(this);
TextView textView = this.f103tv;
if (textView == null) {
Intrinsics.throwUninitializedPropertyAccessException("tv");
textView = null;
}
textView.setText("aaaa");
System.out.println(new StringBuffer().append("方法耗时:").append(System.currentTimeMillis() - startTime).toString());//javassist插入的代码段
}
结语
如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )