调试研究Shadow对字节码编辑的正确姿势

7,735 阅读5分钟

Shadow是通过字节码编辑技术向插件插入中间层,完成插件技术的核心工作的。所以,有必要给新接触字节码编辑技术的同学分享一下研究这项技术的入门姿势。

构建过程介绍

Android 官方的构建过程提供了名为TransForm的API,详见这里 。这个API允许第三方插件在class转换成dex之前编辑class。Shadow就写了这样一个第三方API,就是在插件的build.gradle中添加的apply plugin: 'com.tencent.shadow.plugin'

关于Transform,要注意,一次构建允许有多个Transform。但是构建系统不保证这些Transform的顺序。就是多个Transform谁先执行,谁后执行,你在设计Transform时是不能假定的。Shadow的Transform会修改一些类的父类,或者一些类的名字。如果其他Transform试图以类型或名字查找类的话,那它在Shadow的Transform前执行和之后执行的效果就不一样了。对于这种情况,建议修改Shadow的Transform,将两个Transform写在同一个Plugin中,这样在Plugin内部就可以控制顺序了。

插件应用了Shadow的Plugin后,在构建过程中就会调用到com.tencent.shadow.core.gradle.ShadowPlugin#apply。它是通过projects/sdk/core/gradle-plugin/build.gradle中的这段配置找到目标类的。

gradlePlugin {
    plugins {
        shadow {
            id = "com.tencent.shadow.plugin"
            implementationClass = "com.tencent.shadow.core.gradle.ShadowPlugin"
        }
    }
}

apply中,我们通过这段代码向构建系统注册了我们的Transform程序。

plugin.extension.registerTransform(ShadowTransform(...))

会执行一个Transform任务,比如transformClassesWithShadowTransformForDebug。这个任务会调用ShadowTransform对象的父类方法com.tencent.shadow.core.transform_kit.ClassTransform#transform,进入我们Transform的真正入口

Gradle任务的增量构建检测机制只检查任务的输入和输出是否发生了变化。这在一般情况下显然是合理的。输入没变,输出都在,肯定是不用重新执行任务的。但是在Shadow的开发中,Transform这个任务也是我们源码的一部分。因此在这种默认机制下,插件原始字节码作为输入,插件编辑后字节码作为输出,如果只修改Transform的逻辑,比如将类A重命名为B的逻辑改成A重命名为C。由于输入没变,输出都在,这个更新的Transform逻辑就不会执行了。因此在com.tencent.shadow.core.transform_kit.ClassTransform#getSecondaryFiles中,我们将Transform程序本身也做为了Transform程序的附带输入。这样Transform任务就能判断出输入因为Transform程序本身变化而变化了,因而重新执行Transform任务。这样我才达到了编辑Transform源码,也能一键运行的效果。

查看Transform前后的插件字节码

我们在开发中把sample-plugin做成一个aar库,然后分别包装成正常安装的sample-normal-app和插件sample-plugin-app,就是为了能比较方便的同时查看应用Transform前后的字节码差异。

在Android Studio中直接双击打开一个apk文件(下图中1),然后在详情中选中dex文件(下图中2),就能显示出dex中的类。点击下图中3指向的按钮,隐藏掉没有真正打包在当前dex中的类。然后选择一个类(下图4),按右键,选择Show Bytecode

比如查看正常安装的apk的字节码,如下:

.class public Lcom/tencent/shadow/sample/plugin/app/lib/usecases/activity/TestActivityReCreate;
.super Landroid/app/Activity;
.source "TestActivityReCreate.java"

# direct methods
.method public constructor <init>()V
    .registers 1

    .line 31
    invoke-direct {p0}, Landroid/app/Activity;-><init>()V

    return-void
.end method

再查看插件apk中相同类的字节码,如下:

.class public Lcom/tencent/shadow/sample/plugin/app/lib/usecases/activity/TestActivityReCreate;
.super Lcom/tencent/shadow/core/runtime/ShadowActivity;
.source "TestActivityReCreate.java"

# direct methods
.method public constructor <init>()V
    .registers 1

    .line 31
    invoke-direct {p0}, Lcom/tencent/shadow/core/runtime/ShadowActivity;-><init>()V

    return-void
.end method

可以对比出来,第二行的.super发生了变化,表示这个类的父类被改变了。下面方法中的invoke-direct调用的方法也变化了。

修改Transform程序

比如前面例子中涉及到的系统Activity替换成ShadowActivity,就是在类com.tencent.shadow.core.transform.specific.ActivityTransform中实现的。大家可以直接修改这个类的源码,然后重新运行sample-host就可以看到效果了。

Shadow对所有字节码的修改逻辑都放在了com.tencent.shadow.core.transform.specific包中。

Shadow的transform-kit是Shadow在做字节码编辑工作时沉淀的通用代码,应该可以直接用在Android上进行任何字节码编辑工作。比如直接接入业务,通过实现SpecificTransform进行AOP编程。

transform-kit的设计

transform-kit的设计确实没有久经考验。希望大家用起来之后能够开源共建的改进它。

它目前主要解决了这样几个问题。

  1. ClassTransform解决如何将App自己的类和依赖jar包,输入到内存中待编辑,然后在编辑后再输出到文件。
  2. JavassistTransform解决如何将ClassTransform和Javassist联系起来。
  3. AbstractTransform负责组织Transform的抽象过程,就是例如先setup后fire这样的顺序,在这里固定下来。
  4. AbstractTransformManagerSpecificTransformTransformStep,联合起来组织各个平行不相关的Transform。规定每个SpecificTransform由多个TransformStep构成。先配置Step,再统一执行。这个设计是为了能让每个Transform串行工作,每个Transform工作时都能处理所有的类。而不是每个类按顺序经过所有的Transform,Shadow最早的代码就犯了这个错误。每个类去做完所有Transform,可能再下一个类引用其他类时出现其他类已经处理过或者没有处理过两种情况。
  5. AndroidClassPoolBuilder完成如何将Android SDK的类导入Javassist中使用。

先分享这么多,相信一部分人了解这些就可以通过查阅Javassist的文档学习Javassist的用法,然后查看com.tencent.shadow.core.transform.specific包中已有的代码,就能彻底学会如何扩展Shadow的功能,也能利用Shadow的transform-kit在其他应用中进行AOP编程了。