Android使用Javassist自定义Plugin插件实现字节码修改

前言

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目录结构

  • 编译javassist模块,并上传至本地repo仓库;
    编译javassist模块
    会在根目录下生成repo文件夹内容如下:
    repo文件夹
  • 引用插件
  1. 根目录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()

    }
}
  1. 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插入的代码段
    }

结语

如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值