7、Systrace进阶:自定义Trace、Async Trace、CPU调度分析、帧率优化

好,咱们接着聊Systrace。上一章我们把Systrace的基础操作和常用工具过了一遍。说实话,那些只是入门。真正到了线上问题、性能优化的时候,你会发现默认的Systrace信息根本不够用。为什么?因为系统只给你打了它认为重要的点,而你的业务逻辑、你的自定义线程、你的异步操作,它一概不知。

所以这一章,我们来点硬核的。我会带你看看怎么往Systrace里“塞”你自己的标记,怎么分析CPU调度这种底层行为,以及如何用这些手段去优化帧率。嗯,这些都是我在项目里反复踩坑后总结出来的经验。

7.1 自定义Trace:给你的代码打上标记

先问个问题:你写了一个复杂的渲染流程,或者一个耗时的网络请求。你想知道它在整个系统时间线上到底占了多少时间,跟其他进程的GC、Binder调用有没有重叠。默认的Systrace看不到这些,对吧?

这时候就需要自定义Trace了。说白了,就是在你的代码里插入开始和结束的标记,让Systrace能识别出来。

7.1.1 Java层的Trace API

Android SDK提供了android.os.Trace类。用法很简单:

// 开始一个名为"myExpensiveOperation"的trace块
Trace.beginSection("myExpensiveOperation");
try {
    // 你的耗时操作
    Thread.sleep(200);
} finally {
    // 一定要在finally里结束,防止异常导致trace不配对
    Trace.endSection();
}

我个人习惯在每一个可能成为瓶颈的函数入口都加上这个。注意,beginSectionendSection必须成对出现,而且不能嵌套错误。我曾经见过一个同事,在try块里调用了两次beginSection,但只调用了一次endSection,结果整个Systrace的时间线都乱了,排查了半天。

警告:Trace API有数量限制。系统最多支持8个并发的trace块。如果你嵌套太多,后面的会被忽略。所以别滥用,只标记关键路径。

7.1.2 Native层的Trace API

如果你在写NDK代码,或者分析SurfaceFlinger这种Native服务,可以用ATrace宏:

#include <cutils/trace.h>

ATRACE_CALL();
// 或者手动控制
ATRACE_BEGIN("native_render");
// 渲染逻辑
ATRACE_END();

ATRACE_CALL()这个宏特别方便,它会自动用函数名作为trace名称,并且在函数退出时自动结束。我在分析SurfaceFlinger的合成耗时的时候,就靠这个宏把每个合成步骤都打了出来,最后定位到一个GPU等待的瓶颈。

7.2 Async Trace:追踪异步操作

好,自定义Trace能解决同步调用的问题。但现实世界里,大量操作是异步的。比如网络请求的回调、Handler的post、动画的Choreographer回调。这些操作开始和结束不在同一个线程,甚至不在同一个时间片里。普通的beginSection/endSection就无能为力了。

这时候需要Async Trace。它允许你给一个异步操作分配一个唯一的ID,然后在不同的地方标记它的开始和结束。

7.2.1 使用Async Trace API

// 在发起异步任务的地方
Trace.beginAsyncSection("image_load", 12345);

// 在异步任务真正开始执行的地方(可能在另一个线程)
// 可以什么都不做,或者也打一个标记

// 在异步任务完成的地方
Trace.endAsyncSection("image_load", 12345);

注意,这里的第二个参数是一个整数ID,用来区分同一个名称下的不同实例。比如你同时加载10张图片,每张图片的ID不同,Systrace就能分别显示它们的生命周期。

我记得有一次,我们App的首页图片加载特别慢。用普通的Systrace看,只看到主线程在空闲,但图片就是出不来。后来我用Async Trace把图片从网络请求到解码到显示的全链路都标记出来,才发现问题出在解码线程池的排队上——线程池太小,前面的图片解码卡住了后面的。

技巧:Async Trace在Systrace UI中显示为一条条横向的“长条”,颜色和普通trace块不同。你可以用鼠标悬停查看它的开始时间和持续时间。如果发现某个异步操作持续时间异常长,那基本就是性能瓶颈了。

7.3 CPU调度分析:看看你的线程在干嘛

好了,标记打上了,异步操作也追踪了。但有时候你会发现,你的代码明明没执行多久,但帧率就是上不去。为什么?因为CPU调度器可能把你的线程“晾”在一边了。

Systrace的CPU调度视图,说白了就是告诉你每个CPU核心在每一毫秒在运行哪个线程。这是分析性能问题的终极武器之一。

7.3.1 如何看懂CPU调度图

在Systrace结果中,你会看到类似这样的信息:

  • CPU 0-7的行:每一行代表一个CPU核心。上面有各种颜色的方块,每个方块代表一个线程在运行。
  • 方块的颜色:不同进程的线程颜色不同。你可以通过图例来识别。
  • 方块之间的空隙:代表CPU空闲,或者正在执行切换(上下文切换)。
  • 线程状态:点击一个方块,可以看到该线程当时的运行状态(Running、Runnable、Sleeping等)。

我重点看的是“Runnable”状态。如果一个线程长时间处于Runnable但没被调度到CPU上,说明CPU资源不够,或者有更高优先级的线程在抢占。说白了,就是你的线程在排队等CPU。

7.3.2 常见的CPU调度问题

现象 可能原因 解决方案
主线程频繁出现Runnable但未运行 有其他高优先级线程(如Binder线程、后台任务)抢占了CPU 降低后台线程优先级;使用CPU亲和性(affinity)将后台任务绑定到小核
渲染线程(RenderThread)被频繁抢占 CPU频率太低,或者有中断处理 检查温控策略;使用性能内核(big core)运行渲染线程
多个线程同时争抢同一个CPU核心 线程数超过CPU核心数,或者调度器负载均衡不佳 优化线程池大小;使用sched_setaffinity手动分配核心

我曾经在一个游戏项目中,发现渲染线程每隔几百毫秒就出现一次长达10ms的Runnable等待。用Systrace一看,原来是系统的“kswapd”进程(内存回收)在那个时候抢占了CPU。后来我们通过调整内存分配策略,减少了内存压力,这个问题就消失了。

7.4 帧率优化:从Systrace到实战

好了,理论说完了,我们来点实际的。怎么用Systrace去优化帧率?我总结了一个“三步法”。

7.4.1 第一步:找到掉帧点

打开Systrace,勾选“Frame”相关的选项(比如SurfaceFlingerChoreographer)。你会看到一条条绿色的竖线,代表每一帧。如果某帧的竖线变成了红色或黄色,说明这一帧超时了(超过16.6ms)。

点击这个红色的竖线,Systrace会自动帮你定位到这一帧的时间范围。然后你放大看,看看这一帧里主线程和渲染线程都在干什么。

7.4.2 第二步:分析卡顿原因

常见的卡顿原因有这几类:

  • 主线程耗时操作:比如在onDraw里做了Bitmap解码、文件IO、或者复杂的布局计算。Systrace里会显示一个很长的performTraversals块。
  • 渲染线程等待:比如等待GPU完成上一帧的渲染。Systrace里会显示waitForFence或者dequeueBuffer耗时过长。
  • Binder调用阻塞:比如主线程调用了Binder.transact(),而对方进程响应慢。Systrace里会显示binder transaction
  • CPU调度延迟:就是我们上面说的,线程在Runnable状态等待CPU。

我个人经验是,大部分卡顿都是“主线程耗时”和“CPU调度延迟”的组合。你想想看,主线程稍微忙一点,再加上CPU调度不给力,帧率就崩了。

7.4.3 第三步:针对性优化

找到原因后,优化手段就水到渠成了:

  • 如果是主线程耗时:把耗时操作移到子线程;使用AsyncTaskHandlerThread;或者用View.post()延迟执行。
  • 如果是渲染线程等待:减少过度绘制;使用硬件加速;优化Shader复杂度。
  • 如果是Binder阻塞:检查服务端性能;考虑使用异步Binder调用。
  • 如果是CPU调度延迟:提高线程优先级;使用CPU亲和性;或者跟系统团队沟通调整调度策略。
核心思路:帧率优化的本质,就是让主线程和渲染线程在16.6ms内,尽可能连续地、不被中断地完成工作。Systrace就是你的“监控摄像头”,帮你看到每一毫秒发生了什么。

7.5 知识体系总览

为了让你更直观地理解这一章的内容,我画了一张图。它把自定义Trace、Async Trace、CPU调度分析和帧率优化的关系串了起来。

Systrace进阶知识体系 自定义Trace Java: Trace.beginSection() Native: ATRACE_CALL() 标记关键代码路径 Async Trace beginAsyncSection(id) endAsyncSection(id) 追踪异步操作生命周期 CPU调度分析 查看线程运行状态 识别Runnable等待 分析上下文切换 帧率优化 找到掉帧点 分析卡顿原因 针对性优化 目标:让主线程和渲染线程在16.6ms内连续工作

这张图把四个模块串起来了。从左到右,从基础到应用。自定义Trace和Async Trace是“探针”,帮你采集数据;CPU调度分析是“显微镜”,帮你看到底层行为;帧率优化是“手术刀”,帮你解决问题。缺一不可。

好了,这一章的内容就到这里。Systrace进阶其实就是一个“打标记、看调度、找瓶颈、做优化”的循环。你用得越多,对系统的理解就越深。下次遇到卡顿问题,别急着拍脑袋改代码,先打开Systrace看看,让数据说话。

一句话总结:自定义Trace让你看到自己的代码,Async Trace让你看到异步操作,CPU调度分析让你看到内核行为,三者结合,帧率优化就不再是玄学。

公众号:蓝海资料掘金营,微信deep3321