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还有其他作用这里用不上。

使用步骤:

开发plugin,并应用到module

  • 新建一个module,选择library module
  • 删除module下的所有文件,除了build.gradle,清空build.gradle中的内容
  • 然后新建以下目录 src/main/groovy
  • 修改build.gradle如下
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 版本号    }}
  • 开发插件MyPlugin
  • 开发MyClassTransform
  • 在transform()里面对目录和class注入操作
    代码如下:
package com.exampleimport com.android.build.api.transform.*import com.android.build.gradle.internal.pipeline.TransformManagerimport org.apache.commons.codec.digest.DigestUtilsimport org.apache.commons.io.FileUtilsimport org.gradle.api.Projectpublic 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.exampleimport com.android.build.gradle.AppExtensionimport com.android.build.gradle.AppPluginimport org.gradle.api.Pluginimport org.gradle.api.Projectpublic 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.exampleimport javassist.ClassPoolimport javassist.CtClassimport javassist.CtMethodimport org.gradle.api.Projectpublic 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

更多相关文章

  1. Android(安卓)JNI知识简介
  2. Android(安卓)Material Design 控件之TabLayout 学习
  3. Android(安卓)Makefile and build system 分析
  4. 改进Android(安卓)SlidingMenu实现QQ样式边侧滑抽屉技术
  5. android 资源文件的种类
  6. 使用greenDao操作本地数据库,Android9.0读取数据库失败的问题解决
  7. 如何解决android NDK r8c 老是重新编译源代码的问题
  8. Android(安卓)menu默认样式的设置
  9. android apk反编译(zz)

随机推荐

  1. Android 抗锯齿的设置
  2. Android 插件化框架replugin使用教程之本
  3. Android Studio 报错 ERROR: Could not d
  4. 谷歌升级Android翻译 支持即时语音翻译
  5. Android(安卓)GLSurfaceView
  6. android笔记一(Button)
  7. android 对话框显示工具类
  8. Android中Task任务栈的四种模式
  9. Android拦截Home键
  10. Android常用工具类