使用ASM对字节码插桩

新建一个空的Android工程,只有一个MainActivity:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

使用ASM框架,插入两行日志代码,分别在onCreate方法的第一行和最后一行,目标代码:

@Override
 protected void onCreate(Bundle savedInstanceState) {
        Log.e("MainActivity", "log from asm enter");
        super.onCreate(savedInstanceState);
        this.setContentView(R.layout.activity_main);
        Log.e("MainActivity", "log from asm exit");
    }

最终效果

2024-03-14 16:02:49.215 32007-32007/com.example.myapp E/MainActivity: log from asm enter
2024-03-14 16:02:49.371 32007-32007/com.example.myapp E/MainActivity: log from asm exit

实现步骤

新建一个Java-library module,asm-plugin,build.gradle如下配置:

apply plugin: 'java-library'  // Java-library module必须应用的插件,新建之后默认会有
apply plugin: 'groovy' // 自定义的插件类使用groovy编写
apply plugin: 'java-gradle-plugin'  // 自动注册自定义的插件,否则需要手动在main目录下新建resources/META-INF/gradle-plugins/${plugin-id}.properties


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation gradleApi()
    implementation localGroovy()
    implementation 'org.ow2.asm:asm:9.0' // ASM依赖
    implementation 'org.ow2.asm:asm-commons:9.0'
    implementation 'com.android.tools.build:gradle:3.6.3'

}

gradlePlugin {
    plugins {
        AsmPlugin {
            id = 'com.example.asm-plugin' // 在app等模块使用,例如:apply plugin: 'com.example.asm-plugin'
            implementationClass = 'com.example.asm.AsmPlugin' //等价于在${plugin-id}.properties里面配置implementation-class = com.example.asm.AsmPlugin
        }
    }
}

sourceCompatibility = "7"
targetCompatibility = "7"

sourceSets {
    main {
        groovy {
            srcDir 'src/main/groovy'  //配置groovy源码路径
        }
        java {
            srcDir 'src/main/java'  //配置Java源码路径
        }
        resources {
            srcDir 'src/main/resources'  //配置resources路径,如果手动创建
        }
    }
}

//apply from: file('maven.gradle')
apply from: file('publish.gradle') // 发布到MavenLocal的配置

看下插件module目录结构:

sunshuo@sunshuo-virtual-machine:~/AndroidStudioProjects/myApp/asm-plugin$ tree
.
├── asm-plugin.iml
├── build.gradle
├── maven.gradle
├── publish.gradle
└── src
    └── main
        ├── groovy
        │   └── com
        │       └── example
        │           └── asm
        │               └── AsmPlugin.groovy
        └── java
            └── com
                └── example
                    └── asm
                        ├── AsmTransform.java
                        ├── ClassVisitorAdapter.java
                        └── MethodAdviceAdapter.java

10 directories, 8 files

AsmPlugin.groovy内容如下:

class AsmPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        def extension = project.extensions.getByType(AppExtension)
        extension.registerTransform(new AsmTransform())  //注册一个自定义的AsmTransform
    }
}

AsmTransform.java内容如下:

public class AsmTransform extends Transform {

    private static final List<String> CLASS_FILE_IGNORE = Arrays.asList("R.class", "R$", "Manifest", "BuildConfig.class"); // 忽略这些文件

    @Override
    public String getName() {
        return "AsmPlugin";
    }

    @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 true;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws IOException {
        TransformOutputProvider provider = transformInvocation.getOutputProvider();
        for (TransformInput input : transformInvocation.getInputs()) {
            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
                // directoryInput 的输入目录是:app/build/intermediates/javac/debug/classes
                doDirInputTransform(directoryInput);
                copyQualifiedContent(provider, directoryInput, null, Format.DIRECTORY);
            }
            for (JarInput jarInput : input.getJarInputs()) {
                copyQualifiedContent(provider, jarInput, getUniqueName(jarInput.getFile()), Format.JAR);
            }
        }
    }

    //根据输入的目录,遍历要插桩的class文件
    private void doDirInputTransform(DirectoryInput input) {
        List<File> files = new ArrayList<>();
        listFiles(files, input.getFile());
        for (File file : files) {
            try {
                doAsm(file);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    //核心在这里,读出来二进制文件,访问方法,插入字节码,再写回去
    private void doAsm(File file) throws IOException {
        InputStream is = null;
        FileOutputStream fos = null;
        try {
            is = new FileInputStream(file);
            ClassReader reader = new ClassReader(is);
            ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
            ClassVisitor visitor = new ClassVisitorAdapter(Opcodes.ASM5, writer);
            reader.accept(visitor, ClassReader.EXPAND_FRAMES);
            byte[] code = writer.toByteArray();
            fos = new FileOutputStream(file.getAbsoluteFile());
            fos.write(code);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                is.close();
            }
            if (fos != null) {
                fos.close();
            }
        }
    }

   // 列出所有的文件
    private void listFiles(List<File> files, File file) {
        if (file == null) {
            return;
        }

        if (file.isDirectory()) {
            File[] fl = file.listFiles();
            if (fl == null || fl.length == 0) {
                return;
            }
            for (File f : fl) {
                listFiles(files, f);
            }
        } else if (needTrack(file.getName())) {
            files.add(file);
        }
    }

    private boolean needTrack(String name) {
        boolean ret = false;
        if (name.endsWith(".class")) {
            ret = !CLASS_FILE_IGNORE.contains(name);
        }
        return ret;
    }

    private void copyQualifiedContent(TransformOutputProvider provider, QualifiedContent file, String fileName, Format format) throws IOException {
        boolean useDefaultName = fileName == null;
        File dest = provider.getContentLocation(useDefaultName ? file.getName() : fileName, file.getContentTypes(), file.getScopes(), format);
        if (!dest.exists()) {
            dest.mkdirs();
            dest.createNewFile();
        }
        if (useDefaultName) {
            FileUtils.copyDirectory(file.getFile(), dest);
        } else {
            FileUtils.copyFile(file.getFile(), dest);
        }
    }

    private String getUniqueName(File jar) {
        String name = jar.getName();
        String suffix = "";
        if (name.lastIndexOf(".") > 0) {
            suffix = name.substring(name.lastIndexOf("."));
            name = name.substring(0, name.lastIndexOf("."));
        }
        String hexName = DigestUtils.md5Hex(jar.getAbsolutePath());
        return String.format("%s_%s%s", name, hexName, suffix);
    }
}

可以看到核心在doAsm函数里,这里使用了访问者设计模式,新建ClassVisitorAdapter如下:

public class ClassVisitorAdapter extends ClassVisitor {


    public ClassVisitorAdapter(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        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);
        return new MethodAdviceAdapter(api, methodVisitor, access, name, descriptor);
    }

}

里面有对Feild,Annotation,Method的访问,这里由于是对MainActivity的onCreate访问,所以只在visitMethod方法里返回MethodAdviceAdapter,这个类里面可以回调进入方法,离开方法的时机,内容如下:

public class MethodAdviceAdapter extends AdviceAdapter {


    private String methodName;
    private MethodVisitor methodVisitor;
    /**
     * Constructs a new {@link AdviceAdapter}.
     *
     * @param api
     * @param methodVisitor the method visitor to which this adapter delegates calls.
     * @param access        the method's access flags
     * @param name          the method's name.
     * @param descriptor    the method's descriptor
     */
    protected MethodAdviceAdapter(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
        super(api, methodVisitor, access, name, descriptor);
        this.methodName = name;
        this.methodVisitor = methodVisitor;
    }

    @Override
    protected void onMethodEnter() {
        enter();
    }

    private void enter() {
        if ("<init>".equals(methodName)) {
            return;
        }
        System.out.println("onMethodEnter");
        methodVisitor.visitLdcInsn("MainActivity");
        methodVisitor.visitLdcInsn("log from asm enter");
        methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        methodVisitor.visitInsn(POP);
    }

    @Override
    protected void onMethodExit(int opcode) {
        exit();
    }

    private void exit() {
        // 忽略构造方法
        if ("<init>".equals(methodName)) {
            return;
        }
        System.out.println("onMethodExit");
        methodVisitor.visitLdcInsn("MainActivity");
        methodVisitor.visitLdcInsn("log from asm exit");
        methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        methodVisitor.visitInsn(POP);
    }
}

enter和exit方法插入的代码,我们可以利用AS插件ASM ByteCode Viewer帮助我们生成,安装完成之后,在要插入代码所在的文件右键,执行ASM ByteCode Viewer命令,找到要插入的指令,复制过来即可


image.png

都准备好了,就可以打包这个插件,并配置到本地,看下publish.gradle:

apply plugin: 'maven-publish'
publishing {
    publications {
        mavenJava(MavenPublication) {
            groupId 'com.example'
            artifactId 'asm'
            version '1.0.0'
            from components.java
        }
    }
}
publishing {
    repositories {
        maven {
            // 发布位置
            url uri('../plugins')  
        }
    }
}

点击如下任务,发布插件到本地


image.png

DYJ8G(DA(3D5~$P_{~$5)6I.png

这里也可以使用

apply plugin: 'maven'
uploadArchives {
    repositories {
        mavenDeployer {
            pom.groupId = 'com.example'
            pom.artifactId = 'asm'
            pom.version = '1.0.0'
            //提交到远程服务器
            // repository(url:"服务器地址"){
            //    authentication(userName:'admin',password:'123456')
            // }
            //提交本地maven地址:../plugins
            repository(url: uri('../plugins'))
        }
    }
}

执行如下任务:


W6RU0CWOGYN939LEYKTIRIC.png

然后在工程的build.gradle依赖插件:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    
    repositories {
        google()
        jcenter()
        maven {
            url uri('./plugins')
        }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.3'
        classpath "com.example:asm:1.0.0"  // 根据上面的maven url查找对应的jar包
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven {
            url uri('./plugins')
        }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

之后在app module的build.gradle文件使用插件:

apply plugin: 'com.example.asm-plugin'  // 这里使用的是plugin-id

运行App,即可看到日志代码插入成功。
当然也可以在处理完成写入的目录查看文件是否插入代码:


.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。