zoukankan      html  css  js  c++  java
  • ASM字节码插桩

    个人博客

    http://www.milovetingting.cn

    ASM字节码插桩

    前言

    热修复的多Dex加载方案中,对于5.0以下的系统存在CLASS_ISPREVERIFIED的问题,而解决这个问题的一个方案是:通过ASM插桩,在类的构造方法里引入一个其它dex里的类,从而避免被打上CLASS_ISPREVERIFIED标签。热修复可以参考其它资料或者前面写的一篇文章。本文主要介绍ASM插桩,主要参考 https://juejin.im/post/5c6eaa066fb9a049fc042048

    ASM框架

    ASM是一个可以分析和操作字节码的框架,通过它可以动态地修改字节码内容。使用ASM可以实现无埋点统计、性能监控等。

    什么是字节码插桩

    Android编译过程中,往字节码插入自定义的字节码。

    插桩时机

    Android打包要经过:java文件--class文件--dex文件,通过Gradle提供的Transform API,可以在编译成dex文件前,得到class文件,然后通过ASM修改字节码,即字节码插桩。

    实现

    下面通过自定义Gradle插件来处理class文件来实现插桩。

    自定义Gradle插件

    具体自定义Gradle插件的步骤,这里不再详细介绍,可以参考之前的一篇文章或者自行查阅其它资料。

    处理Class

    插件分为插件部分(src/main/groovy)、ASM部分(src/main/java)

    ASM插桩

    ASMPlugin类继承自Transform并实现Plugin接口,在apply的方法里注册,transform里回调并处理class。

    class ASMPlugin extends Transform implements Plugin<Project> {
    
        @Override
        void apply(Project project) {
            def android = project.extensions.getByType(AppExtension)
            android.registerTransform(this)
        }
    
        @Override
        String getName() {
            return "ASMPlugin"
        }
    
        @Override
        Set<QualifiedContent.ContentType> getInputTypes() {
            return TransformManager.CONTENT_CLASS
        }
    
        @Override
        Set<? super QualifiedContent.Scope> getScopes() {
            return TransformManager.SCOPE_FULL_PROJECT
        }
    
        @Override
        boolean isIncremental() {
            return false
        }
    
        @Override
        void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
            //处理class
        }
    }
    

    主要的逻辑处理都在transform方法里

    @Override
        void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
            println('--------------------ASMPlugin transform start--------------------')
            def startTime = System.currentTimeMillis()
            Collection<TransformInput> inputs = transformInvocation.inputs
            TransformOutputProvider outputProvider = transformInvocation.outputProvider
            //删除旧的输出
            if (outputProvider != null) {
                outputProvider.deleteAll()
            }
            //遍历inputs
            inputs.each { input ->
                //遍历directoryInputs
                input.directoryInputs.each {
                    directoryInput -> handleDirectoryInput(directoryInput, outputProvider)
                }
                //遍历jarInputs
                input.jarInputs.each {
                    jarInput -> handleJarInput(jarInput, outputProvider)
                }
            }
            def time = (System.currentTimeMillis() - startTime) / 1000
            println('-------------------- ASMPlugin transform end --------------------')
            println("ASMPlugin cost $time s")
        }
    

    在transform里处理class文件和jar文件

        /**
         * 处理目录下的class文件
         * @param directoryInput
         * @param outputProvider
         */
        static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
            //是否为目录
            if (directoryInput.file.isDirectory()) {
                //列出目录所有文件(包含子文件夹,子文件夹内文件)
                directoryInput.file.eachFileRecurse {
                    file ->
                        def name = file.name
                        if (isClassFile(name)) {
                            println("-------------------- handle class file:<$name> --------------------")
                            ClassReader classReader = new ClassReader(file.bytes)
                            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.parentFile.absolutePath + File.separator + name)
                            fileOutputStream.write(bytes)
                            fileOutputStream.close()
                        }
                }
            }
            def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
            FileUtils.copyDirectory(directoryInput.file, dest)
        }
    
        /**
         * 处理Jar中的class文件
         * @param jarInput
         * @param outputProvider
         */
        static void handleJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
            if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
                //重名名输出文件,因为可能同名,会覆盖
                def jarName = jarInput.name
                def md5Name = DigestUtils.md5Hex(jarInput.file.absolutePath)
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }
                JarFile jarFile = new JarFile(jarInput.file)
                Enumeration enumeration = jarFile.entries()
                File tempFile = new File(jarInput.file.parent + 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.name
                    ZipEntry zipEntry = new ZipEntry(entryName)
                    InputStream inputStream = jarFile.getInputStream(zipEntry)
                    if (isClassFile(entryName)) {
                        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()
                def dest = outputProvider.getContentLocation(jarName + "_" + md5Name, jarInput.contentTypes, jarInput.scopes, 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"))
        }
    

    在handleDirectoryInput和handleJarInput调用了我们自己定义在src/main/java里的ClassVisitor,

    class ActivityClassVisitor extends ClassVisitor implements Opcodes {
    
        private String mClassName;
    
        private static final String CLASS_NAME_ACTIVITY = "androidx/appcompat/app/AppCompatActivity";
    
        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;
            super.visit(version, access, name, signature, superName, interfaces);
        }
    
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature,
                                         String[] exceptions) {
            MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, 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;
        }
    }
    

    这里为简化操作,只处理了Activity的onCreate和onDestroy方法。在visitMethod方法里又调用了具体的MethodVisitor。如果对字节码不是特别了解的,可以通过在Android Studio中安装ASM Bytecode Outline插件来辅助。

    具体使用:

    安装完成ASM Bytecode Outline后,重启Android Studio,然后在相应的Java文件中右键,选择Show Bytecode outline

    ASM插桩2

    稍待一会后,会生成相应的字节码,在打开的面板中选择ASMified标签

    ASM插桩3

    public class ActivityOnCreateMethodVisitor extends MethodVisitor {
    
        public ActivityOnCreateMethodVisitor(int api, MethodVisitor mv) {
            super(api, mv);
        }
    
        @Override
        public void 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);
    
            super.visitCode();
        }
    
        @Override
        public void visitInsn(int opcode) {
            super.visitInsn(opcode);
        }
    }
    
    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);
        }
    }
    

    在visitCode和visitInsn方法里执行具体的操作。

    在处理Class过程中,可能会出现各种问题,可以通过调试插件来定位问题。可以参考上一篇文章来调试插件。

    引用插件

    在app模块引用插件,这里不再详细介绍,可以参考前面的文章

    将应用运行在手机上,打开后,可以看到日志输出:

    02-25 17:29:45.885 31237 31237 I ASMPlugin: -------------------- MainActivity onCreate --------------------
    02-25 17:29:50.646 31237 31237 I ASMPlugin: -------------------- MainActivity onDestroy --------------------
    

    结语

    这篇文章只是实现了简单的ASM插桩。可以查阅其它资料,了解更多关于字节码、ASM相关的内容。

    源码地址:https://github.com/milovetingting/Samples/tree/master/ASM

  • 相关阅读:
    B2B商城网站前端开发
    Scss开发临时学习过程||webpack、npm、gulp配置
    移动开发注意几点
    拥有的50个CSS代码片段(上)
    css3基础、(弹性、响应式)布局注意点
    js封装、简单实例源码记录
    ES8
    es7与es8
    Iterator
    Math,Number
  • 原文地址:https://www.cnblogs.com/milovetingting/p/12364333.html
Copyright © 2011-2022 走看看