Android字节码插桩

什么是字节码插桩

字节码插桩就是在构建的过程中,通过修改已经编译完成的字节码文件,也就是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 (l
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值