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 中插入一行日志的功能。