LancetX 是一个为Android项目设计的字节码插桩框架,其使用方式类似AspectJ。
该项目核心实现原理参考了 ele开源的 lancet 字节码插桩框架,与原有的lancet的不同点在于 本项目的plugin使用字节跳动的ByteX进行 class 文件的并行化 以便加快编译速度。
另外项目还并修复了原有项目的一些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 等来定义不同的函数字节码修改行为。
用户替换函数调用, 既可以替换 普通成员函数调用,也可以替换 静态函数的调用。 比如替换 所有 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!");
用于替换 new xx() 指令。 比如在项目中,希望将 所有 new Thread() 的调用替换为 new ProxyThread的调用可以通过可以注解实现
@ReplaceNewInvoke()
public static void replaceNewThread(Thread t, ProxyThread proxyThread){
}
@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 可以修改成员变量的值。
@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();
}
}
类似AspectJ的Around功能,可以实现对原函数实现切面编程,支持在原函数前后插入新的代码,控制原函数的调用(通过Origin钩子)。
使用新的函数 替换原有函数的调用, 对于 (JDK/Android SDK)的函数,只能通过proxy的方式修改。
表示修改的目标类
以类的继承体系角度,配置或限定 Insert、Proxy 修改的范围.
- Scope.SELF 代表仅匹配
value
指定的目标类 - Scope.DIRECT 代表匹配
value
指定类的直接子类(直接继承于目标类的) - Scope.All 代表匹配
value
指定类及其所有子类 - Scope.ALL_CHILDREN 代表匹配
value
指定类的所有子类 - Scope.LEAF 代表匹配
value
指定类的最终子类 (即没有任何其他类再继承这个类)
表示修改的目标函数名称
ClassOf
用于函数参数中, 实现对无法import类(私有、包级的)的引用
ClassOf
的 value 一定要按照 **(package_name.)(outer_class_name$)inner_class_name([]...)**
的模板.
比如:
- java.lang.Object
- java.lang.Integer[][]
- A[]
- A$B
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;
}
}
仅用于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();
}
- 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