Android(安卓)AOP(三):在Android中Javassist动态编译代码
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
更多相关文章
- Android(安卓)JNI知识简介
- Android(安卓)Material Design 控件之TabLayout 学习
- Android(安卓)Makefile and build system 分析
- 改进Android(安卓)SlidingMenu实现QQ样式边侧滑抽屉技术
- android 资源文件的种类
- 使用greenDao操作本地数据库,Android9.0读取数据库失败的问题解决
- 如何解决android NDK r8c 老是重新编译源代码的问题
- Android(安卓)menu默认样式的设置
- android apk反编译(zz)