我们之前聊了那么多性能监控和优化的底层原理,现在来看看一个能将这些理论高效落地的实用工具——Lancet。
Lancet 是滴滴开源的一个轻量级、无侵入的 AOP (面向切面编程) 框架。它通过在编译期动态修改字节码的方式,让你能够在不改变源码的情况下,为已有的方法插入额外的逻辑。这在进行统一的方法耗时统计、日志打印、权限校验等操作时,会变得非常方便。
🎯 Lancet 可以用来做什么?
简单来说,只要是重复性的、非业务核心的代码,都可以考虑用 Lancet 抽离出来。常见的应用场景包括:
- 性能监控:无侵入地统计某个方法(如
onCreate、网络请求)的执行耗时。 - 日志打印:在方法执行前后自动打印参数和返回值,方便调试。
- 自动打点:在特定的页面或事件触发时,自动插入埋点代码。
- 权限校验:在执行某些敏感操作前,自动检查权限。
- 登录校验:在需要登录的方法前,自动判断用户是否已登录。
🚀 Lancet 的基本使用
下面我们通过一个最经典的例子——统计应用中所有 Activity 的 onCreate 方法耗时,来一步步学习 Lancet 的使用。
第一步:集成 Lancet
在你的项目根目录的 build.gradle 文件中,添加 Lancet 插件依赖:
buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:3.6.0' // 确保你的 Gradle 版本兼容
classpath 'me.ele:lancet-plugin:1.0.6' // 添加 Lancet 插件
}
}
然后,在 app 模块的 build.gradle 文件中应用插件:
apply plugin: 'com.android.application'
apply plugin: 'me.ele.lancet' // 应用 Lancet 插件
第二步:创建一个 Hook 类
我们需要创建一个 Java 类,在这个类中定义 切面(Aspect),即告诉 Lancet 我们要对哪个类的哪个方法进行 Hook,以及插入什么代码。
package com.your.package.aspect;
import android.os.Looper;
import android.util.Log;
import com.your.package.BaseActivity; // 假设你的 BaseActivity
// 1. 使用 @Proxy 或 @Insert 注解的类,必须放在一个单独的模块中,或者通过配置让 Lancet 扫描到
// 这里为了演示,我们直接放在 app 模块,但需要配置 sourceSets
@Aspect // 这个注解可有可无,但为了清晰可以加上
public class PerformanceAspect {
// 2. 定义一个静态变量来存储开始时间,考虑到多线程,使用 ThreadLocal
private static final ThreadLocal<Long> TIME_CACHE = new ThreadLocal<>();
// 3. 使用 @Proxy 注解标记一个 Hook 方法
// value: 指定要 Hook 的类的全限定名
// method: 指定要 Hook 的方法名
@Proxy(value = "android.app.Activity", method = "onCreate")
public static void onCreate(Activity activity, Bundle savedInstanceState) {
// 4. 在方法执行前记录时间
long start = System.currentTimeMillis();
TIME_CACHE.set(start);
Log.d("LancetDemo", "onCreate start at " + start);
// 5. 调用原方法,这是 Lancet 自动生成的,名称固定为 target
target(activity, savedInstanceState);
// 6. 在方法执行后计算耗时
long cost = System.currentTimeMillis() - TIME_CACHE.get();
Log.d("LancetDemo", activity.getClass().getSimpleName() + " onCreate cost: " + cost + "ms");
}
}
代码解读:
@Proxy:这是 Lancet 最常用的注解之一。它的作用是完全替代原方法,但在我们的替代方法中,我们可以自由地在调用原方法(通过target())的前后插入代码。- 方法签名:
onCreate(Activity activity, Bundle savedInstanceState)。这里有一个关键点:被 Hook 的方法是onCreate(Bundle),但我们的方法签名写的是onCreate(Activity activity, Bundle savedInstanceState)。这是因为 Lancet 要求你的 Hook 方法的第一个参数必须是持有该方法的实例对象(即this),然后才是原方法的参数列表。 target():这是一个由 Lancet 自动生成的静态方法,用来调用原方法。它的参数就是 Hook 方法中除了第一个this参数以外的其他参数。
第三步:让 Lancet 认识我们的 Hook 类
Lancet 通过扫描字节码来寻找被 @Proxy 或 @Insert 注解标记的类。默认情况下,它不会扫描你的 app 模块,因为 app 模块的代码会被多次编译,可能导致问题。最佳实践是将所有 Hook 类放在一个独立的 Java 模块中,然后在 app 模块中依赖它。
如果你只是想快速测试,也可以在 app 的 build.gradle 中临时添加以下配置,让 Lancet 扫描你的包:
android {
// ...
defaultConfig {
// ...
javaCompileOptions {
annotationProcessorOptions {
arguments = [ 'lancet.scan.packages': 'com.your.package.aspect' ] // 指定你的切面包名
}
}
}
}
🔍 @Proxy 与 @Insert 的区别
Lancet 主要提供了两种注解,理解它们的区别很重要:
| 特性 | @Proxy (代理) | @Insert (插入) |
|---|---|---|
| 行为 | 完全取代原方法,然后在你的方法中通过 target() 调用原逻辑。 | 将你的代码插入到原方法的开头或结尾,不改变原方法结构。 |
| 调用原方法 | 必须显式调用 target()。 | 无需调用,原方法会正常运行。 |
| 适用场景 | 需要完全控制方法执行流程,例如修改参数、返回值,或执行耗时操作前后统计。 | 只需在原方法前后添加辅助代码,不影响核心逻辑,例如无侵入的日志打印、权限校验。 |
| 注解参数 | @Proxy(value = "类名", method = "方法名") | @Insert(value = "类名", method = "方法名"),还可选 mayCreateSuper 等参数。 |
💡 更多用法与注意事项
- 返回值处理:如果你的 Hook 方法有返回值,你可以在
target()调用后,对返回值进行修改再返回。 - 构造方法 Hook:Lancet 也可以 Hook 构造方法,只需将
method参数指定为"<init>"即可。 - 处理继承:Lancet 可以 Hook 父类的方法,也可以配置是否 Hook 子类,这需要用到
@Proxy或@Insert的scope参数。 - 插件兼容性:Lancet 对 Android Gradle 插件的版本有一定要求,建议在使用前查阅其官方文档确认兼容性。
Lancet 是一个非常强大的工具,它把复杂的字节码操作封装成了简单的注解。你可以用它来实现很多之前需要手动埋点或者写很多重复代码的功能,极大地提高开发效率和代码的可维护性。