Android 中使用Javassist

来源:互联网 发布:搜狗输入法云计算进程 编辑:程序博客网 时间:2024/05/20 17:59

Javassist

Javassist 是一个执行字节码操作的库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。

Javassist 可以绕过编译,直接操作字节码,从而实现代码注入,所以使用 Javassist 的时机就是在构建工具 Gradle 将源文件编译成 .class 文件之后,在将 .class 打包成 dex 文件之前。

Gradle

Android Studio 项目是使用 Gradle 构建的,构建工具 Gradle 可以看做是一个脚本,包含一系列的Task,依次执行这些 Task 后,项目就打包成功了。

而 Task 有一个重要的概念,那就是 inputsoutputs
Task 通过 inputs 拿到一些东西,处理完毕之后就输出 outputs ,而下一个 Task 的 inputs 则是上一个 Task 的outputs。

例如:一个 Task 的作用是将 java 编译成 class,这个 Task 的 inputs 就是 java 文件的保存目录,outputs 这是编译后的 class 的输出目录,它的下一个 Task 的 inputs 就会是编译后的 class 的保存目录了。


Gradle 由一个个 Task 组成,而这些 Task 都是由 Plugin 来定义的。

比如:

apply plugin : 'com.android.application' 这个 插件定义了将 Module 编译成 application 的一系列 Task。

apply plugin : 'com.android.library' 这个 插件定义了将 Module 编译成 library 的一系列 Task。

不同的 Plugin 提供了不同的 Task 来实际不同的功能。

可以简单的理解为: Gradle只是一个框架,真正起作用的是plugin。而plugin的主要作用是往Gradle脚本中添加Task


我们需要在整个 Gradle 工作的过程中,找到合适的时机来插入自定义的 Plugin,然后在 Plugin 中使用 Javassist 对字节进行操作 ,所以使用 Javassit 的前提是掌握自定义 Gradle 插件,不清楚的可以看前面的 自定义 Gradle 插件 ,这里不多介绍。

在前面说了 Javassist 工作的时机,这个时机在 Gradle1.5 之前并不容易掌握的,从1.5.0-beta1开始,android的gradle插件引入了com.android.build.api.transform.Transform,可以点击 http://tools.android.com/tech-docs/new-build-system/transform-api 查看相关内容,我们刚好就可以利用这个 API 来使用 Javassist 。

transform


流程

1.在 插件的module 中添加依赖:

apply plugin: 'groovy'dependencies {    compile gradleApi() //gradle sdk    compile localGroovy() //groovy sdk    compile 'com.android.tools.build:gradle:2.2.0'    compile 'org.javassist:javassist:3.20.0-GA'}repositories {    jcenter()}

2.在自定义的插件中调用 自定义Transform

public class JavassistPlugin implements Plugin<Project> {    void apply(Project project) {        def log = project.logger        log.error "========================";        log.error "Javassist开始修改Class!";        log.error "========================";        project.android.registerTransform(new JavassistTransform(project))    }}

3.自定义 Transform

package com.deemons.busimport com.android.build.api.transform.*import com.google.common.collect.Setsimport javassist.ClassPoolimport org.apache.commons.io.FileUtilsimport org.gradle.api.Projectpublic class JavassistTransform extends Transform {    Project project    public JavassistTransform(Project project) {    // 构造函数,我们将Project保存下来备用        this.project = project    }    @Override    String getName() {// 设置我们自定义的Transform对应的Task名称        return "JavassistTrans"    }    @Override    // 指定输入的类型,通过这里的设定,可以指定我们要处理的文件类型这样确保其他类型的文件不会传入    Set<QualifiedContent.ContentType> getInputTypes() {        return Sets.immutableEnumSet(QualifiedContent.DefaultContentType.CLASSES)    }    @Override// 指定Transform的作用范围    Set<QualifiedContent.Scope> getScopes() {        return Sets.immutableEnumSet(QualifiedContent.Scope.PROJECT, QualifiedContent.Scope.PROJECT_LOCAL_DEPS,                QualifiedContent.Scope.SUB_PROJECTS, QualifiedContent.Scope.SUB_PROJECTS_LOCAL_DEPS,                QualifiedContent.Scope.EXTERNAL_LIBRARIES)    }    @Override    boolean isIncremental() {        return false    }    @Override    void transform(Context context, Collection<TransformInput> inputs,                   Collection<TransformInput> referencedInputs,                   TransformOutputProvider outputProvider, boolean isIncremental)            throws IOException, TransformException, InterruptedException {        def startTime = System.currentTimeMillis();        // Transform的inputs有两种类型,一种是目录,一种是jar包,要分开遍历        inputs.each { TransformInput input ->            try {                //对 jar包 类型的inputs 进行遍历                input.jarInputs.each {                    //这里处理自定义的逻辑                    MyInject.injectDir(it.file.getAbsolutePath(), "com", project)                    // 重命名输出文件(同目录copyFile会冲突)                    String outputFileName = it.name.replace(".jar", "") + '-' + it.file.path.hashCode()                    def output = outputProvider.getContentLocation(outputFileName, it.contentTypes, it.scopes, Format.JAR)                    FileUtils.copyFile(it.file, output)                }            } catch (Exception e) {                project.logger.err e.getMessage()            }            //对类型为“文件夹”的input进行遍历            input.directoryInputs.each { DirectoryInput directoryInput ->                //文件夹里面包含的是我们手写的类以及R.class、BuildConfig.class以及R$XXX.class等                //这里处理自定义的逻辑                MyInject.injectDir(directoryInput.file.absolutePath, "com", project)                // 获取output目录                def dest = outputProvider.getContentLocation(directoryInput.name,                        directoryInput.contentTypes, directoryInput.scopes,                        Format.DIRECTORY)                // 将input的目录复制到output指定目录                FileUtils.copyDirectory(directoryInput.file, dest)            }        }        ClassPool.getDefault().clearImportedPackages();        project.logger.error("JavassistTransform cast :" + (System.currentTimeMillis() - startTime) / 1000 + " secs");    }}

以上其实都算是模板了,具体怎样使用 javassist ,后面有参考链接,源码里面也有示例,这里就不介绍了。

源码参考 Demo :JavassistDemo

参考

通过自定义Gradle插件修改编译后的class文件

Android热补丁动态修复技术(三)—— 使用Javassist注入字节码,完成热补丁框架雏形(可使用)

安卓AOP实战:Javassist强撸EventBus

Javassist 使用指南(一)

Javassist 使用指南(二)

Javassist 使用指南(三)

原创粉丝点击