Android SDK:TraceHook和BlockMonitor的设计

Android SDK:TraceHook和BlockMonitor的设计

做性能工具,最难的不是分析------是怎么把数据采上来。

SmartInspector 的 Android SDK 层(tracelib)是整个系统的数据源头。CLI 端的 Python 分析、LangGraph 的 Agent 调度、最终的归因报告,全都依赖这一层采集到的 trace 数据。如果 SDK 采不到、采不准、采漏了,后面所有分析都是空中楼阁。

SDK 层的核心是两个类:TraceHook 负责用 Pine AOP 拦截各种方法调用并写入 atrace section,BlockMonitor 负责检测主线程卡顿并抓取阻塞调用栈。两个类加起来不到 1000 行代码,但处理了 Android 运行时环境里各种刁钻的边界情况。

TraceHook 的设计哲学:一切皆可配置

TraceHook 不是一个"装上就跑"的探针。它更像一个工具箱------里面有十几种 Hook 能力,按需开启,动态组合。

Hook 注册的生命周期管理

整个初始化入口只有一个方法:

java 复制代码
public static void init(Context context) {
    if (initialized) return;
    synchronized (TraceHook.class) {
        if (initialized) return;
        HookConfigManager.init(context);
        doInit();
        initialized = true;
        wsClient = new SIClient(context);
        wsClient.connect();
    }
}

双重检查锁保证只初始化一次。初始化流程分三步:加载配置 → 安装 Hook → 启动 WebSocket 客户端。

doInit() 是核心,遍历所有 Hook 类别,逐个检查开关:

java 复制代码
private static void doInit() {
    PineConfig.debug = BuildConfig.DEBUG;
    PineConfig.debuggable = false;

    if (HookConfigManager.isEnabled("activity_lifecycle")) {
        hookActivityLifecycle();
    }
    if (HookConfigManager.isEnabled("fragment_lifecycle")) {
        hookFragmentLifecycle();
    }
    if (HookConfigManager.isEnabled("rv_pipeline") || HookConfigManager.isEnabled("rv_adapter")) {
        hookRecyclerView();
    }
    // ... 其他 Hook 类别 ...
}

每个 Hook 类别对应一个独立的开关,存在 SharedPreferences 里,通过 WebSocket 实时同步 CLI 端的配置变更。这意味着你不需要重新打包 App,在 CLI 里 /config 一下就能开关某个 Hook。

13 种 Hook 类别

TraceHook 目前注册了 13 种 Hook 类别,覆盖了 Android 应用的主要性能瓶颈点:

Hook 类别 拦截目标 Tag 前缀
activity_lifecycle Activity 六个生命周期 + windowFocusChanged SI$
fragment_lifecycle Fragment(AndroidX + android.app)生命周期 SI$
rv_pipeline RecyclerView dispatchLayoutStep 1/2/3、onDraw SI$
rv_adapter Adapter 的 create/bind/recycle SI$
layout_inflate LayoutInflater.inflate SI$inflate#
view_traverse View.measure/layout/draw(排除 RV 和系统控件) SI$view#
handler_dispatch Handler.dispatchMessage(仅主线程) SI$handler#
block_monitor 主线程卡顿检测 SI$block#
network_io OkHttp + HttpURLConnection SI$net#
database_io SQLiteDatabase + Room SI$db#
image_load Glide + Coil SI$img#
input_event Activity.dispatchTouchEvent SI$touch#
compose_tracking Compose Runtime 重组追踪 SI$compose#

这些 Hook 不是全部同时开启的。默认只开启高频场景(Activity/Fragment/RV/Block),其他的按需打开。原因很简单------每个 Hook 都有性能开销,虽然单个 Hook 一次调用的开销在微秒级别,但像 view_traverse 这种 Hook,一次 doFrame 可能触发几百次 measure/layout/draw,累积下来就不容忽视了。

SI$ 前缀:为什么所有 tag 都带这个前缀

所有 atrace section name 都以 SI$ 开头。这不是装逼,是工程刚需。

Perfetto 采集的 atrace 数据里,除了 SmartInspector 写入的 section,还有系统自带的(比如 Choreographer#doFrameRecyclerView)、还有其他三方库写入的。Python 分析端需要一个可靠的过滤条件来区分"我们关心的"和"系统自带的"。

SI$ 就是这个过滤器。Python 端解析 trace 时,只处理 SI$ 开头的 slice,其他的全部忽略。

Pine AOP:动态 Hook 的底层机制

所有 Hook 底层都依赖 Pine 框架。Pine 是一个 Android 平台的 AOP 框架,能在运行时拦截任意 Java 方法的调用。

通用 Hook 模式

TraceHook 里大量使用了一个通用的 Hook 模式------hookConcrete + safeHookMethod

java 复制代码
private static void hookConcrete(Class<?> clazz, String methodName, Class<?>[] paramTypes) {
    Method m = clazz.getDeclaredMethod(methodName, paramTypes);
    Pine.hook(m, new MethodHook() {
        @Override
        public void beforeCall(Pine.CallFrame cf) {
            String tag = autoTag(cf, methodName);
            if (!enterTrace()) return;
            Trace.beginSection(tag);
        }

        @Override
        public void afterCall(Pine.CallFrame cf) {
            exitTrace();
        }
    });
}

每个 Hook 只做两件事:进入方法时 Trace.beginSection(),退出方法时 Trace.endSection()。这会在 Perfetto 的 atrace 数据里产生一个精确的耗时 slice。

动态发现:RecyclerView 的 Hook 模式

有些类在编译时是不知道的。比如 RecyclerView 的 Adapter------你写的是 MyAdapter extends RecyclerView.Adapter,但 MyAdapter 这个类在 SDK 初始化时还不存在。

TraceHook 的解法是"按需发现":Hook RecyclerView.setAdapter(),当 Adapter 被设置时,动态 Hook 这个具体的 Adapter 子类:

java 复制代码
Method setAdapter = rvClass.getDeclaredMethod("setAdapter", adapterClass);
Pine.hook(setAdapter, new MethodHook() {
    @Override
    public void afterCall(Pine.CallFrame cf) {
        Object adapter = cf.args[0];
        if (adapter != null) hookConcreteAdapter(adapter.getClass(), vhClass);
    }
});

同一个 HashSet 去重,确保每个 Adapter 类只 Hook 一次:

java 复制代码
private static final Set<Class<?>> hookedAdapters = new HashSet<>();

private static void hookConcreteAdapter(Class<?> adapter, Class<?> vhClass) {
    synchronized (hookedAdapters) {
        if (hookedAdapters.contains(adapter)) return;
        hookedAdapters.add(adapter);
    }
    // Hook onCreateViewHolder, onBindViewHolder, onViewRecycled ...
}

Fragment 也用了同样的模式------通过 FragmentLifecycleCallbacks 动态发现具体的 Fragment 子类,然后 Hook 其生命周期方法。

atrace 的 127 字节限制

Trace.beginSection() 的 section name 有个硬性限制:不能超过 127 字节

一个完整的 tag 像这样:SI$com.smartinspector.hook.worker.CpuBurnWorker.run------轻轻松松就超过 127 字节了。所以 TraceHook 里有个 shortenFqn() 方法,把完整的类名压缩为保留最后两个包名段:

java 复制代码
private static String shortenFqn(String fqn) {
    if (fqn == null || fqn.length() <= 50) return fqn;
    String outer = fqn;
    String inner = "";
    int dollar = fqn.indexOf('$');
    if (dollar >= 0) {
        outer = fqn.substring(0, dollar);
        inner = fqn.substring(dollar);
    }
    int lastDot = outer.lastIndexOf('.');
    int prevDot = outer.lastIndexOf('.', lastDot - 1);
    return outer.substring(prevDot + 1) + inner;
}

com.smartinspector.hook.worker.CpuBurnWorker$1worker.CpuBurnWorker$1。Python 端的归因模块会根据这个缩短后的类名反向搜索项目源码。

trace 嵌套深度保护

atrace 有个隐含的限制:最多支持 16 层嵌套 section 。超过的 beginSection() 调用会被直接丢弃,导致 endSection()beginSection() 不匹配,后续的 section 全部乱掉。

对于一个"万物皆 Hook"的 SDK 来说,这个问题很现实。一个典型的调用链可能是:

scss 复制代码
Activity.onResume
  → Fragment.onResume
    → RecyclerView.onDraw
      → Adapter.onBindViewHolder
        → ImageView.setImageDrawable (image_load hook)
          → SQLiteDatabase.query (database_io hook)

6 层嵌套,看起来还好。但如果 Adapter 的 onBindViewHolder 里触发了多次 IO,或者 View 的 measure 递归层级很深,很容易就突破 16 层。

TraceHook 用 ThreadLocal<Integer> 做了深度保护:

java 复制代码
private static final int MAX_TRACE_DEPTH = 10;
private static final ThreadLocal<Integer> traceDepth = ThreadLocal.withInitial(() -> 0);

private static boolean enterTrace() {
    int depth = traceDepth.get();
    if (depth >= MAX_TRACE_DEPTH) return false;
    traceDepth.set(depth + 1);
    return true;
}

private static void exitTrace() {
    int depth = traceDepth.get();
    if (depth > 0) traceDepth.set(depth - 1);
    Trace.endSection();
}

设为 10 层而不是 16 层,留了 6 层的余量给系统和三方库。当深度超过 10 时,enterTrace() 返回 false,这次 beginSection() 直接跳过,但对应的 exitTrace() 也会跳过(因为没进入),所以不会出现不匹配。

这里用 ThreadLocal 是必须的------IO Hook(SI$net#SI$db#)可能在工作线程执行,和主线程的 Hook 并发运行,深度计数不能共享。

BlockMonitor:主线程卡顿检测

TraceHook 解决了"哪些方法在执行"的问题,但没有回答"主线程卡在哪里"。一个方法执行了 200ms,在 trace 里就是一个 200ms 的 slice,你需要手动看调用栈才能判断"这是卡顿还是正常的初始化耗时"。

BlockMonitor 自动做这件事。

核心原理:Looper 消息耗时监控

BlockMonitor 的思路和 BlockCanary 一样------监控主线程 Looper 的每一条 Message 处理耗时。Android 的 Looper.loop() 是一个死循环,每次循环分发一条 Message:

java 复制代码
// Looper.loop() 简化
for (;;) {
    Message msg = queue.next();
    // >>>>> Dispatching to xxx
    msg.target.dispatchMessage(msg);
    // <<<<< Finished to xxx
}

只要在 dispatch 前后打时间戳,就能知道每条 Message 的处理耗时。

双策略:Observer vs Printer

Android 10(API 29)引入了 Looper.Observer,能直接监听 Message 的分发事件。这是官方推荐的方式,零字符串分配开销。

Observer@hide API,不能直接调用。BlockMonitor 通过反射 + 动态代理来使用它:

java 复制代码
private static void startWithObserver() {
    Class<?> observerClass = Class.forName("android.os.Looper$Observer");
    
    Object proxy = Proxy.newProxyInstance(
        observerClass.getClassLoader(),
        new Class<?>[]{ observerClass },
        new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) {
                String name = method.getName();
                if ("messageDispatchStarting".equals(name)) {
                    onDispatchStart();
                } else if ("messageDispatched".equals(name)) {
                    // 从 args[1] 拿到 Message 对象,提取 callback 类名
                    onDispatchEnd(msgClass);
                }
                return null;
            }
        });
    
    Method setObserver = Looper.class.getDeclaredMethod("setObserver", observerClass);
    setObserver.setAccessible(true);
    setObserver.invoke(Looper.getMainLooper(), proxy);
}

Android 9 及以下走经典路线------Looper.setMessageLogging(Printer)。Looper 在每条 Message 分发前后会调用 Printer.println(),打印 >>>>> Dispatching to<<<<< Finished to 开头的日志。通过解析这个字符串就能拿到 Message 的信息。

Observer 策略的优势在于:不需要解析字符串,不需要创建 Printer 对象,性能开销几乎为零。而且能直接拿到 Message 对象,精确获取 callback 的类名。

延迟抓栈:捕获真正的阻塞调用

这是 BlockMonitor 设计里最巧妙的部分。

检测到卡顿容易,但要拿到"卡在哪里"的调用栈就难了。你可能在想:在 onDispatchEnd() 里拿一下 Thread.currentThread().getStackTrace() 不就行了?

不行。因为到了 onDispatchEnd(),Message 已经处理完了,调用栈已经回退了。这时候拿到的栈是 Looper.loop() 的栈,不是阻塞方法的栈。

正确做法是:在主线程阻塞的时候,从另一个线程抓它的栈

BlockMonitor 的实现:

  1. 每条 Message 开始分发时,往一个后台 HandlerThread 投递一个延迟 Runnable,延迟时间就是阈值(默认 100ms)
  2. 如果 Message 在 100ms 内处理完,取消这个 Runnable
  3. 如果 100ms 到了,Runnable 触发------此时主线程还在处理这条 Message,从后台线程抓取主线程的调用栈
java 复制代码
private static void scheduleWatchdog() {
    capturedStack = null;
    pendingWatchdog = new Runnable() {
        @Override
        public void run() {
            // 在 watchdog 线程执行,此时主线程还在阻塞
            Thread mainThread = Looper.getMainLooper().getThread();
            StackTraceElement[] stack = mainThread.getStackTrace();
            capturedStack = formatStack(stack, 25);
        }
    };
    watchdogHandler.postDelayed(pendingWatchdog, thresholdMs);
}

抓到的栈经过过滤,只保留用户代码的帧(过滤掉 android.*java.*kotlin.* 等系统前缀),最多保留 25 帧。

事件输出:三管齐下

一次卡顿被检测到后,BlockMonitor 做三件事:

1. 写 atrace section ------SI$block#MsgClass#200ms,在 Perfetto trace 里可见

2. 写 logcat ------以 SIBlock 为 tag,格式是 MsgClass|200ms|stacktrace,Perfetto 的 logcat 采集能抓到

3. 缓存结构化事件 ------BlockEvent 对象存入内存 buffer,CLI 通过 WebSocket 拉取

java 复制代码
private static void onDispatchEnd(String msgClass) {
    long elapsedMs = (System.nanoTime() - dispatchStartNs) / 1_000_000;
    
    if (elapsedMs < thresholdMs) {
        cancelWatchdog();
        return;
    }
    
    // 1. atrace
    Trace.beginSection(SI_PREFIX + "block#" + shortClass + "#" + elapsedMs + "ms");
    Trace.endSection();
    
    // 2. logcat
    Log.w(LOG_TAG, msgClass + "|" + elapsedMs + "ms|" + capturedStack);
    
    // 3. 缓存
    blockEvents.add(new BlockEvent(msgClass, elapsedMs, frames));
}

三种输出通道各有用途:atrace 用于 trace 时间线可视化,logcat 用于快速定位,WebSocket 用于 CLI 端的结构化分析。

buffer 有防 OOM 保护------超过 500 条就清理最老的 100 条。对于一个分析会话(10 秒 trace)来说,500 条卡顿事件绰绰有余。

Release 变体:零开销的 No-op Stub

以上所有的 Hook 和监控逻辑,只在 debug 构建里存在。

tracelib 模块用了 Android 的 build variant 机制,在 src/release/ 目录下放了同名的 no-op 类:

java 复制代码
// src/release/java/com/smartinspector/tracelib/TraceHook.java
public class TraceHook {
    private TraceHook() {}
    public static void init() {}
    public static void init(Context context) {}
    public static SIClient getWsClient() { return null; }
}

// src/release/java/com/smartinspector/tracelib/BlockMonitor.java
public class BlockMonitor {
    public static void start(long thresholdMs) {}
    public static void stop() {}
    public static synchronized String getAndClearEventsJson() { return "[]"; }
}

Release 包里,TraceHook.init() 调用的是一个空方法。编译器会在内联阶段把它优化掉------连方法调用的开销都没有。Pine 框架的依赖也不会打进 release 包(build.gradle 里 releaseImplementation 是空的)。

这是性能工具 SDK 的标准做法:debug 有能力,release 零开销。不需要通过 Proguard 混淆或条件编译,直接靠源码级别的替换就行。

小结

TraceHook 和 BlockMonitor 的设计,本质上是在回答一个问题:怎样在不侵入业务代码的前提下,尽可能多地采集性能数据,同时不把 App 搞卡

答案分几个层面:

  • 按需开启:13 种 Hook 各自独立,默认只开最必要的,其他的通过配置动态打开
  • 深度保护:atrace 嵌套超过 10 层自动跳过,防止溢出
  • 延迟抓栈:从后台线程抓取主线程的真实阻塞调用栈
  • Release 零开销:build variant 机制保证 release 包里一行多余代码都没有

整个 SDK 层大概 1500 行代码,但处理了 Android 运行时的各种边界条件------atrace section 的 127 字节限制、Looper Observer 的 hide API、Fragment 的动态发现、trace 嵌套溢出、卡顿抓栈的时序问题。这些都是实打实踩过的坑。

相关推荐
RoboWizard3 小时前
DIY移动硬盘?2230能否堪大任!
数据库·人工智能·智能手机·性能优化·负载均衡
YYYing.3 小时前
【C++项目之高并发内存池 (五)】一些小细节和性能优化及整体测试
c++·性能优化·高并发·内存池·基数树
Shota Kishi1 天前
深入解析 Solana RPC getTransaction 性能优化:以最近 30 个 epoch 为重点的历史交易检索提速实践
网络协议·性能优化·rpc
安畅检测齐鲁物联网测试中心1 天前
信创软件性能优化测试三步法
性能优化·测试方法·数据库调优·硬件适配·信创软件
前进的李工2 天前
MySQL慢查询日志优化实战
数据库·mysql·性能优化
天若有情6732 天前
前端高阶性能优化:跳出传统懒加载与预加载,基于用户行为做轻量预判加载
前端·性能优化
MU在掘金916952 天前
布局分析:检测XML嵌套过深
性能优化
Highcharts.js2 天前
Highcharts React 性能优化指南|使用 useMemo 渲染五万数据点不卡顿
react.js·性能优化·react hooks·highcharts·usememo·大数据渲染·前端性能
故事还在继续吗2 天前
嵌入式 C 语言程序性能优化
c语言·开发语言·性能优化