LancetX

Project Url: Knight-ZXW/LancetX
Introduction: 饿了么开源的 字节码插桩框架 lancet 的增强版本;修复了一些 Bug,基于 ByteX 提高编译速度,支持插桩功能分组,独立开关配置
More: Author   ReportBugs   
Tags:

LancetX 是一个为 Android 项目设计的字节码插桩框架,其使用方式类似 AspectJ。

该项目核心实现原理参考了 ele 开源的 lancet 字节码插桩框架,与原有的 lancet 的不同点在于 本项目的 plugin 使用字节跳动的 ByteX 进行 class 文件的并行化 以便加快编译速度。 本项目正计划使用 Ai 迁移,以移除 ByteX 依赖

另外项目还并修复了原有项目的一些 BUG,增加了一部分特性,比如提供了 功能分组、单独配置开关的能力。

使用

安装

在项目根目录的 build.gradle 中,引入 ByteX 及 Lancex 插件依赖

buildscript {
    repositories {
        //... 其他 maven 地址
        maven { setUrl("https://artifact.bytedance.com/repository/byteX/") }
    }
    
    dependencies {
      //0.3.0 或其他更高版本
      classpath "com.bytedance.android.byteX:base-plugin:0.3.0"
      classpath "io.github.knight-zxw:lancet-plugin:${lancexVersion}"
    }
}

在 app 目录的 build.gradle 引入 sdk 并配置插件

apply plugin: 'bytex'
ByteX {
    enable true
    enableInDebug true
}


apply plugin: 'LancetX'
LancetX{
    enable true
    enableInDebug true

}
dependencies {
    implementation 'io.github.knight-zxw:lancet-runtime:${lancexVersion}'
}

三分钟示例

LanceX 要求所有的字节码织入定义在申明了 @Weaver 的类中,类名可以随意定义

@Weaver
public class InsertTest{


}

在类中,通过在函数上定义使用不同的注解,如 @ReplaceInvoke @Proxy @Insert 等来定义不同的函数字节码修改行为。

ReplaceInvoke 注解

用户替换函数调用, 既可以替换 普通成员函数调用,也可以替换 静态函数的调用。 比如替换 所有 Log.i 函数 (该函数是一个静态函数)的调用,可以通过如下方式实现

    @ReplaceInvoke(isStatic = true)
    @TargetClass(value = "android.util.Log",scope = Scope.SELF)
    @TargetMethod(methodName = "i")
    public static int replaceLog(String tag,String msg){
        msg = msg + "被替换";
        return Log.e("zxw",msg);
    }

或者替换一个成员函数的调用。 比如有一个 ClassA 其定义如下

public class ClassA {
    public void printMessage(String message){
    }
}

在另一处中有调用 printMessage

ClassA a = new ClassA()
a.printMessage("haha!");

现在希望替换掉 printMessage 的实现, 注意该函数是一个成员函数,我们可以用如下注解实现替换

@Weaver
@Group("replaceInvokeTest")
public class ReplaceInvokeTest {
    @ReplaceInvoke()
    @TargetClass(value = "com.knightboost.lancetx.ClassA",scope = Scope.SELF)
    @TargetMethod(methodName = "printMessage")
    public static void printMessage(ClassA a, String msg){
        msg = msg + "";
        Log.e("ClassA",msg);
    }
}

注意函数的第一个参数表示被替换的类,由于原函数为成员函数,默认将这个对象实例作为第一个函数参数传递过来,其他函数参数为原函数 的参数。 通过该注解,原函数的调用就被替换为

ReplaceInvokeTest.printMessage(a,"haha!");

ReplaceNewInvoke 注解

用于替换 new xx() 指令。 比如在项目中,希望将 所有 new Thread() 的调用替换为 new ProxyThread 的调用可以通过可以注解实现

@ReplaceNewInvoke()
public static void replaceNewThread(Thread t, ProxyThread proxyThread){
}

Insert

@Insert 类似 AspectJ 的 @Around ,可以实现在原函数前后插入代码。 比如我们希望监控 Activity 对象 onCreate 函数的耗时,则可以用以下的定义实现

@Insert(mayCreateSuper = true)
@TargetMethod(methodName = "onCreate")
@TargetClass(value = "android.app.Activity", scope = Scope.LEAF)
public void onCreate2(@Nullable Bundle savedInstanceState) {
    long begin = System.currentTimeMillis();
    Origin.callVoid();
    long end = System.currentTimeMillis();
    Activity activity = ((Activity) This.get());
    Log.e("insertTest", activity + " onCreate cost "+(end-begin)+" ms");
    }

通过 @TargetClass@TargetMethod 表明及约束了对象哪些类的哪些函数进行类修改。@Insert 的 mayCreateSuper 当在目标类未找到目标函数时,是否自动创建该函数被调用父类函数,默认值为 false。 其中 @TargetClass 的 scope 参数,可以实现对目标类的进一步约束,scope 将在其他小节详细介绍,示例中实现效果 是目标类为 android.app.Activity 的所有最终子类。

示例中的 Origin 及 This 是 钩子类,Origin 的相关 API 可以实现对原函数的调用, 而 This 来说, 你可以把它当成 java 中的 this 关键字对待,其表示了被编织类运行时的对象,通过 getFiled()可以获取当前对象的成员变量,通过 putField 可以修改成员变量的值。

Proxy

@Insert在底层的实现是查找 目标类中符合的目标函数实现的,但是对于系统的类,比如 android.util .Log , 并未参与编译流程,这些类最终也不会打包对 APK 中,因此通过 @Insert 的方式无法进行修改。 虽然我们无法修改 Log 类及对应的函数实现,但我们可以修改自身代码(非 JDK、androd SDK )中对这些系统代码的调用。 比如 我们的函数中本来调用了 Log.i()函数,可以修改为我们定义的 LogProxy.i() 函数,在 LogProxy.i()中对原来的函数调用进行切面操作。

@Weaver
public class LogProxy {

    @Proxy()
    @TargetClass(value = "android.util.Log",scope = Scope.SELF)
    @TargetMethod(methodName = "i")
    public static int replaceLogI(String tag,String msg){
        msg = msg + "lancet";
        return (int) Origin.call();
    }
}

API 详解

Insert 注解

类似 AspectJ 的 Around 功能,可以实现对原函数实现切面编程,支持在原函数前后插入新的代码,控制原函数的调用(通过 Origin 钩子)。

Proxy 注解

使用新的函数 替换原有函数的调用, 对于 (JDK/Android SDK)的函数,只能通过 proxy 的方式修改。

TargetClass 注解

表示修改的目标类

Scope

以类的继承体系角度,配置或限定 Insert、Proxy 修改的范围.

  • Scope.SELF 代表仅匹配 value 指定的目标类
  • Scope.DIRECT 代表匹配 value 指定类的直接子类(直接继承于目标类的)
  • Scope.All 代表匹配 value 指定类及其所有子类
  • Scope.ALL_CHILDREN 代表匹配 value 指定类的所有子类
  • Scope.LEAF 代表匹配 value 指定类的最终子类 (即没有任何其他类再继承这个类)

TargetMethod 注解

表示修改的目标函数名称

ClassOf 注解

ClassOf 用于函数参数中, 实现对无法 import 类(私有、包级的)的引用 ClassOf 的 value 一定要按照 **(package_name.)(outer_class_name$)inner_class_name([]...)**的模板. 比如:

  • java.lang.Object
  • java.lang.Integer[][]
  • A[]
  • A$B

Origin

Origin 用来调用原目标方法. 可以被多次调用. Origin.call() 用来调用有返回值的方法. Origin.callVoid() 用来调用没有返回值的方法. 另外,如果你有捕捉异常的需求.可以使用 Origin.call/callThrowOne/callThrowTwo/callThrowThree() Origin.callVoid/callVoidThrowOne/callVoidThrowTwo/callVoidThrowThree() 比如 代理 InputStream 的 read 函数

@TargetClass("java.io.InputStream")
@TargetMethod(methodName="read")
@Proxy()
public int read(byte[] bytes) throws IOException {
    try {
        return (int) Origin.<IOException>callThrowOne();
    } catch (IOException e) {
        e.printStackTrace();
        throw e;
    }
}

This

仅用于Insert 方式的非静态方法的 Hook 中. get() 返回目标方法被调用的实例化对象. 相当于 java 的 this,只不过指向的对象是运行时被修改的那个类的实例对象 **putField & getField** 你可以直接存取目标类的所有属性,无论是 protected or private. 另外,如果这个属性不存在,我们还会自动创建这个属性. Exciting! 自动装箱拆箱肯定也支持了. 一些已知的缺陷:

  • Proxy 不能使用 This
  • 你不能存取你父类的属性. 当你尝试存取父类属性时,我们还是会创建新的属性.

例如:

package com.knightboost.weaver;
public class Main {
    private int a = 1;

    public void nothing(){

    }

    public int getA(){
        return a;
    }
}

@TargetClass("com.knightboost.weaver.Main")
@TargetMethod(methodName="nothing")
@Insert()
public void testThis() {
    Log.e("debug", This.get().getClass().getName());
    This.putField(3, "a");
    Origin.callVoid();
}

一些限制

  1. ReplaceXX 的实现在函数体中 This、Orignal 类及其函数.

功能分组能力

你可能会有对不同的插桩功能进行独立开关控制,而不是全局控制,通过 @Group 注解,你可以为某个 Weaver 类的插桩功能进行分组命名, 在分组之后你可以在 gradle 配置中对这组插桩功能进行单独的开关控制。 动态配置

@Weaver
@Group("insertTest")
public class InsertTest {
}
apply plugin: 'LancetX'
LancetX{
   enable true //插件开关
   enableInDebug //debug 包编译时的插件开关

   weaveGroup{
       //insertTest group 所属的字节码修改功能开关
       insertTest {
           enable true
       }
   }
}

底层实现说明

todo

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools