2021-01-10 Android 插桩<2> (自定义Gra

2021-01-10  本文已影响0人  猫KK

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()方法进行全方法插桩,检测性能。

上一篇下一篇

猜你喜欢

热点阅读