2.AMS 代码插桩
上一篇搞了自定义gradle插件,继续来搞AMS代码插桩,AMS代码插桩比较简单,就是拷拷代码。首先修改TestPlugin.groovy
package com.androidtv.test_plugin
import com.android.build.api.transform.*
import com.android.build.gradle.AppExtension
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.IOUtils
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
class TestPlugin extends Transform implements Plugin<Project> {
static boolean isApp
@Override
void apply(Project project) {
//用来判断是APP 还是其它的lib
isApp = project.plugins.hasPlugin(AppPlugin)
def android
if (isApp) {
//如果为app,使用AppExtension
android = project.extensions.getByType(AppExtension)
} else {
//如果为lib,使用LibraryExtension
android = project.extensions.getByType(LibraryExtension)
}
android.registerTransform(this)
}
@Override
String getName() {
return "TestPlugin"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
//同样区分app和lib
if (isApp){
return TransformManager.SCOPE_FULL_PROJECT
}else {
return TransformManager.PROJECT_ONLY
}
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(TransformInvocation transformInvocation) {
println '--------------- TestPlugin visit start --------------- '
def startTime = System.currentTimeMillis()
Collection<TransformInput> inputs = transformInvocation.inputs
TransformOutputProvider outputProvider = transformInvocation.outputProvider
//删除之前的输出
if (outputProvider != null)
outputProvider.deleteAll()
//遍历inputs
inputs.each { TransformInput input ->
//遍历directoryInputs
input.directoryInputs.each { DirectoryInput directoryInput ->
handleDirectoryInput(directoryInput, outputProvider)
}
//遍历jarInputs
input.jarInputs.each { JarInput jarInput ->
handleJarInputs(jarInput, outputProvider)
}
}
def cost = (System.currentTimeMillis() - startTime) / 1000
println '--------------- TestPlugin visit end --------------- '
println "TestPlugin cost : $cost s"
}
/**
* 处理文件目录下的class文件
*/
static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
//是否是目录
if (directoryInput.file.isDirectory()) {
//列出目录所有文件(包含子文件夹,子文件夹内文件)
directoryInput.file.eachFileRecurse { File file ->
def name = file.name
if (checkClassFile(name)) {
println '----------- deal with "class" file <' + name + '> -----------'
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new AmsClassVisitor(classWriter)
classReader.accept(cv, ClassReader.EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
}
}
}
//处理完输入文件之后,要把输出给下一个任务
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
/**
* 处理Jar中的class文件
*/
static void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {
if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
//重名名输出文件,因为可能同名,会覆盖
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
JarFile jarFile = new JarFile(jarInput.file)
Enumeration enumeration = jarFile.entries()
File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
//避免上次的缓存被重复插入
if (tmpFile.exists()) {
tmpFile.delete()
}
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
//用于保存
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = jarFile.getInputStream(jarEntry)
//插桩class
if (checkClassFile(entryName)) {
//class文件处理
println '----------- deal with "jar" class file <' + entryName + '> -----------'
jarOutputStream.putNextEntry(zipEntry)
ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new AmsClassVisitor(classWriter)
classReader.accept(cv, ClassReader.EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
jarOutputStream.write(code)
} else {
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
jarOutputStream.closeEntry()
}
//结束
jarOutputStream.close()
jarFile.close()
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(tmpFile, dest)
tmpFile.delete()
}
}
/**
* 检查class文件是否需要处理
* @param fileName
* @return
*/
static boolean checkClassFile(String name) {
//只处理需要的class文件
return (name.endsWith(".class") && !name.startsWith("R\$")
&& !"R.class".equals(name) && !"BuildConfig.class".equals(name)
&& name.contains("Activity") && !name.contains("androidx/"))
}
}
其中AmsClassVisitor和AmsMethodVisitor为:
package com.androidtv.test_plugin;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class AmsClassVisitor extends ClassVisitor implements Opcodes {
private static final String[] methodNames = {"onCreate", "onStart", "onResume", "onPause", "onStop", "onDestroy"};
private String mClassName;
private ArrayList<String> names;
public AmsClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
List<String> list = Arrays.asList(methodNames);
names = new ArrayList<>(list);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.mClassName = name;
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
if (this.mClassName.contains("Activity") && !this.mClassName.contains("androidx/")) {
//过滤方法,可以设置对应的过滤规则,过滤想要插桩的方法
if (match(name)) {
System.out.println("AmsClassVisitor : change method ----> " + name);
return new AmsMethodVisitor(mv, name);
}
}
return mv;
}
private boolean match(String name) {
return names.contains(name);
}
@Override
public void visitEnd() {
super.visitEnd();
}
}
package com.androidtv.test_plugin;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
class AmsMethodVisitor extends MethodVisitor {
private String mName;
public AmsMethodVisitor(MethodVisitor methodVisitor, String name) {
super(Opcodes.ASM4, methodVisitor);
mName = name;
}
@Override
public void visitCode() {
super.visitCode();
//插入代码,在方法执行前
}
@Override
public void visitInsn(int opcode) {
if (opcode == Opcodes.RETURN) {
//插入代码在方法执行完成后
}
super.visitInsn(opcode);
}
}
没有什么难的,就拷拷代码就好了,然后我们插入想插入的代码即可,接下来插入代码,使用ASM插件可以将我们想插入的代码转换成字节码的形式,我使用的是下面这个插件,支持kt
WechatIMG778.jpeg
使用ASM插件,将我们想插入的代码写好,例如我想插入一个log打印
WechatIMG779.jpeg
在当前类中写完,右击显示ASM转化代码,将对应的代码拷贝到plugin中的AmsMethodVisitor中
@Override
public void visitCode() {
super.visitCode();
Label label2 = new Label();
mv.visitLabel(label2);
mv.visitLineNumber(16, label2);
//修改一下方法名
mv.visitLdcInsn(mName);
mv.visitVarInsn(ASTORE, 2);
Label label3 = new Label();
mv.visitLabel(label3);
mv.visitLineNumber(17, label3);
mv.visitLdcInsn("TAG");
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitVarInsn(ALOAD, 2);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitLdcInsn(": --------->");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);
}
重新执行uploadArchives生成新的插件,运行app可以在log中看到打印了对应的日志
WechatIMG780.jpeg
需要注意的是,需要在当前类有对应的方法,才会插桩,如果当前类没有对应的方法,是不会插桩的。到这里,ASM插桩就完成了,比较简单,主要是注意导包,利用插桩可以使用Trace.beginSection()方法进行全方法插桩,检测性能。
网友评论