Android开发中Javassist的妙用

2023-11-06

Javassist

Java字节码以二进制的形式存储在.class文件中,每一个class文件包含一个Java类或接口。Javassist框架就是一个用来处理Java字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者修改已有的方法,并且不需要对字节码方面有深入的了解。
Javassist可以绕过编译,直接操作字节码,从而实现代码的注入。所以,使用Javassist框架的最佳时机就是在构建工具gradle将源文件编译成class文件之后,在将class打包成dex文件之前。

Javassist基础

  • 读写字节码
    在Javassist框架中,class文件是用类Javassist.CtClass表示的。一个CtClass对像可以处理一个class文件。
    下面举一个简单的例子。
ClassPool pool=ClassPool.getDefault();
CtClass aClass = pool.get("com.test.A");
aClass.setSuperclass("java.lang.Object");
aClass.writeFile();

在上面这个例子中,我们首先获取一个ClassPool对像。ClassPool是CtClass对像的容器,可以按需读取类文件用来创建并保存CtClass对像,以便之后可能会被使用到。
为了修改类的定义,首先需要使用ClassPool.get()方法从ClassPool中获取一个CtClass对像。使用getDefault方法获取的ClassPool对像使用的是默认系统的类搜索路径。
ClassPool是一个存储CtClass的Hash表,类的名称作为Hash表的key。ClassPool的get方法会从Hash表查找key对应的CtClass对像。如果根据对应的key没有找到CtClass对像,get方法就会创建并返回一个新的CtClass对像,这个对像同时也会保存在Hash表中。
从ClassPool中获取的CtClass对像是可以被修改的。在上面的例子中,将A类的父类改为Object。调用writeFile方法后,这项修改会被写入原始类文件中。writeFile方法会将CtClass对象转换成类文件并写到本地磁盘。同时,也可以使用toBytecode方法来获取修改过的字节码。比如:

byte[] b = aClass.toBytecode();

也可以使用toClass方法直接将CtClass对象转换成Class对象,比如:

Class clazz= aClass.toClass();

toClass方法请求当前线程的ClassLoader加载CtClass对象所代表的类文件,它返回的是该类文件的Class对象。

  • 冻结类
    如果一个CtClass对象通过writeFile、toBytecode、toClass等方法被转换成一个类文件,此CtClass对象就会被冻结起来,不再允许修改,这是因为一个类只能被jvm加载一次。
    其实,一个冻结的CtClass对象也可以被解冻,比如:
aClass.writeFile();
aClass.defrost();//解冻
aClass.setSuperClass();//因为这个类已经解冻了,所以可以更改该类
  • 类搜索路径
    通过ClassPool.getDefault()获取的ClassPool是使用JVM的类搜索路径。如果程序运行在Tomcat等服务器上,ClassPool可能无法找到用户的类,因为Web服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool必须添加额外的类搜索路径,比如:
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassPath(this.getClass()));

在上面的代码示例中,将this指向的类添加到ClassPool的类加载路径中。你可以使用任意Class对象来代替this.getClass(),从而将Class对象添加到类加载路径中,也可以注册一个目录作为搜索路径。比如:

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath("/usr/local/Library");
  • ClassPool
    ClassPool是CtClass对象的容器。因为编译器在编译引用CtClass代表的Java类的源代码时,可能会引用CtClass对象,所以一旦一个CtClass被创建,它就会被保存在ClassPool中。

  • 避免内存溢出
    如果CtClass对象的数量变得非常多,ClassPool有可能会导致巨大的内存消耗。为了避免这个问题,我们可以从ClassPool中显式删除不必要的CtClass对象。如果对CtClass对象调用detach方法,那么该CtClass对象将会从ClassPool中删除。比如:

CtClass aClass = ...;
aClass.writeFile();
aClass.detach();

在调用detach方法之后,就不能再调用这个CtClass对象的任何有关方法了。如果调用了ClassPool的get方法,ClassPool会再次读取这个类文件,并创建一个新的CtClass对象。

  • 注解(Annotations)
    CtClass、CtMethod、CtField和CtConstructor均提供了getAnnotations方法,用于读取对应类型上添加的注解。

  • 在方法体中插入代码
    CtMethod和CtConstructor均提供了insertBefore、insertAfter、addCatch等方法。它们可以把用Java编写的代码片段插入到现有的方法体中。Javassist包括一个用于处理源代码的小型编译器,它接收用Java编写的源代码,然后将其编译成Java字节码,并内联到方法体中。
    也可以按行号来插入代码段(如果行号表包含在类文件中)。向CtMethod和CtConstructor中的insertAt方法提供源代码和原始类定义中的源文件的行号,就可以将编译后的代码插入到指定行号位置。
    insertBefore、insertAfter、insertAt、addCatch等方法都能接收一个表示语句或语句块的String对象。一个语句是一个单一的控制结构,比如if和while,或者以分号结尾的表达式。语句块是一组用{}包围的语句。
    语句和语句块可以引用字段和方法,但不允许访问在方法中声明的局部变量,尽管在块中声明一个新的局部变量是允许的。
    传递给方法insertBefore、insertAfter、addCatch、insertAt的String对象是由Javassist的编译器编译的。

由于编译器支持语言扩展,所以以$开头的几个标识符都有特殊的含义:

  • $0、$1、$2,…
    传递给目标方法的参数使用$1、$2,…来访问,而不是原始的参数名称。$1表示第1个参数,$2表示第2个参数,以此类推。这些变量的类型与参数类型相同。$0等价于this指针。如果方法是静态的,则$0不可用。
  • $args
    变量$args表示所有参数的数组。该变量的类型是Object类型的数组。如果参数类型是原始类型(如int、boolean等),则该参数值将被转换为包装器对象(如java.lang.Integer)以存储在$args中。因此,如果第一个参数的类型不是原始类型,那么$args[0]等于$1。
  • $$
    变量$$是所有参数列表的缩写,用逗号分隔。
  • $_
    CtMethod中的insertAfter方法是在方法的末尾插入编译的代码。在传递给 insertAfter的语句中,不但可以使用特殊符号,如$0、$1,也可以使用$_来表示方法的结果值。
    该变量的类型是方法的返回类型。如果返回结果类型是void,那么$_的类型为Object,$_的值为null。虽然由insertAfter插入的编译代码通常在方法返回之前执行,但是当方法抛出异常时,也可以执行。要在抛出异常时执行它,insertAfter的第二个参数asFinally必须为true。如果抛出异常,$_的值为0或null。在编译代码的执行终止后,最初抛出的异常被重新抛出给调用者。注意,$_的值不会被抛给调用者,而是被抛弃。
  • addCatch
    addCatch插入方法体抛出异常时执行的代码,控制权会返回给调用者。在插入的源代码中,异常用$e表示。
CtMethod m = ...;
CtClass etype = ClassPool.getDefault().get("java.io.IOException");
m.addCatch("{System.out.println($e);throw $e}",etype);

转换成对应的java代码如下:

try{
  //the original method body
}catch(java.io.IOException e){
   System.out.println(e);
   throw e;
}

注意,插入的代码片段必须以throw或return语句结束。

案例

首先,创建一个buildSrc文件夹,引入依赖

apply plugin: 'groovy'//gradle会根据插件的名称,找到这个插件并且调用插件里面的apply方法

repositories {
    maven { url 'https://maven.aliyun.com/repository/google' }
    maven {
        url 'https://maven.aliyun.com/nexus/content/groups/public/'
    }
//    jcenter()
}

sourceSets {
    main {
        groovy {
            srcDir 'src/main/groovy'
        }
    }
}

dependencies {
//    implementation gradleApi() //buildSrc目录下可以不引用gradleApi
    implementation 'com.android.tools.build:gradle:3.4.3'
}

group 'com.brett.gradle'
//version '1.0.2'

然后,在resources文件夹下面创建一个properties

implementation-class=com.brett.gradle.ReleaseHelperPlugin

创建一个插件:

package com.brett.gradle;

import com.android.build.gradle.AppExtension;
import com.android.build.gradle.api.ApplicationVariant
import com.brett.gradle.tasks.GenerateApkTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.invocation.Gradle

import java.util.regex.Matcher
import java.util.regex.Pattern;


class ReleaseHelperPlugin implements Plugin<Project> {

    private static final String sPluginExtensionName = "releaseHelper";
    private static final String ANDROID_EXTENSION_NAME = "android";
    private Project project;


    @Override
    public void apply(Project project) {
        this.project = project;
       // project.getExtensions().create(sPluginExtensionName, CustomExtension.class,project);
        project.getExtensions().findByType(AppExtension).registerTransform(new ReleaseHelperTransform(project))
    }
}
class ReleaseHelperTransform extends Transform {
    private static Project project

    ReleaseHelperTransform(Project project) {
        this.project = project
    }

    @Override
    String getName() {
        return "ReleaseHelperTransform"
    }

    /**
     * 需要处理的数据类型,有两种枚举类型
     * CLASSES 代表处理的 java 的 class 文件,RESOURCES 代表要处理 java 的资源
     * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 指 Transform 要操作内容的范围,官方文档 Scope 有 7 种类型:
     * 1. EXTERNAL_LIBRARIES        只有外部库
     * 2. PROJECT                   只有项目内容
     * 3. PROJECT_LOCAL_DEPS        只有项目的本地依赖(本地jar)
     * 4. PROVIDED_ONLY             只提供本地或远程依赖项
     * 5. SUB_PROJECTS              只有子项目。
     * 6. SUB_PROJECTS_LOCAL_DEPS   只有子项目的本地依赖项(本地jar)。
     * 7. TESTED_CODE               由当前变量(包括依赖项)测试的代码
     * @return
     */
    @Override
    Set<QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
        if (!incremental) {
            outputProvider.deleteAll()
        }

        /**Transform 的 inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历 */
        inputs.each { TransformInput input ->
            /**遍历 jar*/
            input.jarInputs.each { JarInput jarInput ->
                /**重命名输出文件(同目录copyFile会冲突)*/
                String destName = jarInput.file.name

                /**截取文件路径的 md5 值重命名输出文件,因为可能同名,会覆盖*/
                def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath).substring(0, 8)
                /** 获取 jar 名字*/
                if (destName.endsWith(".jar")) {
                    destName = destName.substring(0, destName.length() - 4)
                }
                def dest = outputProvider.getContentLocation(destName + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(jarInput.file, dest)

                context.getTemporaryDir().deleteDir()
            }

            /**遍历目录*/
            input.directoryInputs.each { DirectoryInput directoryInput ->
                CodeInjects.inject(directoryInput.file.absolutePath, project)

                def dest = outputProvider.getContentLocation(directoryInput.name,
                        directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                /**将input的目录复制到output指定目录*/
                FileUtils.copyDirectory(directoryInput.file, dest)
            }
        }
    }
}

import javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import org.gradle.api.Project

public class CodeInjects {
    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())

        File dir = new File(path)
        if(dir.isDirectory()){
            //遍历目录
            dir.eachFileRecurse {File file->
                String filePath = file.absolutePath
                println("CodeInjects filePath:"+filePath)
                if(file.getName().equals("MainActivity.class")){

                    //获取MainActivity.class
                    CtClass ctClass = pool.getCtClass("com.sogou.teemo.test_use_gradle_plugin.MainActivity");
                    println("CodeInjects ctClass = "+ctClass)

                    if(ctClass.isFrozen()){
                        ctClass.defrost()
                    }

                    //获取到onCreate方法
                    CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate");
                    println("CodeInjects 方法名 = " + ctMethod)

                    String insetBeforeStr = """ android.widget.Toast.makeText(this,"插件中自动生成的代码",android.widget.Toast.LENGTH_SHORT).show();
                                            """

                    ctMethod.insertAfter(insetBeforeStr)

                    ctClass.writeFile(path)

                    ctClass.detach()//释放

                }
            }
        }


    }


}

参考资料

https://github.com/jboss-javassist/javassist/wiki/Tutorial-1

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android开发中Javassist的妙用 的相关文章

随机推荐

  • unity打飞碟

    unity hw4 1 编写一个简单的鼠标打飞碟 Hit UFO 游戏 游戏的演示视频地址 http v youku com v show id XMzU0Mjg0NDg3Mg html spm a2h3j 8428770 3416059
  • Python初学 Anaconda安装方法

    Anaconda 指的是一个开源的Python发行版本 其包含了Python conda等180多个科学包及其依赖项 conda是一个开源的包 环境管理器 可以用于在同一个机器上安装不同版本的软件包及其依赖 并能够在不同的环境之间切换 因为
  • 下载blob地址或m3u8格式视频方法以及常见问题解决

    STEP1 在一些视频播放网站视频下方没有直接的下载按钮 此时可以先用电脑F12键查看元素 会出现类似如下界面 STEP2 该界面左上角的位置有一个指针形状的按钮 选用这个按钮 点一下网页里视频播放的区域 审查元素界面会跳转到类似如下的界面
  • Pandas知识点-合并操作join

    Pandas知识点 合并操作join 在Pandas中 join 方法也可以用于实现合并操作 本文介绍join 方法的具体用法 一 基础合并操作 join other 将一个或多个DataFrame加入到当前DataFrame中 实现合并的
  • Redis基础

    文章目录 1 Redis入门 Redis简介 Redis下载与安装 Redis启动与停止 设置Redis密码和允许远程连接 2 Redis数据类型 3 Redis常用命令 字符串string操作命令 哈希hash操作命令 列表list操作命
  • Windows防止程序多开并在多开时弹出已运行程序

    Windows下防止程序多开并在多开时弹出已运行的程序 1 功能说明 在Windows客户端开发时 往往需要禁止客户多开程序的情况 并且在客户再次双击启动图标时显示已启动的程序界面 故而需要下面的功能 使用CreateMutex禁止程序多开
  • CNN网络,CNN+SVM网络对故障分类(python代码)

    1 数据集可以使用多种数据集验证 例如 CWRU PU IMS JNU SEU PHM2010等等 这里使用的是IMS 辛辛那提 正常 内圈故障 外圈故障 滚动体故障 原始数据下载官网 Prognostics Center of Excel
  • CentOS7 彻底清除MySQL

    MySQL完全删除 参考文档 http www centoscn com mysql 2017 0517 8791 html 查看已经安装的服务 rpm qa grep i mysql i 作用是不区分大小写 删除这两个服务 去掉后缀 rp
  • [思维模式-7]:《如何系统思考》-3- 认识篇 - 什么是系统?系统的特征?

    目录 第1章 什么是系统 1 1 万事万物都是一个有机的系统 1 2 系统的科学定义 1 3 系统的构成 1 4 系统的分类 第2章 动态复杂系统的八大特征 2 1 目的性 2 8 边界 2 3 结构影响行为 2 4 总体大于部分之和 2
  • 数据大屏适配方案 (scale)

    目录 适配方案分析 vw vh 什么是vw和vh vw和百分比的区别是什么 vw怎么使用 实现思路 案例 scale方案 一 scale 方法 1 scaleX x 2 scaleY y 3 scale x y 案例 大屏之关键 前期的自适
  • TemplateSyntaxError at /statistics/ ‘staticfiles‘ is not a registered tag li

    报错django template exceptions TemplateSyntaxError staticfiles is not a registered tag library Mustbe one of admin list ad
  • app基本控件

    一个完整的APP包括四大类 各种 栏 内容视图 控制元素 临时视图 各种 栏 状态栏 导航栏 标签栏 工具栏 搜索栏 范围栏 内容视图 列表视图 卡片视图 集合视图 图片视图 文本视图 控制元素 用于控制产品行为或显示的信息 临时视图 警告
  • IDEA设置启动选择项目

    IDEA设置启动选择项目 IDEA2019 3 5启动后自行选择项目 而不是进入上一次关闭的项目 通过下面的设置修改 将Reopen last project on startup勾选去除
  • 从零开始学Qt(四)信号与槽

    信号与槽 书不记 熟读可记 义不精 细思可精 1 信号 槽是啥 古有 烽火狼烟 传递消息 敌人来犯的消息迅速传达开来 是多么的聪慧啊 烟就是信号 下一个燃火台看到烟后就点燃燃料 这个就槽 换种方法说就是 一个按钮被点击了 会触发一个点击的信
  • 校园网连路由器

    1 校园网 并记住账号及其密码 2 一台电脑 Windows 系统 3 一台路由器 4 两条网线 准备完成 下面开始正式干活 第一步 电脑插上网线 路由器通电插上网线 在你的电脑登陆你的校园网 第二步 打开你的电脑搜索CMD 并以管理员权限
  • QString和QByteArray的区别

    QString和QByteArray的区别 本质 格式转换 QString字符串格式化打印 长度 本质 QString是对QByteArray的再次封装 QString可以通过char 来构造 也可以通过QByteArray来构造 QByt
  • CentripetalNet: Pursuing High-quality Keypoint Pairs for Object Detection解读

    摘要 基于关键点的检测器得到了不错的性能 但关键点匹配错误是普遍性存在的 严重的影响了其检测器的性能 作者通过centripetal shif将相同的类别的实例分开 CentripetalNet是预测角点的位置和向心偏移 通过向向心偏移来对
  • Web API: URL.createObjectURL()实践

    1 问题 URL createObjectURL的介绍如下 The URL createObjectURL static method creates a DOMString containing a URL representing th
  • 图形学数学基础之1D采样分布计算方法Inverse Method

    作者 i dovelemon 日期 2017 09 04 来源 CSDN 主题 Rendering Equation Probability Density Function Cumulative Density Function 引言 前
  • Android开发中Javassist的妙用

    Javassist Java字节码以二进制的形式存储在 class文件中 每一个class文件包含一个Java类或接口 Javassist框架就是一个用来处理Java字节码的类库 它可以在一个已经编译好的类中添加新的方法 或者修改已有的方法