Android 代码插桩,可以在源文件的任意位置插入自定义代码。
由于插入代码需要使用 groovy 脚本编写一些插件,所以可以先了解插件相关知识:Gradle 脚本相关知识
asm插桩项目准备工作
在这里,首选新建一个空白的项目。一般来说 AndroidStudio 空白项目跑起来会是 Hello World ,使用此空白项目进行测试
asm 插桩需要入口,所以,这里使用 buildSrc
默认路径的方式,进行脚本编写。
首先在项目根目录,创建一个文件夹名称:buildSrc
这个文件夹必须在项目根目录,与 app 目录同级
文件夹名称必须使用 buildSrc 。这是默认的插件目录
然后在 buildSrc 目录下创建子目录:
buildSrc
|----src
|----|----main
|----|----|----java
然后在 java
目录下再次创建子目录(此时的子目录是java文件的包路径)
然后在 buildSrc
目录下新建文件 build.gradle
,文件内容如下:
apply plugin: 'java-library' sourceSets { main{ // groovy{ // srcDir 'src/main/groovy' // } java{ srcDir 'src/main/java' } resources { srcDir 'src/main/resources' } } } //获取远程库的仓库地址 repositories { jcenter() mavenCentral() maven { url "https://dl.google.com/dl/android/maven2/" } } //开发插件需要的库 dependencies { implementation gradleApi() implementation localGroovy() implementation 'com.android.tools.build:gradle:4.0.1' implementation 'org.ow2.asm:asm:7.0' implementation 'org.ow2.asm:asm-commons:7.0' }
在一些文章中,可能使用的是
groovy
编写脚本。而此处我使用的是 java 编写脚本。其中:dependencies
中的依赖项都是需要的。否则在编写过程中可能会出现类找不到的问题
编写插件
编写一个 AsmTransform
文件内容如下:
package plugin; import com.android.build.api.transform.QualifiedContent; import com.android.build.api.transform.Transform; import java.util.Set; public class AsmTransform extends Transform { @Override public String getName() { return null; } @Override public Set<QualifiedContent.ContentType> getInputTypes() { return null; } @Override public Set<? super QualifiedContent.Scope> getScopes() { return null; } @Override public boolean isIncremental() { return false; } }
编写一个 Task 入口文件
package plugin; import com.android.build.gradle.AppExtension; import org.gradle.api.Plugin; import org.gradle.api.Project; public class Main implements Plugin<Project> { @Override public void apply(Project project) { System.out.println("test"); AppExtension appExtension = project.getExtensions().getByType(AppExtension.class); // println '----------- 开始注册 >>>>> -----------' AsmTransform transform = new AsmTransform(); appExtension.registerTransform(transform); } }
配置task
当上方两个的 Transform 和 Main 入口文件编写完成后,在 app 项目中引用:
打开项目根目录/app 目录的 build.gradle
文件
在文件最末尾添加:
apply plugin: Main
这里的
Main
就是编写的入口文件名称,如果取名是其它的,则自行修改。
在编写后,会提示无法找到 Main 的java 文件,使用智能提示即可正确导入,一般会在文件最前面导入路径:
import plugin.Main
如果包名路径不同,则 import 的路径也会不同
实现代码插入功能
当上方的配置都完成后,已经可以实现代码注入了。接下来只要实现
AsmTransform
中的方法即可
分别修改 AsmTransform.java
文件中的四个方法:
@Override public String getName() { return "AsmTransform"; } @Override public Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS; } @Override public Set<? super QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT; } @Override public boolean isIncremental() { return false; }
然后再重新实现 transform
方法:
System.out.println("start trans form"); long startTime = System.currentTimeMillis(); Collection<TransformInput> inputs = transformInvocation.getInputs(); TransformOutputProvider outputProvider = transformInvocation.getOutputProvider(); //删除旧的输出 if (outputProvider != null) { outputProvider.deleteAll(); } inputs.forEach(transformInput -> { transformInput.getDirectoryInputs().forEach(directoryInput -> { handleDirectoryInput(directoryInput, outputProvider); }); transformInput.getJarInputs().forEach(jarInput -> { try { handleJarInput(jarInput, outputProvider); } catch (IOException e) { e.printStackTrace(); } }); }); long endTime = System.currentTimeMillis(); System.out.println("end time:" + (endTime - startTime));
一些新建的方法实现如下:
static void transJavaFile(File file) { String name = file.getName(); if (isClassFile(name)) { System.out.println("-------------------- handle class file:<$name> --------------------" + file.getName()); try { ClassReader classReader = new ClassReader(new FileInputStream(file)); ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS); ClassVisitor classVisitor = new ActivityClassVisitor(classWriter); classReader.accept(classVisitor, org.objectweb.asm.ClassReader.EXPAND_FRAMES); byte[] bytes = classWriter.toByteArray(); FileOutputStream fileOutputStream = new FileOutputStream(file.getParentFile().getAbsolutePath() + File.separator + name); // FileOutputStream fileOutputStream = new FileOutputStream(name); fileOutputStream.write(bytes); fileOutputStream.close(); System.out.println("-------------------- handle class file: success --------------------"); } catch (IOException e) { System.out.println("-------------------- handle class file: error --------------------"); e.printStackTrace(); } } } static void getAllFile(File inFile) { if (inFile.isDirectory()) { for (File fileChild : Objects.requireNonNull(inFile.listFiles())) { getAllFile(fileChild); } } else { transJavaFile(inFile); } } /** * 处理目录下的class文件 * * @param directoryInput * @param outputProvider */ static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) { //是否为目录 if (directoryInput.getFile().isDirectory()) { //列出目录所有文件(包含子文件夹,子文件夹内文件) getAllFile(directoryInput.getFile()); } File dest = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY); try { FileUtils.copyDirectory(directoryInput.getFile(), dest); } catch (IOException e) { e.printStackTrace(); } } /** * 处理Jar中的class文件 * * @param jarInput * @param outputProvider */ static void handleJarInput(JarInput jarInput, TransformOutputProvider outputProvider) throws IOException { if (jarInput.getFile().getAbsolutePath().endsWith(".jar")) { //重名名输出文件,因为可能同名,会覆盖 String jarName = jarInput.getName(); String md5Name = DigestUtils.md5Hex(jarInput.getFile().getAbsolutePath()); if (jarName.endsWith(".jar")) { jarName = jarName.substring(0, jarName.length() - 4); } JarFile jarFile = new JarFile(jarInput.getFile()); Enumeration<JarEntry> enumeration = jarFile.entries(); File tempFile = new File(jarInput.getFile().getParent() + File.separator + "temp.jar"); //避免上次的缓存被重复插入 if (tempFile.exists()) { tempFile.delete(); } JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tempFile)); //保存 while (enumeration.hasMoreElements()) { JarEntry jarEntry = enumeration.nextElement(); String entryName = jarEntry.getName(); ZipEntry zipEntry = new ZipEntry(entryName); InputStream inputStream = jarFile.getInputStream(zipEntry); if (isClassFile(entryName)) { System.out.println("-------------------- handle jar file:<$entryName> --------------------"); jarOutputStream.putNextEntry(zipEntry); ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream)); ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS); ClassVisitor classVisitor = new ActivityClassVisitor(classWriter); classReader.accept(classVisitor, org.objectweb.asm.ClassReader.EXPAND_FRAMES); byte[] bytes = classWriter.toByteArray(); jarOutputStream.write(bytes); } else { jarOutputStream.putNextEntry(zipEntry); jarOutputStream.write(IOUtils.toByteArray(inputStream)); } jarOutputStream.closeEntry(); } jarOutputStream.close(); jarFile.close(); File dest = outputProvider.getContentLocation(jarName + "_" + md5Name, jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR); FileUtils.copyFile(tempFile, dest); tempFile.delete(); } } /** * 判断是否为需要处理class文件 * * @param name * @return */ static boolean isClassFile(String name) { return (name.endsWith(".class") && !name.startsWith("R$") && "R.class" != name && "BuildConfig.class" != name && name.contains("Activity")); }
上方代码的基本逻辑是,遍历每一个 class
以及 jar
文件。然后通过判断,看是否是当前需要修改的 class
文件。
然后将 class 文件的相关信息传入 Visitor 中进行判断修改:
ActivityClassVisitor
实现如下:
package plugin; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; public class ActivityClassVisitor extends ClassVisitor implements Opcodes { private String mClassName; private static final String CLASS_NAME_ACTIVITY = "com/example/plugintest/MainActivity"; private static final String METHOD_NAME_ONCREATE = "onCreate"; private static final String METHOD_NAME_ONDESTROY = "onDestroy"; public ActivityClassVisitor(ClassVisitor cv) { super(Opcodes.ASM5, cv); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { mClassName = name; System.out.println("-------------------- ActivityClassVisitor,visit mClassName:" + mClassName + " --------------------"); super.visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions); if (CLASS_NAME_ACTIVITY.equals(mClassName)) { if (METHOD_NAME_ONCREATE.equals(name)) { System.out.println("-------------------- ActivityClassVisitor,visit method:" + name + " --------------------"); return new ActivityOnCreateMethodVisitor(Opcodes.ASM5, methodVisitor); } else if (METHOD_NAME_ONDESTROY.equals(name)) { System.out.println("-------------------- ActivityClassVisitor,visit method:" + name + " --------------------"); return new ActivityOnDestroyMethodVisitor(Opcodes.ASM5, methodVisitor); } } return methodVisitor; } }
在这个 ActivityClassVisitor
中,首先判断是否是当前我要修改的文件。如果是,则往两个方法中插入代码,这明哥方法名是:onCreate
, onDestory
他们的修改器实现如下:
ActivityOnCreateMethodVisitor
修改器
package plugin; import org.objectweb.asm.MethodVisitor; import static org.objectweb.asm.Opcodes.INVOKESTATIC; import static org.objectweb.asm.Opcodes.POP; public class ActivityOnCreateMethodVisitor extends MethodVisitor { public ActivityOnCreateMethodVisitor(int api, MethodVisitor mv) { super(api, mv); } @Override public void visitCode() { System.out.println("-------------------- ActivityOnCreateMethodVisitor,visitCode --------------------"); mv.visitLdcInsn("ASMPlugin"); mv.visitLdcInsn("-------------------- MainActivity onCreate --------------------"); mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;" + "Ljava/lang/String;)I", false); mv.visitInsn(POP); System.out.println("-------------------- ActivityOnCreateMethodVisitor,visitCode over --------------------"); super.visitCode(); } @Override public void visitInsn(int opcode) { super.visitInsn(opcode); } }
ActivityOnDestroyMethodVisitor
修改器代码:
package plugin; import org.objectweb.asm.MethodVisitor; import static groovyjarjarasm.asm.Opcodes.INVOKESTATIC; import static groovyjarjarasm.asm.Opcodes.POP; public class ActivityOnDestroyMethodVisitor extends MethodVisitor { public ActivityOnDestroyMethodVisitor(int api, MethodVisitor mv) { super(api, mv); } @Override public void visitCode() { super.visitCode(); mv.visitLdcInsn("ASMPlugin"); mv.visitLdcInsn("-------------------- MainActivity onDestroy --------------------"); mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;" + "Ljava/lang/String;)I", false); mv.visitInsn(POP); } @Override public void visitInsn(int opcode) { super.visitInsn(opcode); } }
点击运行,在执行 Task :app:transformClassesWithAsmTransformForDebug
时,会往 class 插入自定义的代码。
然后在控制台即可看到日志输出:
2021-01-05 12:09:16.207 13686-13686/com.example.plugintest I/ASMPlugin: -------------------- MainActivity onCreate --------------------
至此,已经实现了往 Activity 中插入一行日志的功能。
微信扫一扫:分享
微信里点“发现”,扫一下
二维码便可将本文分享至朋友圈。