精华内容
参与话题
问答
  • 字节码插桩技术

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

    字节码插桩

    我们知道JVM是不能直接执行.java 代码,也不能直接执行.class文件,它只能执行.class 文件中存储的指令码。这就是为什么class需要通过classLoader 装载以后才能运行。基于此机制可否在ClassLoader装载之前拦截修改class当中的内容(jvm 指令码)从而让程序中包含我们的埋点逻辑呢?答案是肯定的,但需要用到两个技术 javaagent与javassist 。前者用于拦截ClassLoad装载,后者用于操作修改class文件。

    javaagent

    javaagent介绍

    javaagent 是java1.5之后引入的特性,其主要作用是在class 被加载之前对其拦截,以插入我们的监听字节码

    javaagent jar包

    javaagent 最后展现形式是一个Jar包,有以下特性:

    1.必须 META-INF/MANIFEST.MF中指定Premain-Class 设定启agent启动类。

    2.在启类需写明启动方法 public static void main(String arg,)

    3.不可直接运行,只能通过 jvm 参数-javaagent:xxx.jar 附着于其它jvm 进程运行。

     

    javaagent使用

    1、编写agent方法

    public class MyAgent {
        public static void premain(String args, Instrumentation instrumentation) throws Exception {
            System.out.println("Hello javaagent permain:"+args);
        }
    }

    2、添加premain-class参数

    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>2.2</version>
        <configuration>
            <archive>
                <manifestEntries>
                    <Project-name>${project.name}</Project-name>
                    <Project-version>${project.version}</Project-version>
                    <Premain-Class>com.javaagent.MyAgent</Premain-Class>
                    <Can-Redefine-Classes>true</Can-Redefine-Classes>
                    <Can-Retransform-Classes>true</Can-Retransform-Classes>
                </manifestEntries>
            </archive>
            <skip>true</skip>
        </configuration>
    </plugin>

    3、构建打包

    4、在任一JAVA应用中 添加jvm 参数并启动 -javaagent:xxx.jarjavaagent META-INF/MANIFEST.MF

    参数说明:

    Premain-Class:必填,agent启动

    classCan-Redefine-Classes:默认为false ,是否允许重新定义

    classCan-Retransform-Classes:默认为false,是否允许重置Class,重置后相当于class 从classLoade中清除,下次有需要的时候会重新装载,也会重新走Transformer 流程。

    Boot-Class-Path:agent 所依赖的jar 路径,多个用空格分割

    创建一个测试类MyAgentTest并运行查看结果

    public class MyAgentTest {
        public static void main(String[] args) {
            System.out.println("main");
        }
    }
    //运行结果:main

    添加jvm参数

    参数内容:-javaagent:/Users/jinyunlong/IdeaProjects/test-agent/target/test-agent-1.0-SNAPSHOT.jar=123

    再次运行测试类MyAgentTest并查看结果

    javassist

    javassist介绍

    javassist是一个开源的分析、编辑和创建Java字节码的类库。其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成(注:也可以使用ASM实现,但需要会操作字节码指令,学习使用成本高)

    javassist使用

    使用javassist需要引入javasssist的jar包,添加内容如下:

    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.18.1-GA</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <Project-name>${project.name}</Project-name>
                            <Project-version>${project.version}</Project-version>
                            <Premain-Class>com.javaagent.MyAgent</Premain-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                            <Boot-Class-Path>javassist-3.18.1-GA.jar</Boot-Class-Path>
                        </manifestEntries>
                    </archive>
                    <skip>true</skip>
                </configuration>
            </plugin>
        </plugins>
    </build>

    演示插入打印当前时间

    创建类MyServer

    public class MyServer {
        public Integer sayHello(String name,String message){
            System.out.println("hello");
            return 0;
        }
    }

    myAgent类

    创建测试类并调用MyServer中的sayHello方法

    演示计算方法调用时间

    类MyAgent

    public class MyAgent {
        public static void premain(String args, Instrumentation instrumentation) throws Exception {
            instrumentation.addTransformer(new ClassFileTransformer() {
                public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, 
                byte[] classfileBuffer) throws IllegalClassFormatException {
                    if(!"com/javaagent/MyServer".equals(className)){
                        return null;
                    }
                    try {
                        return buildMonitorClass();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    return null;
                }
            },true);
    
        }
        private static byte[] buildMonitorClass() throws Exception{
            /**
             * 1、拷贝一个新的方法
             * 2、修改原方法名
             * 3、加入监听代码
             */
            ClassPool pool = new ClassPool();
            pool.appendSystemPath();
            CtClass ctClass = pool.get("com.javaagent.MyServer");
            CtMethod ctMethod = ctClass.getDeclaredMethod("sayHello");
            CtMethod copyMethod = CtNewMethod.copy(ctMethod,ctClass,new ClassMap());
            ctMethod.setName("sayHello$agent");
            copyMethod.setBody("{\n" +
                    "    long begin = System.nanoTime();\n" +
                    "    try {\n" +
                    "        return sayHello$agent($1,$2);\n" +
                    "    } finally {\n" +
                    "        System.out.println(System.nanoTime() - begin);}\n" +
                    "    }");
            ctClass.addMethod(copyMethod);
            return ctClass.toBytecode();
        }
    }

    修改类MyServer

    public class MyServer {
    
        public Integer sayHello(String name,String message){
            System.out.println("hello name:"+name+",message:"+message);
            return 0;
        }
    }

    修改测试类并运行

    public class MyAgentTest {
        public static void main(String[] args) {
            MyServer myServer = new MyServer();
            myServer.sayHello("paul","1234");
        }
    }
    //运行结果:
    hello name:paul,message:1234
    186537

    javassist 特殊语法

    展开全文
  • 这里的代码可以分为源码和字节码,而我们所说的插桩一般指字节码插桩。 图1是Android开发者常见的一张图,我们编写的源码(.java)通过javac编译成字节码(.class),然后通过dx/d8编译成dex文件。 我们下面要讲的...

    1 什么是插桩?

    听到关于“插桩”的词语,第一眼觉得会很高深,那到底什么是插桩呢?用通俗的话来讲,插桩就是将一段代码通过某种策略插入到另一段代码,或替换另一段代码。这里的代码可以分为源码和字节码,而我们所说的插桩一般指字节码插桩。 图1是Android开发者常见的一张图,我们编写的源码(.java)通过javac编译成字节码(.class),然后通过dx/d8编译成dex文件。

    我们下面要讲的插桩,就是在.class转为.dex之前,修改.class文件从而达到修改或替换代码的目的。 那有人肯定会有这样的疑问?既然插桩是插入或替换代码,那为何我不自己直接插入或替换呢?为何还要用这么“复杂”的工具?别着急,第二个问题将会给你答案。

    2 插桩的应用场景有哪些?

    技术是服务于业务的,一个无法推进业务进步的技术并不值得我们学习。在上面,我们对插桩的理解是:插入,替换代码。那么,结合这个核心主线我们来挖掘插桩能被应用的场景有哪些?

    代码插入

    我们所熟悉的ButterKnife,Dagger这些常用的框架,也是在编译期间生成了代码,简化了程序员的操作。假设有这么一个需求,要监控某些或者所有方法的执行耗时?你会怎么做呢?如果你监控的方法只有十几个或者几十个,那么也许通过程序员自身的编码就能轻松解决;但是如果监控的方法达到百千甚至万级别,你还通过编码来解决?那么程序员存在的价值在哪里?面对这样的重复劳动问题,最先想到的就应该是自动化,也就是我们今天所讲的插桩。通过插桩,我们扫描每一个class文件,并针对特定规则进行字节码修改从而达到监控每个方法耗时的目的。关于如何实现这样的需求,后面我会详细讲述。

    代码替换

    如果遇到这么一个需求,需要将项目中所有使用某个方法(如Dialog.show())的地方替换成自己包装的方法(MyDialog.show()),那么你该如何解决呢?有人会说,直接使用快捷键就能全局替换。那么有两个问题

    1. 如果有其他类定义了show()方法,并被调用了,直接使用快捷键是否会被错误替换?
    2. 如果其他引用包使用了该方法,你怎么替换呢?

    没关系,插桩同样可以解决你的问题。 综合上面所说的两点,其实很多业务场景都使用了插桩技术,比如无痕埋点,性能监控等。

    3 掌握插桩应该具备的基础知识有哪些?

    上面讲了插桩的应用场景,是否现在想跃跃欲试呢?别着急,想掌握好插桩技术,练就扎实的插桩功底,我们是需要具备一些基础知识的。

    • 熟练掌握字节码相关技术。可参考 一文让你明白Java字节码

    • Gradle自定义插件,直接参考官网 Writing Custom plugins

    • 如果你想运用在Android项目中,那么还需要掌握Transform API, 这是android在将class转成dex之前给我们预留的一个接口,在该接口中我们可以通过插件形式来修改class文件。

    • 字节码修改工具。如AspectJ,ASM,javasisst。这里我推荐使用ASM,关于ASM相关知识,在下一章我给大家简单介绍。同样大家可以参考 Asm官方文档

    • groovy语言基础

    如果你具备了上面5块知识,那么恭喜你,会很顺利的完成字节码插桩技术了。下面,我通过实战一个很简单的例子,带领大家一起领略插桩的风采。

    4 使用ASM进行字节码插桩

    1 什么是ASM?

    ASM是生成和转换已编译的Java类工具,就是我们插桩需要使用的工具。

    2 两种API?

    ASM提供了两种API来生成和转换已编译类,一个是核心API,以基于事件形式来表示类;另一个是树API,以基于对象形式来表示类。

    3 基于事件形式

    我们通过上面的基础知识,了解到类的结构,类包含字段,方法,指令等;基于事件的API把类看作是一系列事件来表示,每一个类的事件表示一个类的元素。类似解析XML的SAX

    4 基于对象形式

    基于对象的API将类表示成一棵对象树,每个对象表示类的一部分。类似解析XML的DOM

    5 优缺点比较

    事件形式 对象形式
    内存占用
    实现难度

    通过上面表格,我们清楚的了解到:

    • 事件API内存占用少于对象API,因为事件API不需要在内存中创建和存储对象树
    • 事件API实现难度比对象API大,因为事件API在任意时刻类中只有一个元素可使用,但是对象API能获得整个类。

    那么接下来,我们就通过比较容易实现的对象API入手,一起完成上面的需求。 我们Android的构建工具是Gradle,因此我们结合transform和Gradle插件方式来完成该需求,接下来我们来看看gradle官方提供的3种插件形式 6 Gradle插件的3种形式

    插件形式 说明
    Build script 直接在build script中写插件代码,不可复用
    buildSrc 独立项目结构,只能在本构建体系中复用,无法提供给其他项目
    Standalone 独立项目结构,发布到仓库,可以复用

    由于我们是demo,并不需要共享给其他项目,因此采用buildSrc方式即可,但是正常项目中都采用Standalone形式。

    5 插桩实践

    目标 : 删除所有以test开头的方法

    接下来我们来完成一个非常小的需求,删除所有以test开头的方法。为什么说这是一个小需求,因为这并不涉及指令的操作,所有操作通过方法名完成即可。通过完成这个demo,只是抛砖引玉。如若后期需要,可以逐步深入到指令级别替换。 接下来的步骤就是创建demo的过程

    • 1 新建buildSrc目录,用来存放源代码位置。针对不同语言可以新建不同目录。
      如上图所示的是buildSrc的结构。
    • 2 在buildSrc的gradle文件中我们需要配置如下代码
    apply plugin: 'groovy'
    dependencies {
       compile gradleApi()//在使用自定义插件时候,一定要引用org.gradle.api.Plugin
       compile 'com.android.tools.build:gradle:3.3.2'//使用自定义transform时候,需要引用com.android.build.api.transform.Transform
       compile 'org.ow2.asm:asm:6.0'
       compile 'commons-io:commons-io:2.6'
    }
    repositories {
       mavenCentral()
       jcenter()
       google()
    }
    复制代码
    • 3 重写Transform API 在groovy目录下新建一个groovy类并继承Transform,注意导包com.android.build.api.transform,并实现抽象方法和transform方法,如下
    class MyTransform extends Transform {
       Project project
       MyTransform(Project project) {
           this.project = project
       }
       @Override
       String getName() {
           return "MyTransform"
       }
       //设置输入类型,我们是针对class文件处理
       @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(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
           inputs.each {
               TransformInput input ->
                   input.getJarInputs().each {
                   //处理jar文件,代码太多,这里暂时不贴
                   }
                   input.getDirectoryInputs().each {
                   //处理目录文件,这里的ASMHelper.transformClass()是修改字节码逻辑
                       def destDir = transformInvocation.outputProvider.getContentLocation(
                               "${dir.name}_transformed",
                               dir.contentTypes,
                               dir.scopes,
                               Format.DIRECTORY)
                       if (dir.file) {
                           def modifiedRecord = [:]
                           dir.file.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) {
                               File classFile ->
                                   def className = classFile.absolutePath.replace(dir.getFile().getAbsolutePath(), "")
                                   if (!ASMHelper.filter(className)) {
                                       def transformedClass = ASMHelper.transformClass(classFile, dir.file, transformInvocation.context.temporaryDir)
                                       modifiedRecord[(className)] = transformedClass
                                   }
                           }
                           FileUtils.copyDirectory(dir.file, destDir)
                           modifiedRecord.each { name, file ->
                               def targetFile = new File(destDir.absolutePath, name)
                               if (targetFile.exists()) {
                                   targetFile.delete()
                               }
                               FileUtils.copyFile(file, targetFile)
                           }
                           modifiedRecord.clear()
                   }
           }
       }
    }
    复制代码
    • 4 实现字节码修改逻辑 Transform我们已经定义完成,接下来就要针对读入的字节码进行修改。我们采用对象API进行解析class文件。一共就是3个步骤:
    1. 将输入流转化为ClassNode
    2. 处理ClassNode,这里就是我们的业务逻辑所在
    3. 将ClassNode转为字节数组输出 当然还有其他文件的IO操作,这里因为篇幅限制未贴出,如若需要demo,可以私信。
    static byte[] modifyClass(InputStream inputStream) {
           ClassNode classNode = new ClassNode(Opcodes.ASM5)
           ClassReader classReader = new ClassReader(inputStream)
           //1 将读入的字节转为classNode
           classReader.accept(classNode, 0)
           //2 对classNode的处理逻辑
           Iterator<MethodNode> iterator = classNode.methods.iterator();
           while (iterator.hasNext()) {
               MethodNode node = iterator.next()
               if (node.name.startsWith("test")) {
                   iterator.remove()
               }
           }
           ClassWriter classWriter = new ClassWriter(0)
           //3  将classNode转为字节数组
           classNode.accept(classWriter)
           return classWriter.toByteArray()
       }
    复制代码
    • 5 插件化 上面我们完成了字节码修改逻辑以及定义Transform,但是并没有完成插件的定义。结合Transform API我们了解到,需要将我们自定义的Transform注册到插件中,如下
    class MyPlugin implements Plugin<Project> {
        @Override
        void apply(Project project) {
            project.android.registerTransform(new MyTransform(project))
        }
    }
    复制代码
    • 6 提供可对外使用的插件 插件完成了,但是怎么才能对外使用呢?上面我们说到,我们采取3种插件形式之一的buildSrc。我们上文中创建了plugin.properties文件。只需要在该文件中编辑实现类即可
    implementation-class=MyPlugin
    复制代码
    • 7 应用方应用插件 在应用方的gradle文件中做如下配置
    apply plugin: 'plugin'
    复制代码

    上面代码我们注意到,plugin这个插件和plugin.properties的文件名是一样的。是的,应用方应用的插件名和我们定义的properties文件名保持一致。

    • 8 结果展示 源代码如下,经过我们插件处理之后,编译后的字节码应该没有了testDemo方法。
    public class MainActivity extends Activity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(android.R.layout.activity_list_item);
        }
        public void testDemo() {
            System.out.println("demo test");
        }
    }
    复制代码

    那么,处理后的字节码在哪呢?在*$project/build/intermediates/transforms/MyTransform/...* MyTransform是我自定义Transform的类名,下面有debug和release包。继续下去大家应该能找到对应的类。

    上图我们看到,已经没有的testDemo方法。成功!

    6 结束语

    通过上面实战练习,相信你已经初步掌握了插桩的基本技术,但是这还远远不够;在项目中会遇到各式各样的问题,现实情况可能没有demo这么简单;不过没关系,如果在插桩过程中遇到任何问题,都可以私信给我,我将尽我所能的给你提供最优质的免费咨询服务。同时,我也非常欢迎大家互相交流技术,共同成长。

    展开全文
  • 这里的代码可以分为源码和字节码,而我们所说的插桩一般指字节码插桩。 我们都知道JAVA是面向对象(继承、封装、多态),而插桩的意义在于面向切面(AOP),可想而知单方面的面向对象开发有许多的局限性,而结合...

    一、什么是插桩?

    很多人听到关于“插桩”的词语,第一眼觉得会很高深,那到底什么是插桩呢?用通俗的话来讲,插桩就是将一段代码通过某种策略插入到另一段代码,或替换另一段代码。这里的代码可以分为源码和字节码,而我们所说的插桩一般指字节码插桩。

    我们都知道JAVA是面向对象(继承、封装、多态),而插桩的意义在于面向切面(AOP) ,可想而知单方面的面向对象开发有许多的局限性,而结合面向切面编程可以说补足了我们的这种局限性。举个例子:在onClick中一般都要做防抖动操作,这样是为了避免多次打开页面的问题。一般实现的话是在每个onClick实现第二次点击的时候加个时间判断。而插桩的话业务端可以不写任何代码通过插桩的方法把这个时间判断插入的字节码里面。从标题名字看Java字节码:是Java虚拟机执行的一种虚拟指令格式。通过JVM转换生成机器指令插桩:是在保证被测程序原有逻辑完整性的基础上在程序中插入一些探针(又称为“探测仪”)。

    二、插桩能带来什么

    三、AOP思想

     

     

    银行系统会有一个取款流程,我们可以把方框里的流程合为一个,另外系统还会有一个查询余额流程,我们先把这两个流程放到一起,有没有发现,这个两者有一个相同的验证流程,我们先把它们圈起来再说下一步,有没有想过可以把这个验证用户的代码是提取出来,不放到主流程里去呢,这就是AOP的作用了,有了AOP,你写代码时不要把这个验证用户步骤写进去,即完全不考虑验证用户。

    什么是AOP:把这些横跨并嵌入众多模块里的功能(如监控每个方法的性能) 集中起来,放到一个统一的地方来控制和管理能给我带来什么:不修改源代码的情况下给程序动态统一添加功能的一种技术,把散落在程序中的公共部分提取出来,做成切面类,这样的好处在于,代码的可重用,一旦涉及到该功能的需求发生变化,只要修改该代码就行,否则,你要到处修改,如果只要修改1、2处那还可以接受,万一有1000处呢。

    4 Android打包流程插桩入口

    这是app打包流程的整个过程而我把这个打包流程主要分为一下步骤:

    • aapt来打包资源文件,生成R.java文件
    • 处理AIDL,生成对应的.java接口文件
    • 编译Java文件,生成对应的.class文件
    • 把.class文件转化成Davik VM支持的.dex文件
    • 打包生成未签名的.apk文件。

    字节码插桩入口:我们知道Android程序从Java源代码到可执行的Apk包主要分析两个环节:

    • javac:将源文件编译成class格式的文件
    • dex:将class格式的文件汇总到dex格式的文件中

    我们要想对字节码进行修改,只需要在javac之后,dex之前对class文件进行字节码扫描,并按照一定规则进行过滤及修改就可以了,这样修改过后的字节码就会在后续的dex打包环节被打到apk中,这就是我们的插桩入口。

    插桩方式一、:transform api

    每个Transform其实都是一个gradle task,Android编译器中的TaskManager将每个Transform串连起来,第一个Transform接收来自javac编译的结果,以及已经拉取到在本地的第三方依赖(jar. aar),还有resource资源,注意,这里的resource并非android项目中的res资源,而是asset目录下的资源。这些编译的中间产物,在Transform组成的链条上流动,每个Transform节点可以对class进行处理再传递给下一个Transform。我们常见的混淆,Desugar等逻辑,它们的实现如今都是封装在一个个Transform中,而我们自定义的Transform,会插入到这个Transform链条的最前面。

    对于Android Gradle Plugin 版本在1.5.0及以上的情况,Google官方提供了transformapi用作字节码插桩的入口。

    implementation 'com.android.tools.build:gradle:1.5.0'复制代码
    

    一般使用方法为:extends Transform重写transform()

    插桩方式二:hook dx.jar

    需要引入Instrumentation

    7.png

    通过Java Instrumentation机制,为获得插桩入口,对于apk build过程进行了两处插桩(即hook),图中标红部分:

    Instrumentation:指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。

    • 在build进程,对ProcessBuilder.start()方法进行插桩
      ProcessBuilder类是J2SE 1.5在java.lang中新添加的一个新类,此类用于创建操作系统进程,它提供一种启动和管理进程的方法,start方法就是开始创建一个进程,对它进行插桩,使得通过下面方式启动dx.jar进程执行dex任务时:

      java  dex.jar  com.android.dx.command.Main  --dex
      

      增加参数-javaagent agent.jar,使得dex进程也可以使用Java Instrumentation机制进行字节码插桩

    • 在dex进程
      对我们的目标方法com.android.dx.command.Main.processClasses进行字节码插入,从而实现打入apk的每一个项目中的类都按照我们制定的规则进行过滤及字节码修改。

    build进程使用Instrumentation的方式时之前叙述过的VirtualMachine.loadAgent方式(方式二),dex进程中的方式则是-javaagent agent.jar方式(方式一)。

    由此,我们获得了进行字节码插桩的入口,下面我们就使用ASM库的API,对项目中的每一个类进行扫描,过滤,及字节码修改。

    五、自定义Gradle插件

    1、创建一个Android library Module工程

    2、build.gradle改成groovy方式

    apply plugin: 'groovy'
    
        dependencies {
            compile gradleApi()
            compile localGroovy()
        }复制代码
    

    3、新建.groovy类继承 Plugin并实现apply方法,注意:类的后缀不再是.java而是.groovy

    4、在main下创建resources目录

    5、增加对应的maven deployer发布到本地或远程仓库

    6、使用已发布的仓库

    六、ASM

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

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

    Framework First time Later times
    Javassist 257 5.2
    BCEL 473 5.5
    ASM 62.4 1.1

    可以使用一个插件[ASM Bytecode Outline]更有效的用ASM编写字节码

    ASM(core api) 按照visitor模式按照class文件结构依次访问class文件的每一部分,有如下几个重要的visitor。

    操作流程

    1. 需要创建一个 ClassReader 对象,将 .class 文件的内容读入到一个字节数组中
    2. 然后需要一个 ClassWriter 的对象将操作之后的字节码的字节数组回写
    3. 需要事件过滤器 ClassVisitor。在调用 ClassVisitor 的某些方法时会产生一个新的 XXXVisitor 对象,当我们需要修改对应的内容时只要实现自己的 XXXVisitor 并返回就可以了

    ClassReader 类

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

    ClassWriter 类

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

    ClassVisitor 抽象类

    • 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()
      该方法是当扫描器完成类扫描时才会调用,如果想在类中追加某些方法

    MethodVisitor & AdviceAdapter

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

    AdviceAdapter

    其中比较重要的几个方法如下:

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

    字节码基础

    • 全限定名即为全类名中的“.”,换为“/”,举例:

      类android.widget.AdapterView.OnItemClickListener的全限定名为:
      android/widget/AdapterView$OnItemClickListener复制代码
      
    • 描述符(descriptors):
      1.类型描述符,如下图所示:

    在class文件中类型 boolean用“Z”描述,数组用“[”描述(多维数组可叠加),那么我们最常见的自定义引用类型呢?“L全限定名;”.例如:
    Android中的android.view.View类,描述符为“Landroid/view/View;”

    2.方法描述符的组织结构为:

    (参数类型描述符)返回值描述符复制代码复制代码
    

    其中无返回值void用“V”代替,举例:

    方法boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id)  的描述符如下:
    (Landroid/widget/ExpandableListView;Landroid/view/View;IJ)Z复制代码
    

    对上图中三个步骤的详细说明:

    步骤一:

    ASM的ClassVisitor对所有类的class文件进行扫描,在visitMethod()方法中判断是不是BaseActivity,如果是进行步骤二,否则终止扫描;

    步骤二:

    ClassVisitor每扫描到一个方法时,在visitMethod中进行如下判定:

    1. 是不是要过滤的<init>方法

    如果判定通过,则证明本次扫描到的方法是需要注入字节码的方法,然后将
    将扫描逻辑交给MethodVisitor,进行字节码的修改(步骤三)。

    步骤三:修改扫码到的方法字节码

    假设待修改的方法如下:

    public int test() {
      try {  
          Thread.sleep(1000);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
    }
    

    修改之后需要变成:

    public int test() {
       long startTime = System.currentTimeMillis();
       try {  
           Thread.sleep(1000);}
       catch (InterruptedException e){ 
           e.printStackTrace();  
       }
       long timing = System.currentTimeMillis() - startTime;
       BlockManager.timingPage(getLocalClassName(), timing);
    }
    

    七、 结束语

    通过上面实战练习,相信你已经初步掌握了插桩的基本技术,但是这还远远不够;在项目中会遇到各式各样的问题,现实情况可能没有demo这么简单;不过没关系,大家转发加关注,如果在插桩过程中遇到任何问题,都可以私信给我,或者我这里也有也一份关于字节码插桩的详细视频教程,大家可以加入Android开发学习交流群:818520403 ,免费领取视频,看看前辈们是如何在编程的世界里傲然前行!群里不停更新最新的教程和学习方法,同时,我也非常欢迎大家互相交流技术,共同成长。

    展开全文
  • 什么是字节码插桩 字节码插桩就是在构建的过程中,通过修改已经编译完成的字节码文件,也就是class文件,来实现功能的添加 从技术上来说,字节码插桩是自定义Gradle插件、ASM、Java字节码、切面编程的综合应用 字节...

    前言

    什么是字节码插桩

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

    从技术上来说,字节码插桩是自定义Gradle插件、ASM、Java字节码、切面编程的综合应用

    字节码插桩可以做什么

    举个例子,APP全量统计的时候,经常需要建立很多埋点。这是个很大重复性工作,那么可以通过字节码插桩,在apk打包之前,对class文件需要的地方进行埋点。这样就可以实现无埋点的全量统计。

    下面我们来逐一介绍用到的知识,可能需要学习很多东西,学完这些将会有很大的收货!!

    一、切面编程 AOP

    AOP(Aspect Oriented Program的首字母缩写)是一种面向切面编程的思想。这种编程思想是相对于OOP(ObjectOriented Programming即面向对象编程)来说的。

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

    但是存在一个问题,如果每个类中都需要同样的功能,例如日志,统计等。这个是面向对象的编程天生的缺点,就是分散代码的同时,也增加了代码的重复性。按照OOP的思想,我们需要在各个模块里面都添加统计代码。但是如果按照AOP的思想,可以将统计的地方抽象成切面,只需要在切面里面添加统计代码就OK了。

    字节码插桩是AOP编程一种很好的实现方式,在后台开发的Spring框架中已经在使用切面编程来添加操作日志记录在这里插入图片描述

    二、APK打包流程

    在APK打包的时候,我们要对字节码进行修改,那么就需要了解整个打包流程,知道在哪个过程中可以获取到字节码。

    官网的打包流程介绍的不是很详细,下图介绍了详细的打包流程。

    apk打包使用的工具是gradle,Android 提供了Gradle插件 com.android.tools.build:gradle,使得我们可以轻松执行这个打包流程
    在这里插入图片描述

    经过“Java Compiler步骤”,也就是代码编译,系统便生成了.class文件。这些class文件经过dex步骤再次转化成Android识别的.dex文件。

    既然我们要做字节码插桩,就必须hook打包流程,在dex步骤之前对class字节码进行扫描与重新编织,然后将编织好的class文件交给dex过程。这样就实现了所谓的无埋点。

    那么问题来了,怎么才能在打包流程中,添加我们想要执行的操作。也就是说如何才能拦截住打包流程呢? 我们下面分解

    三、自定义Gradle插件

    整个打包流程是由Android Gradle插件 com.android.tools.build:gradle提供的,在1.5.0-beta1 及以后的版本,添加了Transform API ,允许第三方Gradle 插件,在打包为dex 之前,可以对class进行操作。(这些书都有记载,不是我在乱掰。详见官网

    那么由此引出两个知识点,介绍这两个的篇幅有点长,所以列出一下网上比较好的文章

    3.1、自定义Gradle插件

    自定义Gradle插件 官方文档:Developing Custom Gradle Plugins

    在AndroidStudio中自定义Gradle插件
    拥抱 Android Studio 之五:Gradle 插件开发

    对以上两边文章,有两点补充:

    1、关于自定义Gradle插件 ,网上中文文档,基本都还是在介绍使用使用groovy,现在已经支持使用kotlin来编写Gradle插件。两者只在目录方面有些差异。最后的demo就是使用kotlin来编写Gradle插件的

    2、在build.gradle 文件中,通常会出现 apply plugin: 'com.android.application' 这里apply plugin 的是groovy 语言调用函数的方式,这句代码会调用com.android.application插件的apply() 函数

    3.2、如何使用Transform API

    Transform详解

    四、Java字节码

    Java 字节码(英语:Java bytecode)是Java虚拟机执行的一种指令格式。通俗来讲字节码就是经过javac命令编译之后生成的Class文件。Class文件包含了Java虚拟机指令集和符号表以及若干其他的辅助信息。Class文件是一组以8位字节为基础单位的二进制流,所有数据项目严格按照顺序紧凑的排列在Class文件之中,中间没有任何分隔符,这使得整个Class文件中存储的内容几乎全是程序运行时的必要数据。

    通俗的说就是,Java代码编译后生成的Class文件,这个文件是二进制。这个文件有个规定,第几位到第几位是什么数据,

    4.1、关于字节码几个重要的内容::

    1、 Class文件中使用全限定名来表示一个类的引用,全限定名很容易理解,即把类名所有“.”换成了“/”

    例如:
    android.widget.TextView 的全限定名 android/widget/TextView

    2、描述符
    Class文件中使用描述符,描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。

    1. 基本数据类型(byte char double float int long short boolean)以及代表无返回值的void类型都用一个大写字符( Type Signature)来表示
    2. 对象类型则用字符“L”加对象的全限定名来表示,一般对象类型末尾都会加一个“;”来表示全限定名的结束。

    类型签名

    Type Signature Java Type
    Z boolean
    B byte
    C char
    S short
    I int
    J long
    F float
    D double
    L fully-qualified-class ;fully-qualified-class
    [ type type[]
    ( arg-types ) ret-type method type
    4.2、Java字节码

    一文让你明白Java字节码

    看了字节码,相信你一定有疑问,是不是对class文件修改很复杂呀?其实根本没有这么复杂,而且已经有很多支持字节码编织的框架,学习字节码,是为了在使用框架修改字节码时更上手一点

    五、 Java 字节码编织框架——ASM

    什么是ASM?

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

    看完下面这篇文章,基本可以直接上手使用ASM框架了,通过这个框架,我们可以修改class文件,增加函数,修改函数、各种逻辑等等编程操作。

    AOP 的利器:ASM 3.0 介绍,这篇文章有点过时,但是基本的原理没变,所以可以学习其思想

    ASM官方文档

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

    如果想深入学习ASM,可以查看这个系列的文章:
    1.1 ASM-简介-目的

    ASM Bytecode Outline

    插件ASM Bytecode Outline,可以把java代码转为ASM框架 的代码,那么我们可以先修改好一个类的代码,把代码转为ASM框架的代码,然后把需要的代码复制到,这样就可以在自定义的gradle plugin 中批量自动去修改目标类了。

    参考:
    Android字节码插桩采坑笔记
    通过自定义 Gradle 插件修改编译后的 class 文件

    ASM官方文档
    https://www.sharezer.com/archives/1574

    JVM的类型签名对照表

    展开全文
  • Android AOP之字节码插桩

    千次阅读 2017-02-13 19:43:32
    背景  本篇文章基于《网易乐得无埋点数据收集SDK》总结而成,关于网易乐得无埋点数据采集SDK的功能介绍以及技术总结后续会有文章进行阐述,本篇单讲SDK中用到的Android端AOP的实现。  随着流量红利时代过去,...
  • ASM字节码插桩

    2020-02-25 22:32:19
    ASM字节码插桩 前言 热修复的多Dex加载方案中,对于5.0以下的系统存在CLASS_ISPREVERIFIED的问题,而解决这个问题的一个方案是:通过ASM插桩,在类的构造方法里引入一个其它dex里的类,从而避免被打上CLASS_...
  • / 今日科技快讯 /8月19日华为公司19日晚发布媒体声明,称反对美国商务部将另外46家华为实体列入“实体名单”,呼吁美国政府停止对华为的不公正对待,将华为移出“...
  • Android字节码插桩实战

    千次阅读 2017-06-04 16:52:42
    主题 安卓开发 理解本文需要一定的Java汇编...利用Android字节码插桩技术可以很方便地帮助我们实现很多手术刀式的代码设计,如无埋点统计上报、轻量级AOP等。下面我们就通过一次实战,把这门技术真正用起来
  • 字节码编程插桩这种技术常与 Javaagent 技术结合用在系统的非入侵监控中,这样就可以替代在方法中进行硬编码操作。比如,你需要监控一个方法,包括;方法信息、执行耗时、出入参数、执行链路以及异常等。那么就非常...
  • 参考: Android字节码插桩采坑笔记 通过自定义 Gradle 插件修改编译后的 class 文件 ASM官方文档
  • 字节码插桩就是修改节码文件(.class). 如同 gson框架是用来做操json数据结构的,那么asm框架就是用来操作.class文件结构的。 那么这有什么用处呢? 这个是一个很强大而且很高级的功能。我们可能知道反射hook...
  • Android字节码插桩

    2019-12-11 18:49:20
    什么是字节码插桩 字节码插桩就是在构建的过程中,通过修改已经编译完成的字节码文件,也就是class文件,来实现功能的添加。 简单来讲,我们要实现无埋点对客户端的全量统计。这里的统计概括的范围比较广泛,常见的...
  • 经验总是不停刨坑刨出来的,最近结合工作需要并熟悉学习android ASM 字节码插桩的环境下,开发了一个方法 hook 的插件,虽然在各方其他开源项目的参考下,还是刨了不少坑,下面就来记录下。 gradle 知识点记录 –dry...
  • gradle用于构建项目,其plugin插件用于完成一特定功能,而有些时候我们希望在插件中完成对项目内容的一些更改,这就需要我们在gradle构建过程中,获取到源文件才能进行,所幸的是,gradle plugin从1.5.0版本开始,为...
  • 通过自定义gradle插件来实现基于字节码插桩的AOP功能 背景 大家都知道android studio一直以来都是依靠gradle来进行项目构建。那么在构建的过程当中是否提供了aop的切面,来暴露给我们使用呢?答案是肯定的。我们...
  • 师傅:徒儿,听过字节码插桩嘛? 徒儿:师傅,徒儿诗如李白,貌如潘安,字如王羲之,歌如刘德华,智慧如诸葛亮,数学如华罗庚,对于字节码插桩,我怎么能不懂呐。 师傅:徒儿,你这什么时候,练就了吹牛逼吹的都...
  • 利用Android字节码插桩技术可以很方便地帮助我们实现很多手术刀式的代码设计,如无埋点统计上报、轻量级AOP等。下面我们就通过一次实战,把这门技术真正用起来。 奇葩需求 假设有这样一个需求,我们需要在本项目工程...
  • 关于android字节码插桩

    千次阅读 2018-01-30 11:39:57
    基于字节码插桩可以实现面向切面的编程, 实际是在字节码中插入要执行的相关程序. 通过非侵入的方式实现切面编程. (1)AOP和OOP 如果说oop模块化编程, 是把功能封装到一个模块中, 那么aop就是把众多模块中的问题, ...
  • 一、什么是插桩 QQ空间曾经发布的《热修复解决方案》中利用 Javaassist库实现向类的构造函数中插入一段代码解决 CLASS_ISPREVERIFIED...字节码插桩顾名思义就是在我们编写的源码编译成字节码(Class)后,在Andro...
  • Android 字节码插桩

    2019-01-17 11:45:56
    一、为什么要插桩 我们都知道JAVA是面向对象(继承、封装、多态),而插桩的意义在于面向切面(AOP),可想而知单方面的面向对象开发有许多的局限性,而结合面向切面编程可以说补足了我们的这种局限性。举个例子:...

空空如也

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

字节码插桩