asm与javassist作为修改字节码文件的两种常用方式,在编译过程中,可以动态修改class字节码文件。前文已用javassist方式实现热修复的前置步骤,本文用asm实现动态的给每个activity插入Log日志。
一、插件的生成与依赖导入
1、用该文章中的方法:Android中Plugin插件工程的自动生成 自动生成一个android的插件模板;
2、加入asm依赖,加入依赖后的插件build的格式如下:
dependencies{
compile gradleApi()
compile localGroovy()
implementation 'com.android.tools.build:gradle:3.6.3'
//asm依赖
implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'
}
在最外层的build中,添加依赖如下:
dependencies {
classpath 'com.android.tools.build:gradle:3.6.3'
//asm路径
classpath 'com.sunny.asmplugin:asmplugin:1.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
二、插件的注册与编写
修改AsmPlugin文件如下,该文件将SunnyAsmTransForm注册到工程中。
public class AsmPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
def android = project.extensions.getByType(AppExtension)
println '----------- registering SunnyAsmTransForm -----------'
SunnyAsmTransForm transform = new SunnyAsmTransForm()
android.registerTransform(transform)
println '----------- registered SunnyAsmTransForm -----------'
}
}
SunnyAsmTransForm负责实现具体逻辑:
package com.sunny.asmplugin
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import groovy.io.FileType
import org.apache.commons.io.FileUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
/**
* @author shenxiao* @version V1.0* @package com.sunny.easyuse* @date 2020/6/22 4:34 PM
*/
public class SunnyAsmTransForm extends Transform {
SunnyAsmTransForm() {
}
@Override
public String getName() {
return "SunnyAsmTransForm"
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
public Set<QualifiedContent.Scope> getScopes() {
return TransformManager.PROJECT_ONLY
}
@Override
public boolean isIncremental() {
return false;
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
//拿到所有的class文件
Collection<TransformInput> transformInputs = transformInvocation.inputs
TransformOutputProvider outputProvider = transformInvocation.outputProvider
if (outputProvider != null) {
outputProvider.deleteAll()
}
transformInputs.each { TransformInput transformInput ->
// 遍历directoryInputs(文件夹中的class文件) directoryInputs代表着以源码方式参与项目编译的所有目录结构及其目录下的源码文件
// 比如我们手写的类以及R.class、BuildConfig.class以及MainActivity.class等
transformInput.directoryInputs.each { DirectoryInput directoryInput ->
File dir = directoryInput.file
if (dir) {
dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File file ->
System.out.println("find class: " + file.name)
//对class文件进行读取与解析
ClassReader classReader = new ClassReader(file.bytes)
//对class文件的写入
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
//访问class文件相应的内容,解析到某一个结构就会通知到ClassVisitor的相应方法
ClassVisitor classVisitor = new SunnyClassVisitor(classWriter)
//依次调用 ClassVisitor接口的各个方法
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
//toByteArray方法会将最终修改的字节码以 byte 数组形式返回。
byte[] bytes = classWriter.toByteArray()
//通过文件流写入方式覆盖掉原先的内容,实现class文件的改写。
//FileOutputStream outputStream = new FileOutputStream( file.parentFile.absolutePath + File.separator + fileName)
FileOutputStream outputStream = new FileOutputStream(file.path)
outputStream.write(bytes)
outputStream.close()
}
}
//处理完输入文件后把输出传给下一个文件
def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes,
directoryInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
}
}
上文中涉及到的SunnyClassVisitor与SunnyMethodVisitor,是实现修改字节码的具体文件:
SunnyClassVisitor中指定了需要访问的方法:
package com.sunny.asmplugin;
/**
* @author shenxiao
* @version V1.0
* @package com.sunny.asmplugin
* @date 2020/9/8 11:41 AM
*/
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class SunnyClassVisitor extends ClassVisitor {
private String className;
private String superName;
public SunnyClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.className = name;
this.superName = superName;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println("ClassVisitor visitMethod name-------" + name + ", superName is " + superName);
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
if (superName.equals("android/support/v7/app/AppCompatActivity")
|| superName.equals("androidx/appcompat/app/AppCompatActivity") ) {
if (name.startsWith("onCreate")) {
//处理onCreate()方法
return new SunnyMethodVisitor(mv, className, name);
}
}
return mv;
}
@Override
public void visitEnd() {
super.visitEnd();
}
}
SunnyMethodVisitor实现方法中修改字节码逻辑:
package com.sunny.asmplugin;
/**
* @author shenxiao
* @version V1.0
* @package com.sunny.asmplugin
* @date 2020/9/8 11:42 AM
*/
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class SunnyMethodVisitor extends MethodVisitor {
private String className;
private String methodName;
public SunnyMethodVisitor(MethodVisitor methodVisitor, String className, String methodName) {
super(Opcodes.ASM5, methodVisitor);
this.className = className;
this.methodName = methodName;
}
//方法执行前插入
@Override
public void visitCode() {
super.visitCode();
System.out.println("MethodVisitor visitCode------");
mv.visitLdcInsn("TAG");
mv.visitLdcInsn(className + "---->" + methodName);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
}
}
到此完成了插件编写,将插件上传到本地后,可应用插件。
三、插件的使用
新建工程,工程中含MainActivity文件。在工程的build中,加上:
apply plugin: 'com.sunny.asmplugin'
编译工程后,可在app/build/intermediates/transforms/SunnyAsmTransForm/debug/0/com/sunny/asm/MainActivity.class中查看插入的日志代码:
public class MainActivity extends AppCompatActivity {
public MainActivity() {
}
protected void onCreate(Bundle savedInstanceState) {
//此处为asm新增的日志代码
Log.i("TAG", "com/sunny/asm/MainActivity---->onCreate");
super.onCreate(savedInstanceState);
this.setContentView(2131296284);
}
}
至此,完成asm动态修改字节码逻辑。