精华内容
下载资源
问答
  • 一、" 字节码插桩 " 技术简介、 二、AspectJ 插桩工具、 三、ASM 插桩工具、





    一、" 字节码插桩 " 技术简介



    性能优化 , 插件化 , 热修复 , 等技术都需要用到 " 字节码插桩 " 相关技术 ;


    OOP 面向对象编程 , 主要针对业务逻辑进行开发 , 3 3 3 大特点 封装 , 继承 , 多态 ;

    AOP 面向切面编程 , 主要针对某个动作进行开发 ;

    • 如 : 给 Android 中所有的 Activity 添加安全策略 , 如果每个页面逐个手动添加 , 可能存在遗漏 , 并且会造成代码冗余 ; 通过 AOP 面向切面编程 , 完成上述操作 ;

    " 字节码插桩 " 技术应用 :

    • 代码生成 : 编译时生成代码 , 提高开发效率 , 减少手工工作量 , 降低出错概率 ;
    • 代码修改 : 为某些三方库添加崩溃 try catch 异常捕获机制 ;
    • 代码监控 : 编译时插桩 , 监控应用各种性能 , 如页面打开时间 , 页面停留时间 ; 友盟应该用了该技术 ;
    • 代码分析 : 使用编译时字节码插桩技术 , 自定义代码检查 ;

    字节码插桩原理 : 使用 javac 编译出 .class 字节码文件之后 , 使用 ASM 或 AspectJ 修改 .class 字节码文件 , 然后使用 dx 工具将修改后的 .class 字节码文件打包到 .dex 文件中 ;

    在这里插入图片描述





    二、AspectJ 插桩工具



    AspectJ 插桩工具 :

    使用简单 : 使用 AspectJ 插桩工具修改字节码文件 , 不需要了解 .class 字节码文件的二进制格式 ;

    成熟稳定 : 字节码操作 如果错了 1 1 1 个字节 , 整个字节码就无法正常工作 , 因此修改字节码操作 , 必须稳定 , 这也是插桩工具的必备条件 ;

    固定切入点 : AspectJ 只能在固定的几个切入点插入 , 如 : 方法调用前 , 方法内部 , 异常前后 , 变量修改 ; 不能完成很细致的操作 , 如将某些特定规则的字节码序列作为切入点 ;

    匹配规则 : AspectJ 的匹配规则类似于正则表达式 , 如 : 匹配 onXXX 方法 , 会匹配到 onCreate 方法 , 也会匹配到 onDestroy 方法 ;

    性能低 : AspectJ 插入逻辑时 , 会添加一些额外冗余代码 , 生成的字节码肯定大于之前的字节码文件 , 对原来的性能也有一定影响 , 修改后的字节码文件 性能低于 修改前的字节码文件 ;





    三、ASM 插桩工具



    ASM 插桩工具 :

    操作灵活 : 可以在字节码 任何位置 , 自定义修改 , 插入 , 删除 相关逻辑 ;

    上手很难 : 使用 ASM 的前提是必须 对 Java 的 .class 字节码文件有比较深入的了解 ;

    展开全文
  • Android字节码插桩

    千次阅读 2019-12-11 18:49:20
    什么是字节码插桩 字节码插桩就是在构建的过程中,通过修改已经编译完成的字节码文件,也就是class文件,来实现功能的添加。 简单来讲,我们要实现无埋点对客户端的全量统计。这里的统计概括的范围比较广泛,常见的...

    什么是字节码插桩

    字节码插桩就是在构建的过程中,通过修改已经编译完成的字节码文件,也就是class文件,来实现功能的添加。

    简单来讲,我们要实现无埋点对客户端的全量统计。这里的统计概括的范围比较广泛,常见的场景有:

    • 页面(Activity、Fragment)的打开事件
    • 各种点击事件的统计,包括但不限于Click LongClick TouchEvent
    • Debug期需要统计各个方法的耗时。注意这里的方法包括接入的第三方SDK的方法。
    • 待补充

    要实现这些功能需要拥有哪些技术点呢?

    • 面向切面编程思想(AOP)
    • Android打包流程
    • 自定义Gradle插件
    • Java字节码
    • 字节码编织(ASM)
    • 结合自己的业务实现统计代码

    面向切面编程思想(AOP)

    AOP(Aspect Oriented Program)是一种面向切面编程的思想。这种编程思想是相对于OOP(ObjectOriented Programming)来说的。说破天,咱们要实现的功能还是统计嘛,大规模的重复统计行为是典型的AOP使用场景。所以搞懂什么是AOP以及为什么要用AOP变得很重要。

    先来说一下大家熟悉的面向对象编程:面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。

    但是面向对象的编程天生有个缺点就是分散代码的同时,也增加了代码的重复性。比如我希望在项目里面所有的模块都增加日志统计模块,按照OOP的思想,我们需要在各个模块里面都添加统计代码,但是如果按照AOP的思想,可以将统计的地方抽象成切面,只需要在切面里面添加统计代码就OK了。

    在这里插入图片描述

    其实在服务端的领域AOP已经被各路大佬玩的风生水起,例如Spring这类跨时代的框架。我第一次接触AOP就是在学习Spring框架的的时候。最常见实现AOP的方式就是代理。

    AOP 是一种编程思想,但是它的实现方式有很多,比如:Spring、AspectJ、JavaAssist、ASM 等。由于我是做 Android 开发的,所以会用 Android 中的一些例子。

    • JakeWharton 的 hugo 就是一个典型的应用,其利用了自定义 Gradle 插件 + AspectJ 的方式,将有特定注解的方法的参数、返回结果和执行时间打印到 Logcat 中,方便开发调试。
    • 最近在学习 Java 字节码和 ASM 方面的知识,所以也照猫画虎,写了一个TraceLog,实现了和 hugo同样的功能,将特定注解的方法的参数、返回结果和执行时间打印到 Logcat 中,方便开发调试,不过我使用的是 自定义 Gradle 插件 + ASM 的方式。后面会讲。

    Android打包流程

    详见 android Apk打包过程概述

    自定义Gradle插件

    详见 Gradle自定义插件

    如何使用Transform API

    因为是编译期间搞事情,所以首先要在编译期间找一个时间点,这也就是本节 Transform 的内容,找到“作案”地点后,接下来就是“作案对象”了,这里选择的是对编译后的 .class 字节码下手,要用到的工具就是后面要介绍的 ASM 了。
    在这里插入图片描述
    上面是官方出品的编译打包签名流程,我们要搞事情的位置就是 Java Compiler 编译成 .class Files 之到打包为 .dex Files 这之间。Google 官方在 Android Gradle 的 1.5.0 版本以后提供了 Transfrom API, 允许第三方自定义插件在打包 dex 文件之前的编译过程中操作 .class 文件,所以这里先要做的就是实现一个自定义的 Transform 进行.class文件遍历拿到所有方法,修改完成对原文件进行替换。

    下面说一下如何引入 Transform 依赖,在 Android gradle 插件 1.5 版本以前,是有一个单独的 transform api 的;从 2.0 版本开始,就直接并入到 gradle api 中了。

    Gradle 1.5:

    Compile ‘com.android.tools.build:transfrom-api:1.5.0’
    

    Gradle 2.0 开始:

    implementation 'com.android.tools.build:gradle:3.5.2'
    

    Transform是作用在.class编译后,打包成.dex前,可以对.class和resource进行再处理的部分。为了验证,我们建立一个项目Build的一次。
    在这里插入图片描述
    可以很清楚的看到,原生就带了一系列Transform供使用。那么这些Transform是怎么组织在一起的呢,我们用一张图表示:
    在这里插入图片描述
    每个Transform其实都是一个gradle task,Android编译器中的TaskManager将每个Transform串连起来,第一个Transform接收来自javac编译的结果,以及已经拉取到在本地的第三方依赖(jar. aar),还有resource资源,注意,这里的resource并非android项目中的res资源,而是asset目录下的资源。 这些编译的中间产物,在Transform组成的链条上流动,每个Transform节点可以对class进行处理再传递给下一个Transform。我们常见的混淆,Desugar等逻辑,它们的实现如今都是封装在一个个Transform中,而我们自定义的Transform,会插入到这个Transform链条的最前面。

    但其实,上面这幅图,只是展示Transform的其中一种情况。而Transform其实可以有两种输入,一种是消费型的,当前Transform需要将消费型型输出给下一个Transform,另一种是引用型的,当前Transform可以读取这些输入,而不需要输出给下一个Transform,比如Instant Run就是通过这种方式,检查两次编译之间的diff的。

    Transform解读

    class TraceTransform extends Transform {
    
        @Override
        String getName() {
            return "TraceLog"    }
    
        @Override
        Set<QualifiedContent.ContentType> getInputTypes() {
            return TransformManager.CONTENT_CLASS    }
    
        @Override
        Set<? super QualifiedContent.Scope> getScopes() {
            return TransformManager.SCOPE_FULL_PROJECT    }
    
        @Override
        boolean isIncremental() {
            return true
        }
    
        @Override
        void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
            super.transform(transformInvocation)
            ......
        }
    

    我们一项项分析:

    (1)

    @Override
        String getName() {
            return "TraceLog"    }
    

    Name顾名思义,就是我们的Transform名称,再回到我们刚刚Build的流程里:
    在这里插入图片描述
    这个最终的名字是如何构成的呢?好像跟我们这边的定义的名字有区别。以transform开头,之后拼接ContentType,这个ContentType代表着这个Transform的输入文件的类型,类型主要有两种,一种是Classes,另一种是Resources,ContentType之间使用And连接,拼接完成后加上With,之后紧跟的就是这个Transform的Name,name在getName()方法中重写返回即可。

    (2)

    @Override
        Set<QualifiedContent.ContentType> getInputTypes() {
            return TransformManager.CONTENT_CLASS    }
    

    先来看代码注释,注释写的很清晰了,必须是CLASSES(0x01),RESOURCES(0x02)之一,相当于Transform需要处理的类型。

     /**
         * Returns the type(s) of data that is consumed by the Transform. This may be more than
         * one type.
         *
         * <strong>This must be of type {@link QualifiedContent.DefaultContentType}</strong>
         */
        @NonNull
        public abstract Set<ContentType> getInputTypes();
        
        ----------------------------------
        
         /**
         * The type of of the content.
         */
        enum DefaultContentType implements ContentType {
            /**
             * The content is compiled Java code. This can be in a Jar file or in a folder. If
             * in a folder, it is expected to in sub-folders matching package names.
             */
            CLASSES(0x01),
    
            /** The content is standard Java resources. */
            RESOURCES(0x02);
    
            private final int value;
    
            DefaultContentType(int value) {
                this.value = value;
            }
    
            @Override
            public int getValue() {
                return value;
            }
        }
    

    (3)

    @Override
        Set<? super QualifiedContent.Scope> getScopes() {
            return TransformManager.SCOPE_FULL_PROJECT    }
    

    先来看源码注释,这个的作用相当于用来Transform表明作用域

     /**
         * Returns the scope(s) of the Transform. This indicates which scopes the transform consumes.
         */
        @NonNull
        public abstract Set<Scope> getScopes();
    开发一共可以选如下几种:
    
     /**
         * The scope of the content.
         *
         * <p>
         * This indicates what the content represents, so that Transforms can apply to only part(s)
         * of the classes or resources that the build manipulates.
         */
        enum Scope implements ScopeType {
            /** Only the project (module) content */
            PROJECT(0x01),
            /** Only the sub-projects (other modules) */
            SUB_PROJECTS(0x04),
            /** Only the external libraries */
            EXTERNAL_LIBRARIES(0x10),
            /** Code that is being tested by the current variant, including dependencies */
            TESTED_CODE(0x20),
            /** Local or remote dependencies that are provided-only */
            PROVIDED_ONLY(0x40),
    
            /**
             * Only the project's local dependencies (local jars)
             *
             * @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}
             */
            @Deprecated        PROJECT_LOCAL_DEPS(0x02),
            /**
             * Only the sub-projects's local dependencies (local jars).
             *
             * @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}
             */
            @Deprecated        SUB_PROJECTS_LOCAL_DEPS(0x08);
    

    一般来说如果是要处理所有class字节码,Scope我们一般使用TransformManager.SCOPE_FULL_PROJECT。即

    public static final Set<Scope> SCOPE_FULL_PROJECT =
                Sets.immutableEnumSet(
                        Scope.PROJECT,
                        Scope.SUB_PROJECTS,
                        Scope.EXTERNAL_LIBRARIES);
    

    (4)

    @Override
    boolean isIncremental() {
        return true
    }
    

    增量编译开关。当我们开启增量编译的时候,相当input包含了changed/removed/added三种状态,实际上还有notchanged。需要做的操作如下:

    • NOTCHANGED: 当前文件不需处理,甚至复制操作都不用;
    • ADDED、CHANGED: 正常处理,输出给下一个任务;
    • REMOVED: 移除outputProvider获取路径对应的文件。

    (5)

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        ......
    }
    

    先来看一下源码注释,它是Transform处理文件的核心代码:

     /**
         * Executes the Transform.
         *
         * <p>The inputs are packaged as an instance of {@link TransformInvocation}
         * <ul>
         *     <li>The <var>inputs</var> collection of {@link TransformInput}. These are the inputs
         *     that are consumed by this Transform. A transformed version of these inputs must
         *     be written into the output. What is received is controlled through
         *     {@link #getInputTypes()}, and {@link #getScopes()}.</li>
         *     <li>The <var>referencedInputs</var> collection of {@link TransformInput}. This is
         *     for reference only and should be not be transformed. What is received is controlled
         *     through {@link #getReferencedScopes()}.</li>
         * </ul>
         *
         * A transform that does not want to consume anything but instead just wants to see the content
         * of some inputs should return an empty set in {@link #getScopes()}, and what it wants to
         * see in {@link #getReferencedScopes()}.
         *
         * <p>Even though a transform's {@link Transform#isIncremental()} returns true, this method may
         * be receive <code>false</code> in <var>isIncremental</var>. This can be due to
         * <ul>
         *     <li>a change in secondary files ({@link #getSecondaryFiles()},
         *     {@link #getSecondaryFileOutputs()}, {@link #getSecondaryDirectoryOutputs()})</li>
         *     <li>a change to a non file input ({@link #getParameterInputs()})</li>
         *     <li>an unexpected change to the output files/directories. This should not happen unless
         *     tasks are improperly configured and clobber each other's output.</li>
         *     <li>a file deletion that the transform mechanism could not match to a previous input.
         *     This should not happen in most case, except in some cases where dependencies have
         *     changed.</li>
         * </ul>
         * In such an event, when <var>isIncremental</var> is false, the inputs will not have any
         * incremental change information:
         * <ul>
         *     <li>{@link JarInput#getStatus()} will return {@link Status#NOTCHANGED} even though
         *     the file may be added/changed.</li>
         *     <li>{@link DirectoryInput#getChangedFiles()} will return an empty map even though
         *     some files may be added/changed.</li>
         * </ul>
         *
         * @param transformInvocation the invocation object containing the transform inputs.
         * @throws IOException if an IO error occurs.
         * @throws InterruptedException
         * @throws TransformException Generic exception encapsulating the cause.
         */
        public void transform(@NonNull TransformInvocation transformInvocation)
                throws TransformException, InterruptedException, IOException {
            // Just delegate to old method, for code that uses the old API.
            //noinspection deprecation
            transform(transformInvocation.getContext(), transformInvocation.getInputs(),
                    transformInvocation.getReferencedInputs(),
                    transformInvocation.getOutputProvider(),
                    transformInvocation.isIncremental());
        }
    

    大致意思如下,具体大家一定要仔细看注释:

    • 如果拿取了getInputs()的输入进行消费,则transform后必须再输出给下一级
    • 如果拿取了getReferencedInputs()的输入,则不应该被transform。
    • 是否增量编译要以transformInvocation.isIncremental()为准。

    在 transform 方法中主要做的事情就是把 Inputs 保存到 outProvider 提供的位置去。生成的位置见下图:
    在这里插入图片描述
    后面会讲到代码,主要有两个 transform 方法,一个 transformJar 就是简单的拷贝,另一个 transformDirectory,我们就是在这里用 ASM 对字节码进行修改的。

    Transform注册和使用

    在gradle插件中注册

    class TracePlugin implements Plugin<Project>{
    
        @Override
        void apply(Project project) {
            println "------trace plugin begin-------"
            def android = project.extensions.findByType(AppExtension.class)
            android.registerTransform(new TraceTransform(project))
            println "------trace plugin end-------"
        }
    
    }
    

    参考 Transform详解

    Java字节码

    详见 《深入理解Java虚拟机》第6章 类文件结构

    Java字节码编织框架——ASM

    什么是ASM?

    ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

    为什么选择ASM来进行字节码编织?

    有前人做了实验。参考网易乐得团队的实验结果:
    在这里插入图片描述

    通过上表可见,ASM的效率更高。不过效率高的前提是该库的语法更接近字节码层面。所以上面的虚拟机相关知识显得更加重要。

    ASM 库是一款基于 Java 字节码层面的代码分析和修改工具。ASM 可以直接生产二进制的 class 文件,也可以在类被加载入 JVM 之前动态修改类行为。
    ASM 库的结构如下所示:
    在这里插入图片描述

    • Core:为其他包提供基础的读、写、转化Java字节码和定义的API,并且可以生成Java字节码和实现大部分字节码的转换,在 访问者模式和
      ASM 中介绍的几个重要的类就在 Core API 中:ClassReader、ClassVisitor 和 ClassWriter 类.
    • Tree:提供了 Java 字节码在内存中的表现
    • Commons:提供了一些常用的简化字节码生成、转换的类和适配器
    • Util:包含一些帮助类和简单的字节码修改类,有利于在开发或者测试中使用
    • XML:提供一个适配器将XML和SAX-comliant转化成字节码结构,可以允许使用XSLT去定义字节码转化

    Core API 介绍

    (1)ClassVisitor 抽象类

    如下所示,在 ClassVisitor 中提供了和类结构同名的一些方法,这些方法会对类中相应的部分进行操作,而且是有顺序的:visit [ visitSource ] [ visitOuterClass ] ( visitAnnotation | visitAttribute )* (visitInnerClass | visitField | visitMethod )* visitEnd
    public abstract class ClassVisitor {

        ......
    
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces);
    public void visitSource(String source, String debug);
    public void visitOuterClass(String owner, String name, String desc);
    public AnnotationVisitor visitAnnotation(String desc, boolean visible);
    public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible);
    public void visitAttribute(Attribute attr);
    public void visitInnerClass(String name, String outerName, String innerName, int access);
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value);
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions);
    public void visitEnd();}
    
    • void visit(int version, int access, String name, String signature, String superName, String[] interfaces) 该方法是当扫描类时第一个调用的方法,主要用于类声明使用。下面是对方法中各个参数的示意:visit( 类版本 , 修饰符 , 类名 ,泛型信息 , 继承的父类 , 实现的接口)
    • AnnotationVisitor visitAnnotation(String desc, boolean visible) 该方法是当扫描器扫描到类注解声明时进行调用。下面是对方法中各个参数的示意:visitAnnotation(注解类型 , 注解是否可以在JVM 中可见)。
    • FieldVisitor visitField(int access, String name, String desc, String signature, Object value) 该方法是当扫描器扫描到类中字段时进行调用。下面是对方法中各个参数的示意:visitField(修饰符 , 字段名 , 字段类型 , 泛型描述 , 默认值)
    • MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) 该方法是当扫描器扫描到类的方法时进行调用。下面是对方法中各个参数的示意:visitMethod(修饰符 , 方法名 , 方法签名 ,泛型信息 , 抛出的异常)
    • void visitEnd() 该方法是当扫描器完成类扫描时才会调用,如果想在类中追加某些方法

    (2)ClassReader 类

    这个类会将 .class 文件读入到 ClassReader 中的字节数组中,它的 accept 方法接受一个 ClassVisitor 实现类,并按照顺序调用 ClassVisitor 中的方法。

    (3)ClassWriter 类

    ClassWriter 是一个 ClassVisitor 的子类,是和 ClassReader 对应的类,ClassReader 是将 .class 文件读入到一个字节数组中,ClassWriter 是将修改后的类的字节码内容以字节数组的形式输出。

    (4)MethodVisitor & AdviceAdapter

    MethodVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Method 时就转入 MethodVisitor 接口处理。
    AdviceAdapter 是 MethodVisitor 的子类,使用 AdviceAdapter 可以更方便的修改方法的字节码。其中比较重要的几个方法如下:

    • void visitCode():表示 ASM 开始扫描这个方法
    • void onMethodEnter():进入这个方法
    • void onMethodExit():即将从这个方法出去
    • void onVisitEnd():表示方法扫码完毕

    (5)FieldVisitor 抽象类

    FieldVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Field 时就转入 FieldVisitor 接口处理。和分析 MethodVisitor 的方法一样,也可以查看源码注释进行学习,这里不再详细介绍。

    操作流程

    1. 需要创建一个 ClassReader 对象,将 .class 文件的内容读入到一个字节数组中
    2. 然后需要一个 ClassWriter 的对象将操作之后的字节码的字节数组回写
    3. 需要事件过滤器 ClassVisitor。在调用 ClassVisitor 的某些方法时会产生一个新的 XXXVisitor
      对象,当我们需要修改对应的内容时只要实现自己的 XXXVisitor 并返回就可以了
    input.directoryInputs.each { DirectoryInput directoryInput ->
        if (directoryInput.file.isDirectory()) {
            directoryInput.file.eachFileRecurse { File file ->
                def name = file.name
                if (name.endsWith(".class") && !(name == ("R.class"))
                        && !name.startsWith("R\$") && !(name == ("BuildConfig.class"))) {
    
                    ClassReader reader = new ClassReader(file.bytes)
                    ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
                    ClassVisitor visitor = new TraceVisitor(writer)
                    reader.accept(visitor, ClassReader.EXPAND_FRAMES)
    
                    byte[] code = writer.toByteArray()
                    def classPath = file.parentFile.absolutePath + File.separator + name
                    FileOutputStream fos = new FileOutputStream(classPath)
                    fos.write(code)
                    fos.close()
                }
            }
        }
    

    这个库也没什么可展开描述的,值得参考的资源:
    AOP 的利器:ASM 3.0 介绍
    ASM 库的介绍和使用

    虽然有了ASM这种框架,可以很方便的修改class文件,但是如果不熟悉框架的使用,写起来还是有点吃力
    人类总是懒惰的,试图找出一些捷径,于是有了一款Idea插件——ASM Bytecode Outline

    ASM Bytecode Outline

    详见 【我的Android进阶之旅】Android Studio 使用 ASM Bytecode Outline 插件来研究Java字节码
    插件ASM Bytecode Outline,可以把java代码转为ASM框架的代码,那么我们可以先修改好一个类的代码,把代码转为ASM框架的代码,然后把需要的代码复制到插件中,这样就可以在自定义的gradle plugin中批量自动去修改目标类了。

    TraceLog

    使用自定义 Gradle 插件 + ASM 的方式实现了和 JakeWharton 的 hugo 库同样的功能的库,将特定注解的方法的传入参数、返回结果和执行时间打印到 Logcat 中,方便开发调试。
    在这里插入图片描述

    整个工程分3个模块,主模块是调用方,就是使用@TraceLog的地方。plugin模块是自定义的gradle插件。因为打印日志和业务无关性,这里把打印日志的功能单独拆分成一个模块tracelibrary。plugin模块依赖traceLibrary,在字节码插桩时调用traceLibrary里面的方法打印日志。

    自定义gradle plugin

    build.gradle

    apply plugin: 'groovy'
    //使用该插件,才能使用uploadArchives
    apply plugin: 'maven'
    
    repositories {
        jcenter()
    }
    
    dependencies {
        //使用gradle sdk
        compile gradleApi()
        //使用groovy sdk
        compile localGroovy()
        implementation 'com.android.tools.build:gradle:3.5.2'
        implementation 'org.ow2.asm:asm-all:5.2'
    }
    
    sourceCompatibility = "1.8"
    targetCompatibility = "1.8"
    
    uploadArchives {
        repositories.mavenDeployer {
            pom.version = '1.0.0'
            pom.artifactId = 'tracePlugin'
            pom.groupId = 'com.example.watson.plugin'
            repository(url: "file:///D:/repository/")
        }
    }
    

    TracePlugin.groovy

    class TracePlugin implements Plugin<Project>{
    
        @Override
        void apply(Project project) {
            println "------trace plugin begin-------"
            def android = project.extensions.findByType(AppExtension.class)
            android.registerTransform(new TraceTransform(project))
            println "------trace plugin end-------"
        }
    
    }
    

    TraceTransform.groovy

    class TraceTransform extends Transform {
    
        Project project
    
        TraceTransform(Project project) {
            this.project = project
        }
    
        @Override
        String getName() {
            return "TraceLog"
        }
    
        @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 {
            transformInvocation.inputs.each { TransformInput input ->
                input.directoryInputs.each { DirectoryInput directoryInput ->
                    if (directoryInput.file.isDirectory()) {
                        directoryInput.file.eachFileRecurse { File file ->
                            def name = file.name
                            if (name.endsWith(".class") && !(name == ("R.class"))
                                    && !name.startsWith("R\$") && !(name == ("BuildConfig.class"))) {
    
                                ClassReader reader = new ClassReader(file.bytes)
                                ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
                                ClassVisitor visitor = new TraceVisitor(writer)
                                reader.accept(visitor, ClassReader.EXPAND_FRAMES)
    
                                byte[] code = writer.toByteArray()
                                def classPath = file.parentFile.absolutePath + File.separator + name
                                FileOutputStream fos = new FileOutputStream(classPath)
                                fos.write(code)
                                fos.close()
                            }
                        }
                    }
    
                    def dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name,
                            directoryInput.contentTypes, directoryInput.scopes,
                            Format.DIRECTORY)
    
    
                    FileUtils.copyDirectory(directoryInput.file, dest)
                }
    
                input.jarInputs.each { JarInput jarInput ->
                    def jarName = jarInput.name
                    def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                    if (jarName.endsWith(".jar")) {
                        jarName = jarName.substring(0, jarName.length() - 4)
                    }
    
                    def dest = transformInvocation.outputProvider.getContentLocation(jarName + md5Name,
                            jarInput.contentTypes, jarInput.scopes, Format.JAR)
    
                    FileUtils.copyFile(jarInput.file, dest)
                }
            }
        }
    }
    

    TraceVisitor.groovy

    class TraceVisitor extends ClassVisitor {
    
        private String mClassName
    
        TraceVisitor(ClassVisitor classVisitor) {
            super(Opcodes.ASM5, classVisitor)
        }
    
        @Override
        void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            super.visit(version, access, name, signature, superName, interfaces);
            this.mClassName = name
        }
    
        @Override
        MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions)
            methodVisitor = new TraceMethodVisitor(Opcodes.ASM5, methodVisitor, access, mClassName, name, desc)
            return methodVisitor
        }
    }
    

    TraceMethodVisitor.groovy

    class TraceMethodVisitor extends AdviceAdapter {
    
        private static final String COST_ANNOTATION_DESC = "Lcom/example/tracelibrary/TraceLog;"
    
        private boolean isInjected = false
    
        private int startTimeId
    
        private int methodId
    
        private String className
    
        private String methodName
    
        private String desc
    
        private boolean isStaticMethod
    
        private Type[] argumentArrays
    
        TraceMethodVisitor(int api, MethodVisitor mv, int access, String className, String methodName, String desc) {
            super(api, mv, access, methodName, desc)
            this.className = className
            this.methodName = methodName
            this.desc = desc
            argumentArrays = Type.getArgumentTypes(desc)
            isStaticMethod = ((access & Opcodes.ACC_STATIC) != 0)
        }
    
        @Override
        AnnotationVisitor visitAnnotation(String desc, boolean visible) {
            if (COST_ANNOTATION_DESC.equals(desc)) {
                isInjected = true
            }
            return super.visitAnnotation(desc, visible)
        }
    
        @Override
        protected void onMethodEnter() {
            if (isInjected) {
                methodId = newLocal(Type.INT_TYPE)
                mv.visitMethodInsn(INVOKESTATIC, "com/example/tracelibrary/core/MethodCache", "request", "()I", false)
                mv.visitIntInsn(ISTORE, methodId)
    
                for (int i = 0; i < argumentArrays.length; i++) {
                    Type type = argumentArrays[i]
                    int index = isStaticMethod ? i : (i + 1)
                    switch (type.getSort()) {
                        case Type.BOOLEAN:
                        case Type.CHAR:
                        case Type.BYTE:
                        case Type.SHORT:
                        case Type.INT:
                            mv.visitVarInsn(ILOAD, index)
                            box(type)
                            break
                        case Type.FLOAT:
                            mv.visitVarInsn(FLOAD, index)
                            box(type)
                            break
                        case Type.LONG:
                            mv.visitVarInsn(LLOAD, index)
                            box(type)
                            break
                        case Type.DOUBLE:
                            mv.visitVarInsn(DLOAD, index)
                            box(type)
                            break
                        case Type.ARRAY:
                        case Type.OBJECT:
                            mv.visitVarInsn(ALOAD, index)
                            box(type)
                            break
                    }
                    mv.visitVarInsn(ILOAD, methodId)
                    visitMethodInsn(INVOKESTATIC, "com/example/tracelibrary/core/MethodCache", "addMethodArgument",
                            "(Ljava/lang/Object;I)V", false)
                }
    
                startTimeId = newLocal(Type.LONG_TYPE)
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
                mv.visitIntInsn(LSTORE, startTimeId)
            }
        }
    
        @Override
        protected void onMethodExit(int opcode) {
            if (isInjected) {
                if (opcode == RETURN) {
                    visitInsn(ACONST_NULL)
                } else if (opcode == ARETURN || opcode == ATHROW) {
                    dup()
                } else {
                    if (opcode == LRETURN || opcode == DRETURN) {
                        dup2()
                    } else {
                        dup()
                    }
                    box(Type.getReturnType(this.methodDesc))
                }
                mv.visitLdcInsn(className)
                mv.visitLdcInsn(methodName)
                mv.visitLdcInsn(desc)
                mv.visitVarInsn(LLOAD, startTimeId)
                mv.visitVarInsn(ILOAD, methodId)
                mv.visitMethodInsn(INVOKESTATIC, "com/example/tracelibrary/core/MethodCache", "updateMethodInfo",
                        "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JI)V", false)
    
                mv.visitVarInsn(ILOAD, methodId)
                mv.visitMethodInsn(INVOKESTATIC, "com/example/tracelibrary/core/MethodCache",
                        "printMethodInfo", "(I)V", false)
            }
        }
    }
    

    第三方库文件

    看到,自定义gradle插件的TraceMethodVisitor会在方法执行前后织入需要的功能,这些功能就是第三方库的内容。

    build.gradle

    apply plugin: 'com.android.library'
    //使用该插件,才能使用uploadArchives
    apply plugin: 'maven'
    
    android {
        compileSdkVersion 29
        buildToolsVersion "29.0.2"
    
    
        defaultConfig {
            minSdkVersion 15
            targetSdkVersion 29
            versionCode 1
            versionName "1.0"
    
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
            consumerProguardFiles 'consumer-rules.pro'
        }
    
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
        }
    
    }
    
    dependencies {
        compile 'org.ow2.asm:asm-all:5.2'
    }
    
    uploadArchives {
        repositories.mavenDeployer {
            pom.version = '1.0.0'
            pom.artifactId = 'traceLibrary'
            pom.groupId = 'com.example.watson.library'
            repository(url: "file:///D:/repository/")
        }
    }
    

    MethodCache.java

    public class MethodCache {
    
        /**
         * 方法缓存默认大小
         */
        private static final int INIT_CACHE_SIZE = 1024;
        /**
         * 方法名缓存
         */
        private static Vector<MethodInfo> mCacheMethods = new Vector<>(INIT_CACHE_SIZE);
    
        /**
         * 占位并生成方法ID
         *
         * @return 返回 方法 Id
         */
        public static int request() {
            mCacheMethods.add(new MethodInfo());
            return mCacheMethods.size() - 1;
        }
    
        public static void addMethodArgument(Object argument, int id) {
            MethodInfo methodInfo = mCacheMethods.get(id);
            methodInfo.addArgument(argument);
        }
    
        public static void updateMethodInfo(Object result, String className, String methodName, String methodDesc, long startTime, int id) {
            MethodInfo methodInfo = mCacheMethods.get(id);
            methodInfo.setCost((System.currentTimeMillis() - startTime));
            methodInfo.setResult(result);
            methodInfo.setMethodDesc(methodDesc);
            methodInfo.setClassName(className);
            methodInfo.setMethodName(methodName);
        }
    
        public static void printMethodInfo(int id) {
            MethodInfo methodInfo = mCacheMethods.get(id);
            Printer.printMethodInfo(methodInfo);
        }
    }
    

    MethodInfo.java

    public class MethodInfo {
    
        private static final String OUTPUT_FORMAT = "The method's name is %s ,the cost is %dms and the result is ";
    
        private String mClassName;              // 类名
        private String mMethodName;             // 方法名
        private String mMethodDesc;             // 方法描述符
        private Object mResult;                 // 方法执行结果
        private long mCost;                     // 方法执行耗时
        private List<Object> mArgumentList;     // 方法参数列表
    
        MethodInfo() {
            mArgumentList = new ArrayList<>();
        }
    
        @Override
        public String toString() {
            return String.format(Locale.CHINA, OUTPUT_FORMAT, getMethodName(), mCost) + mResult;
        }
    
        /**
         * @param className 设置类名
         */
        public void setClassName(String className) {
            mClassName = className;
        }
    
        /**
         * @return 返回类名
         */
        public String getClassName() {
            mClassName = mClassName.replace("/", ".");
            return mClassName;
        }
    
        /**
         * @param methodName 设置方法名
         */
        public void setMethodName(String methodName) {
            mMethodName = methodName;
        }
    
        /**
         * @return 返回方法名
         */
        public String getMethodName() {
            StringBuilder msg = new StringBuilder();
            Type[] argumentTypes = Type.getArgumentTypes(mMethodDesc);
            msg.append('(');
            for (int i = 0; i < argumentTypes.length; i++) {
                msg.append(argumentTypes[i].getClassName());
                if (i != argumentTypes.length - 1) {
                    msg.append(", ");
                }
            }
            msg.append(')');
            mMethodName = mMethodName + msg.toString();
            return mMethodName;
        }
    
        /**
         * @param cost 设置方法执行耗时
         */
        public void setCost(long cost) {
            this.mCost = cost;
        }
    
        /**
         * @return 返回方法执行耗时
         */
        public long getCost() {
            return mCost;
        }
    
        /**
         * @param result 设置方法执行结果
         */
        public void setResult(Object result) {
            this.mResult = result;
        }
    
        /**
         * @return 返回方法执行结果
         */
        public Object getResult() {
            return mResult;
        }
    
        /**
         * @param methodDesc 设置方法描述符
         */
        public void setMethodDesc(String methodDesc) {
            this.mMethodDesc = methodDesc;
        }
    
        /**
         * 添加方法参数
         *
         * @param argument 方法参数
         */
        public void addArgument(Object argument) {
            mArgumentList.add(argument);
        }
    
        /**
         * @return 得到方法参数列表
         */
        public List<Object> getArgumentList() {
            return mArgumentList;
        }
    }
    

    Printer.java

    public class Printer {
        private static final String TAG = "TraceLog";
    
        private static final char TOP_LEFT_CORNER = '┌';
        private static final char BOTTOM_LEFT_CORNER = '└';
        private static final char HORIZONTAL_LINE = '│';
        private static final String DOUBLE_DIVIDER = "───────────────────────────────────------";
        private static final String TOP_BORDER = TOP_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER;
        private static final String BOTTOM_BORDER = BOTTOM_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER;
    
        private static final String CLASS_NAME_FORMAT = "%s The class's name: %s";
        private static final String METHOD_NAME_FORMAT = "%s The method's name: %s";
        private static final String ARGUMENT_FORMAT = "%s The arguments: ";
        private static final String RESULT_FORMAT = "%s The result: ";
        private static final String COST_TIME_FORMAT = "%s The cost time: %dms";
    
        public static void printMethodInfo(MethodInfo methodInfo) {
            Log.i(String.valueOf(0) + TAG, TOP_BORDER);
            Log.i(String.valueOf(1) + TAG, String.format(CLASS_NAME_FORMAT, HORIZONTAL_LINE, methodInfo.getClassName()));
            Log.i(String.valueOf(2) + TAG, String.format(METHOD_NAME_FORMAT, HORIZONTAL_LINE, methodInfo.getMethodName()));
            Log.i(String.valueOf(3) + TAG, String.format(ARGUMENT_FORMAT, HORIZONTAL_LINE) + methodInfo.getArgumentList());
            Log.i(String.valueOf(4) + TAG, String.format(RESULT_FORMAT, HORIZONTAL_LINE) + methodInfo.getResult());
            Log.i(String.valueOf(5) + TAG, String.format(Locale.CHINA, COST_TIME_FORMAT, HORIZONTAL_LINE, methodInfo.getCost()));
            Log.i(String.valueOf(6) + TAG, BOTTOM_BORDER);
        }
    }
    

    最后是注解的定义:

    @Target(ElementType.METHOD)
    public @interface TraceLog {
    }
    

    主Module

    主Module是使用方,使用方式:

    (1)项目工程的gradle.build添加gradle编译脚本依赖:

    buildscript {
        repositories {
            maven {
                url uri('D:/repository')
            }
        }
        dependencies {
            classpath 'com.example.watson.plugin:tracePlugin:1.0.0'
        }
    }
    

    这里我使用的仓库是本地文件夹,以后可以上传服务器,做到远程依赖

    (2)在需要使用的 module 中的 build.gradle 中应用插件:

    apply plugin: com.example.watson.plugin.TracePlugin
    

    同时添加第三方库依赖,这里同样使用的仓库是本地文件夹,以后可以上传服务器,做到远程依赖

    repositories {
        maven {
            url uri('D:/repository')
        }
    }
    ...
    implementation 'com.example.watson.library:traceLibrary:1.0.0'
    

    (3)添加注解

    在需要被hook的方法上添加@TraceLog注解

    public class MainActivity extends AppCompatActivity {
        private static final String TAG = "MainActivity";
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    printPerson(new Person(66, "watson"), 100, true, (byte) 0, 'W');
                }
            });
    
        }
    
        @TraceLog
        private Person printPerson(Person person, int x, boolean flag, byte time, char temp) {
            Log.i(TAG, "flag is " + flag);
            Log.i(TAG, "time is " + time);
            Log.i(TAG, "temp is " + temp);
            person.setName("jack");
            person.setAge(x);
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return person;
        }
    }
    

    在编译后,我们可以找到被织入代码后的类文件方法:

    D:\project\plugin-master\app\build\intermediates\transforms\TraceLog\debug\28\com\example\watson\MainActivity.class

    @TraceLog
    private Person printPerson(Person person, int x, boolean flag, byte time, char temp) {
        int var6 = MethodCache.request();
        MethodCache.addMethodArgument(person, var6);
        MethodCache.addMethodArgument(new Integer(x), var6);
        MethodCache.addMethodArgument(new Boolean(flag), var6);
        MethodCache.addMethodArgument(new Byte(time), var6);
        MethodCache.addMethodArgument(new Character(temp), var6);
        long var7 = System.currentTimeMillis();
        Log.i("MainActivity", "flag is " + flag);
        Log.i("MainActivity", "time is " + time);
        Log.i("MainActivity", "temp is " + temp);
        person.setName("jack");
        person.setAge(x);
    
        try {
            Thread.sleep(1000L);
        } catch (Exception var10) {
            var10.printStackTrace();
        }
    
        MethodCache.updateMethodInfo(person, "com/example/watson/MainActivity", "printPerson", "(Lcom/example/watson/Person;IZBC)Lcom/example/watson/Person;", var7, var6);
        MethodCache.printMethodInfo(var6);
        return person;
    }
    

    点击按钮,测试结果:
    在这里插入图片描述
    DEMO下载地址

    参考:
    Android字节码插桩——详细讲解 附带Demo
    从 Java 字节码到 ASM 实践

    展开全文
  • 原本是想写一篇介绍字节码插桩的文章,但无奈的是使用字节码插桩之前需要使用到自定义 Gradle Plugin,似乎暗示着这篇文章并不会短。 在了解字节码插桩之前,我们先了解编译插桩。 编译插桩是什么 相信大家都使用...

    原本是想写一篇介绍字节码插桩的文章,但无奈的是使用字节码插桩之前需要使用到自定义 Gradle Plugin,似乎暗示着这篇文章并不会短。


    在了解字节码插桩之前,我们先了解编译插桩。

    编译插桩是什么

    相信大家都使用过 ButterKnife,了解过它原理的都知道,它是在编译期间生成相应的 java 文件,到运行时,通过反射机制去获取该生成类,并调用其绑定方法,从而做到控件绑定。(什么?你还没了解过 ButterKnife 原理?赶紧去看看吧——《从手写ButterKnife到掌握注解、AnnotationProcessor》

    编译插桩其实就是指在代码编译期间修改已有的代码或者生成新代码。

    字节码插桩是什么

    字节码插桩其实就是比编译插桩更细化一步,编译插桩的范围是指编译过程中,这里包含了java --> class --> dex的整套流程,而字节码插桩只针对于 class 这一步,对生成后的 class 文件进行修改

    字节码插桩前言

    首先,我们先了解下什么情况下会用到字节码插桩。学技术并不是为了秀技术,而是为了解决业务问题。

    我们先想象一个业务场景——我们需要记录用户的点击事件,这时,我们会怎么做?

    • 在每个 onClick() 方法下调用统计代码?这也太繁琐了!更何况人总会有忘记的时候,很容易出现遗漏的情况。
    • 创建新的点击类,每次设置点击监听时使用新的点击类?在自己写的代码上用还好,但是第三方库类怎么办?

    这时就可以用上字节码插桩了!因为 Java 文件编译成 class 后,这时可以获取全部的 class 文件,包含自己写的代码和其它库类的。拿到 class 文件后,就可以进行批量修改,并且对于 Java 文件是无感的,因为我们只针对 class 文件。

    在使用字节码插桩之前,我们需要获取到每个 class 文件,这时,需要使用到自定义Transform,而自定义Transform 需要在自定义 Gradle Plugin 时进行注册,所以,我们需要先学习下如何自定义一个 Gradle Plugin。

    说明文本终于写完,赶紧系好安全带,准备出发,Go!Go!Go!

    自定义Gradle

    由于这是我在 Android Studio 里面进行创建自定义 Gradle,所以很多配置需要自己手写,会比较麻烦,不过没关系,我都写好了,到时你们进行 copy 即可。

    创建Module

    首先,我们需要新建一个 Module:

    Android Studio --> File --> New --> New Module --> Java or Kotlin Library --> click_plugin(命名自取)

    更新build.gradle

    覆盖掉原有的build.gradle文件内容:

    apply plugin: 'groovy'
    apply plugin: 'maven'
    dependencies {
        implementation fileTree(dir: 'libs', includes: ['*.jar'])
        implementation gradleApi()
        implementation localGroovy()
        implementation 'com.android.tools.build:gradle:4.0.0'
    }
    //这两个是配置信息,后续会用到,命名自取
    group='bjsdm.plugin'
    version='1.0.0'
    uploadArchives{
        repositories{
            mavenDeployer{
                // 本地的 Maven 地址设置
                // 部署到本地,也就是项目的根目录下
                // 部署成功会创建一个 bjsdm_repo 文件夹,命名自取
                repository(url: uri('../bjsdm_repo'))
            }
        }
    }
    

    主要注意的是这三个配置:

    • group='bjsdm.plugin'
    • version='1.0.0'
    • repository(url: uri('../bjsdm_repo'))

    创建 Plugin

    创建ClickPlugin.groovy文件

    这里的包名报错不用管它,识别出了问题。

    package bjsdm.plugin
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    public class ClickPlugin implements Plugin<Project>{
        @Override
        void apply(Project project) {
            println("配置成功--------->ClickPlugin")
        }
    }
    

    配置

    进行配置:

    implementation-class=bjsdm.plugin.ClickPlugin
    

    这个就是刚刚所写的类,至于bjsdm.click.properties的命名可以自取,后续配置也会用到。

    部署

    进行 plugin 的部署任务:

    uploadArchives进行双击操作。

    可以看到在根目录下有以下文件生成:

    依赖

    好了,东西生成完毕,这时需要在 app 的build.gradle进行依赖即可:

    主要的是蓝框里面的配置,也就是所提醒的命名,忘了的话,可以翻回去看看。

    apply plugin: 'bjsdm.click'
    buildscript {
        repositories{
            google()
            jcenter()
            //自定义插件仓库地址
            maven {url '../bjsdm_repo'}
        }
        dependencies {
            //加载自定义插件 group + module + version
            classpath 'bjsdm.plugin:click_plugin:1.0.0'
        }
    }
    

    测试

    使用构建命令进行测试:

    ./gradlew clean assembledebug
    

    成功输出了!说明我们创建自定义 Gradle Plugin 成功!

    自定义Transform

    创建ClickTransform.groovy文件:

    重写方法说明

    • getName():设置名字。
    • getInputTypes():用于过滤文件类型。填什么类型,就把该类型的全部文件返回。默认有以下两种类型:
      • QualifiedContent.DefaultContentType.CLASSES:class 文件类型。
      • QualifiedContent.DefaultContentType.RESOURCES:资源文件类型。
    • getScopes():用于规定检索的范围:
      • QualifiedContent.Scope.PROJECT:主 Project。
      • QualifiedContent.Scope.SUB_PROJECTS:其它 Module。
      • QualifiedContent.Scope.EXTERNAL_LIBRARIES:外部库。
      • QualifiedContent.Scope.TESTED_CODE:当前变量的测试代码,包含依赖库。
      • QualifiedContent.Scope.PROVIDED_ONLY:本地或远程的依赖项。
      • QualifiedContent.Scope.PROJECT_LOCAL_DEPS:主 Project 的本地依赖项,包含本地 jar,已废弃,使用QualifiedContent.Scope.EXTERNAL_LIBRARIES代替
      • QualifiedContent.Scope.SUB_PROJECTS_LOCAL_DEPS:其它 Module 的本地依赖项,包含本地 jar,已废弃,使用QualifiedContent.Scope.EXTERNAL_LIBRARIES代替
    • isIncremental():是否支持增量编译。

    根据上述内容,我们可以完善下ClickTransform.groovy

    public class ClickTransform extends Transform {
        @Override
        String getName() {
            return "ClickTransform"
        }
        @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
        }
    }
    

    其中:

    • TransformManager.CONTENT_CLASS:ImmutableSet.of(CLASSES),其实就是把QualifiedContent.DefaultContentType.CLASSES放到 Set 中。
    • TransformManager.SCOPE_FULL_PROJECT:ImmutableSet.of(Scope.PROJECT, Scope.SUB_PROJECTS, Scope.EXTERNAL_LIBRARIES),含义同上。

    以上两个参数都可以根据不同场景自己进行取值。

    好像忘了什么,不是说,全部 class 文件都可以经过 Transform 吗?不然怎么做字节码插桩操作。

    transform()

    确实,所以,我们还需要重写Transformtransform()方法:(旧版本)

        @Override
        void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs,
                       TransformOutputProvider outputProvider, boolean isIncremental)
                throws IOException, TransformException, InterruptedException {
            super.transform(context, inputs, referencedInputs, outputProvider, isIncremental)
        }
    

    不过,以上方法已被弃用,现在推荐使用:(新版本)

        @Override
        void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
            super.transform(transformInvocation)
        }
    

    这两个版本的区别在于,把旧版本中的全部参数封装到新版本的TransformInvocation中,但其实新版本还是在默认调用旧版本的方法:

        public void transform(@NonNull TransformInvocation transformInvocation)
                throws TransformException, InterruptedException, IOException {
            // Just delegate to old method, for code that uses the old API.
            //noinspection deprecation
            transform(transformInvocation.getContext(), transformInvocation.getInputs(),
                    transformInvocation.getReferencedInputs(),
                    transformInvocation.getOutputProvider(),
                    transformInvocation.isIncremental());
        }
    

    transform()方法就像加工通道,我们通过inputs把东西放进去,加工完后,就放到outputProvider中。

    • inputs:传过来的数据,有两种格式:
      • jar 包格式。以 jar 包的形式参与编译,例如依赖的 jar 包。
      • 目录格式。以源码的形式参与编译,例如我们在项目中书写的代码。
    • outputProvider:输出的目录,将修改完的文件复制到输出目录中。

    一定要重写transform(),因为Transform里面的transform()方法是个空方法:

        @Deprecated
        @SuppressWarnings("UnusedParameters")
        public void transform(
                @NonNull Context context,
                @NonNull Collection<TransformInput> inputs,
                @NonNull Collection<TransformInput> referencedInputs,
                @Nullable TransformOutputProvider outputProvider,
                boolean isIncremental) throws IOException, TransformException, InterruptedException {
        }
    

    相当于在transform()这个加工通道中,把东西放进去了,但是没东西出来一样,因为没有把修改完后的文件复制到输出目录中。

    所以,我们现在先实现一个基础功能,打印所放进去的东西的名字:

        @Override
        void transform(TransformInvocation transformInvocation)
                throws TransformException, InterruptedException, IOException {
            //获取输入项进行遍历
            def transformInputs = transformInvocation.inputs
            transformInputs.each { TransformInput transformInput ->
                //遍历 jar 包
                transformInput.jarInputs.each { JarInput jarInput ->
                    println("jarInput:" + jarInput)
                    //使用 JarFile 进行解压
                    def enumeration = new JarFile(jarInput.file).entries()
                    while (enumeration.hasMoreElements()){
                        //获取 jar 里面的内容
                        def entry = enumeration.nextElement()
                        println("jarInput File:" + entry.name)
                    }
                }
                //遍历目录
                transformInput.directoryInputs.each { DirectoryInput directoryInput ->
                    println("directoryInputs:" + directoryInput)
                    //获取目录里面的文件
                    directoryInput.file.eachFileRecurse { File file ->
                        println("directoryInputs File:" + file.name)
                    }
                }
            }
        }
    

    注册

    我们将上面写好的 Transform 注册到 ClickPlugin 中:

        @Override
        void apply(Project project) {
            println("配置成功--------->ClickPlugin")
            project.android.registerTransform(new ClickTransform())
        }
    

    双击 uploadArchives 部署,运行./gradlew clean assembledebug命令:

    由于输出的内容太多了,就不全部截取,只截取部分,很明显地有输出:

    • jarInput
    • jarInput File
    • directoryInputs
    • directoryInputs File

    到这里,终于可以松一口气了,基本流程已经走通,只剩下字节码的更改和把修改后的文件放到输出目录。在这里,我只演示更改 directory 下面的 class 文件,至于 jar 包的 class 文件的更改类似,有兴趣再去深究。

    字节码处理

    关于字节码处理,这里使用了 ASM 工具,主要用到其三个类:

    • ClassReader:负责对 .class 文件进行读取解析。
    • ClassVisitor:负责访问 .class 文件中各个元素,例如读取到方法的时候,会自动调用内部相应的 MethodVisitor。明显的分工操作。
    • ClassWriter:生成字节码工具类,将字节码输出为 byte 数组。

    我们在编写代码前先在build.gradle(:click_plugin)加个依赖:

        //ASM依赖
        implementation 'org.ow2.asm:asm:9.1'
        implementation 'org.ow2.asm:asm-commons:9.1'
    

    ClassVisitor

    我们需要创建一个 ClassVisitor 对 class 文件进行过滤操作,对于符合条件的 method 修改其相应的 method 读取。

    ClickClassVisitor.java,存放在:

    public class ClickClassVisitor extends ClassVisitor {
        public ClickClassVisitor(ClassVisitor classVisitor) {
            super(Opcodes.ASM4, classVisitor);
        }
        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            MethodVisitor methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions);
            //判断方法
            if (name.startsWith("onClick")) {
                System.out.println("onClick");
                //处理点击方法
                return new ClickMethodVisitor(methodVisitor);
            }
            return methodVisitor;
        }
    }
    
    

    MethodVisitor

    在进行代码插入前,我们需要先了解下我们所插入的代码:

    Log.e("TAG", "CLICK")
    

    但是在执行时,其实是分成三部分:

    • “TAG” --> 使用 A 代指
    • “CLICK” --> 使用 B 代指
    • e(Log, A, B)

    为什么要把字符串抽出来呢?

    那时因为字符串在字节码结构中,其实也是一张表,一张存在常量池的表:

    具体的关于字节码结构的介绍,可以参考《字节码结构分析》

    至于这段代码真正编译出来的字节码,我们可以通过反编译进行查看。假如是使用 Android Studio 开发,使用 kotlin 语言,可以直接使用Tools --> Kotlin --> Show Kotlin Bytecode

    参照以上内容,我们可以继续ClickMethodVisitor.java的编写:

    public class ClickMethodVisitor extends MethodVisitor {
        public ClickMethodVisitor(MethodVisitor methodVisitor) {
            super(Opcodes.ASM9, methodVisitor);
        }
        @Override
        public void visitCode() {
            super.visitCode();
            mv.visitLdcInsn("TAG");
            mv.visitLdcInsn("CLICK");
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "e",
                    "(Ljava/lang/String;Ljava/lang/String;)I", false);
            mv.visitInsn(Opcodes.POP);
        }
    }
    

    当然,有更简单的方式,安装ASM Bytecode Viewer Support Kotlin插件,可以在 Android Studio 里面搜索安装。

    安装重启 Android Studio,右键文件 --> Asm Bytecode Viewer :

    修改 transform() 方法:

     @Override
        void transform(TransformInvocation transformInvocation)
                throws TransformException, InterruptedException, IOException {
            //获取输入项进行遍历
            def transformInputs = transformInvocation.inputs
            //获取输出目录
            def transformOutputProvider = transformInvocation.outputProvider
            transformInputs.each { TransformInput transformInput ->
                //遍历 jar 包
                transformInput.jarInputs.each { JarInput jarInput ->
                    //直接将 jar 包 copy 到输出目录
                    File dest = transformOutputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                    FileUtils.copyFile(jarInput.file, dest)
                }
                //遍历目录
                transformInput.directoryInputs.each { DirectoryInput directoryInput ->
                    //获取目录里面的 class 文件
                    directoryInput.file.eachFileRecurse { File file ->
                        if (file.absolutePath.endsWith(".class")){
                            //对于class文件进行读取解析
                            def classReader = new ClassReader(file.bytes)
                            //将class文件内容写入到ClassWriter中
                            def classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                            //使用ClickClassVisitor去读取内容
                            def classVisitor = new ClickClassVisitor(classWriter)
                            //开始读取
                            classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                            //获取修改后的内容
                            def bytes = classWriter.toByteArray()
                            //覆盖之前的文件
                            def outputStream = new FileOutputStream(file.path)
                            outputStream.write(bytes)
                            outputStream.close()
                        }
                    }
                    //将 Directory 的文件 copy 到输出目录
                    File dest = transformOutputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                    FileUtils.copyDirectory(directoryInput.file, dest)
                }
            }
        }
    

    代码看着多,其实都是注释和 copy 代码,不难理解。

    到这里差不多算结束了,不过别忘了写个点击监听事件:

    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            tv_click.setOnClickListener {
                Log.e("TAG", "正常点击事件")
            }
        }
    }
    

    双击 uploadArchives 部署。

    运行…

    终于完成…

    GitHub 地址:https://github.com/bjsdm/TestCode


    这是我的公众号,关注获取第一信息!!欢迎关注支持下,谢谢!

    展开全文
  • ASM字节码插桩

    千次阅读 2020-02-25 22:32:19
    ASM字节码插桩 前言 热修复的多Dex加载方案中,对于5.0以下的系统存在CLASS_ISPREVERIFIED的问题,而解决这个问题的一个方案是:通过ASM插桩,在类的构造方法里引入一个其它dex里的类,从而避免被打上CLASS_...

    个人博客

    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

    展开全文
  • Android字节码插桩demo

    2021-08-04 10:18:37
    Android字节码插桩demo 1. 基本概念 1.1 java字节码 Java字节码是Java虚拟机执行的一种虚拟指令格式。 可通过javac 编译java文件得到字节码文件。 javap 分析字节码文件内容。 1.2 插桩 面向切面编程的一种编程实现...
  • 参考: Android字节码插桩采坑笔记 通过自定义 Gradle 插件修改编译后的 class 文件 ASM官方文档
  • 了解 Gradle,Android Transform 与"字节码插桩" 文章目录了解 Gradle,Android Transform 与"字节码插桩"前言了解 GradleGradle 是什么Gradle 构建过程的三个阶段了解 Gradle Project执行流程编译流程了解 Gradle ...
  • 字节码插桩技术

    千次阅读 2019-06-18 22:39:49
    字节码插桩 我们知道JVM是不能直接执行.java 代码,也不能直接执行.class文件,它只能执行.class 文件中存储的指令码。这就是为什么class需要通过classLoader 装载以后才能运行。基于此机制可否在ClassLoader装载...
  • 字节码插桩框架ASM(一)

    千次阅读 2019-12-24 10:12:41
    字节码插桩就是修改节码文件(.class). 如同 gson框架是用来做操json数据结构的,那么asm框架就是用来操作.class文件结构的。 那么这有什么用处呢? 这个是一个很强大而且很高级的功能。我们可能知道反射hook...
  • 什么是字节码插桩 字节码插桩就是在构建的过程中,通过修改已经编译完成的字节码文件,也就是class文件,来实现功能的添加 从技术上来说,字节码插桩是自定义Gradle插件、ASM、Java字节码、切面编程的综合应用 字节...
  • Android字节码插桩实战

    2019-09-15 00:55:55
    理解本文需要一定的Java汇编指令基础...利用Android字节码插桩技术可以很方便地帮助我们实现很多手术刀式的代码设计,如无埋点统计上报、轻量级AOP等。下面我们就通过一次实战,把这门技术真正用起来。 ###奇葩需求...
  • 师傅:徒儿,听过字节码插桩嘛? 徒儿:师傅,徒儿诗如李白,貌如潘安,字如王羲之,歌如刘德华,智慧如诸葛亮,数学如华罗庚,对于字节码插桩,我怎么能不懂呐。 师傅:徒儿,你这什么时候,练就了吹牛逼吹的都...
  • HiBeaver是一个用于进行Android字节码插桩的Gradle插件,可以用来实现Android轻量级AOP设计
  • 字节码插桩技术可以帮我们实现业务层模块和功能模块的关联,并在项目结构避免其耦合,比如ARouter实现各模块路由表的注册; 2 背景 一般我们使用Transform会有下面两种场景 我们需要对编译class文件做自定义的处理...
  • 关于android字节码插桩

    千次阅读 2018-01-30 11:39:57
    基于字节码插桩可以实现面向切面的编程, 实际是在字节码中插入要执行的相关程序. 通过非侵入的方式实现切面编程. (1)AOP和OOP 如果说oop模块化编程, 是把功能封装到一个模块中, 那么aop就是把众多模块中的问题, ...
  • Android AOP实现原理之字节码插桩(一)参考博客 Android AOP之字节码插桩博客 Android热补丁动态修复技术(三)—— 使用Javassist注入字节码,完成热补丁框架雏形(可使用)由衷感谢以上博主分享的技术知识!...
  • 行业分类-设备装置-基于JAVA字节码插桩和JAVA方法挂钩检测JAVA沙箱逃逸攻击
  • title: Android AOP之字节码插桩author: 陶超description: 实现数据收集SDK时,为了实现非侵入的,全量的数据采集,采用了AOP的思想,探索和实现了一种Android上AOP的方式。本文基于数据收集SDK的AOP实现总结而成。...
  • 作者:享学课堂Lance老师 转载请声明出处! 一、什么是插桩 QQ空间曾经发布的《热修复...字节码插桩顾名思义就是在我们编写的源码编译成字节码(Class)后,在Android下生成dex之前修改Class文件,修改或者增强原有代.
  • 利用Android字节码插桩技术可以很方便地帮助我们实现很多手术刀式的代码设计,如无埋点统计上报、轻量级AOP等。下面我们就通过一次实战,把这门技术真正用起来。 奇葩需求 假设有这样一个需求,我们需要在本项目工程...
  • 什么是字节码插桩 字节码插桩就是在构建的过程中,通过修改已经编译完成的字节码文件,也就是class文件,来实现功能的添加 从技术上来说,字节码插桩是 自定义Gradle插件+ASM+Java字节码+切面编程 的综合应用 字节码...
  • 字节码插桩」统计方法耗时(第一篇:初出茅庐)- 第311篇 「字节码插桩」统计方法耗时(第二篇:崭露头角)- 第311篇 师傅:上一节,我们对于javaagent有了一个简单的认知,但是对于如何修改修改字节码,我们还....
  • 这里的代码可以分为源码和字节码,而我们所说的插桩一般指字节码插桩。 图1是Android开发者常见的一张图,我们编写的源码(.java)通过javac编译成字节码(.class),然后通过dx/d8编译成dex文件。 我们下面要讲的...
  • 最开始有研究字节码插桩技术冲动的是我们接入了一款统计类的SDK(这里我就不具体说是哪款了)。他们的套路是第三方开发者需要接入他们的插件(Gradle Plugin),然后便可以实现无埋点进行客户端的全量数据统计(全量的意思...
  • LargeImageMonitor是一个使用ASM进行字节码插桩的大图监控框架,可以对我们要加载的图片进行监控,如果出现图片超过阈值的情况会进行报警。 功能与特点 1.支持Glide,Fresco,Picasso,Universal Image Loader。 2....

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,805
精华内容 722
关键字:

字节码插桩