美文网首页
2021-01-10 Android 插桩<2> (自定义Gra

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

作者: 猫KK | 来源:发表于2021-01-10 18:04 被阅读0次

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

    相关文章

      网友评论

          本文标题:2021-01-10 Android 插桩<2> (自定义Gra

          本文链接:https://www.haomeiwen.com/subject/fwflaktx.html