渲染缓慢

UI 渲染是指从您的应用生成帧并在屏幕上显示它的过程。为了确保用户与您的应用交互流畅,您的应用必须在 16 毫秒内渲染帧以达到每秒 60 帧 (fps)。要了解为什么首选 60 fps,请参阅 Android 性能模式:为什么是 60fps?。如果您尝试实现 90 fps,则此窗口缩短为 11 毫秒,对于 120 fps,则为 8 毫秒。

如果您超过此窗口 1 毫秒,这并不意味着帧延迟了 1 毫秒,而是 Choreographer 完全丢弃了帧。如果您的应用 UI 渲染缓慢,则系统将被迫跳过帧,用户会感觉到应用卡顿。这称为 *卡顿*。此页面介绍了如何诊断和修复卡顿。

如果您正在开发不使用 View 系统的游戏,那么您将绕过 Choreographer。在这种情况下,帧速率控制库 可帮助 OpenGLVulkan 游戏在 Android 上实现流畅渲染和正确的帧速率控制。

为了帮助提高应用质量,Android 会自动监控您的应用的卡顿情况,并在 Android 核心指标仪表板上显示信息。有关如何收集数据的详细信息,请参阅 使用 Android 核心指标监控应用的技术质量

识别卡顿

查找导致应用卡顿的代码可能很困难。本节介绍三种识别卡顿的方法。

视觉检查可以让您在几分钟内浏览应用中的所有用例,但它提供的细节不如 Systrace。Systrace 提供更多详细信息,但如果您对应用中的所有用例都运行 Systrace,则可能会收到大量难以分析的数据。视觉检查和 Systrace 都可以在您的本地设备上检测卡顿。如果无法在本地设备上重现卡顿,则可以构建自定义性能监控来测量现场设备上应用的特定部分。

视觉检查

视觉检查可以帮助您识别产生卡顿的用例。要执行视觉检查,请打开您的应用并手动浏览应用的不同部分,并在 UI 中寻找卡顿。

以下是执行视觉检查的一些技巧。

  • 运行应用的发布版——或者至少是非可调试版。ART 运行时禁用了几个重要的优化来支持调试功能,因此请确保您查看的内容与用户看到的内容类似。
  • 启用配置文件 GPU 渲染。配置文件 GPU 渲染会在屏幕上显示条形,直观地显示渲染 UI 窗口的帧所花费的时间相对于每帧 16 毫秒基准的多少。每个条形都有彩色组件,对应于渲染管道中的一个阶段,因此您可以看到哪个部分花费的时间最长。例如,如果帧在处理输入方面花费了大量时间,请查看处理用户输入的应用代码。
  • 浏览 常见卡顿源中的组件,例如 RecyclerView
  • 冷启动启动应用。
  • 在较慢的设备上运行您的应用以加剧问题。

当您找到产生卡顿的用例时,您可能对导致应用卡顿的原因有了一个很好的了解。如果您需要更多信息,可以使用 Systrace 进一步调查原因。

Systrace

尽管 Systrace 是一种显示整个设备正在执行的操作的工具,但它对于识别应用中的卡顿很有用。Systrace 的系统开销最小,因此您可以在检测过程中体验到真实的卡顿。

在设备上执行卡顿用例时,使用 Systrace 记录跟踪。有关如何使用 Systrace 的说明,请参阅 在命令行上捕获系统跟踪。Systrace 按进程和线程拆分。在 Systrace 中查找您的应用进程,它看起来像图 1。

Systrace example
图 1. Systrace 示例。

图 1 中的 Systrace 示例包含以下用于识别卡顿的信息。

  1. Systrace 显示每帧何时绘制,并为每帧着色以突出显示缓慢的渲染时间。这有助于您比视觉检查更准确地找到单个卡顿帧。有关更多信息,请参阅 检查 UI 帧和警报
  2. Systrace 会检测应用中的问题,并在单个帧和 警报面板中显示警报。最好按照警报中的说明操作。
  3. Android 框架和库(例如 RecyclerView)的部分包含跟踪标记。因此,systrace 时间线显示这些方法何时在 UI 线程上执行以及执行这些方法需要多长时间。

查看 Systrace 输出后,您可能怀疑应用中的某些方法会导致卡顿。例如,如果时间线显示缓慢帧是由 RecyclerView 耗时过长引起的,您可以 向相关代码添加自定义跟踪事件 并重新运行 Systrace 以获取更多信息。在新 Systrace 中,时间线将显示您的应用方法何时被调用以及执行这些方法需要多长时间。

如果 Systrace 没有显示有关 UI 线程工作为何耗时过长的详细信息,则使用 Android CPU Profiler 记录采样或检测到的方法跟踪。通常,方法跟踪不适合识别卡顿,因为它们会由于较大的开销而产生误报卡顿,并且它们无法查看线程正在运行还是被阻塞。但是,方法跟踪可以帮助您识别应用中耗时最长的那些方法。识别这些方法后,添加跟踪标记 并重新运行 Systrace 以查看这些方法是否会导致卡顿。

有关更多信息,请参阅 了解 Systrace

自定义性能监控

如果无法在本地设备上重现卡顿,则可以在应用中构建自定义性能监控以帮助识别现场设备上卡顿的来源。

为此,请使用 FrameMetricsAggregator 从应用的特定部分收集帧渲染时间,并使用 Firebase 性能监控 记录和分析数据。

要了解更多信息,请参阅 Android 性能监控入门

冻结帧

冻结帧是指渲染时间超过 700 毫秒的 UI 帧。这是一个问题,因为您的应用似乎卡住了,并且在帧渲染期间几乎整整一秒钟内都无法响应用户输入。我们建议优化应用以在 16 毫秒内渲染一帧,以确保 UI 流畅。但是,在应用启动期间或转换到不同的屏幕时,初始帧渲染时间超过 16 毫秒是正常的,因为您的应用必须从头开始加载视图、布局屏幕并执行初始绘制。这就是 Android 将冻结帧与缓慢渲染分开跟踪的原因。应用中的任何帧都不应该花费超过 700 毫秒来渲染。

为了帮助您提高应用质量,Android 会自动监控应用中的冻结帧,并在 Android 性能指标面板中显示信息。有关如何收集数据的更多信息,请参阅 使用 Android 性能指标监控应用的技术质量

冻结帧是缓慢渲染的一种极端形式,因此诊断和解决问题的过程相同。

跟踪卡顿

Perfetto 中的 FrameTimeline 可以帮助跟踪缓慢或冻结的帧。

缓慢帧、冻结帧和 ANR 之间的关系

缓慢帧、冻结帧和 ANR 都是应用可能遇到的不同形式的卡顿。请参阅下表以了解区别。

缓慢帧 冻结帧 ANR
渲染时间 16 毫秒到 700 毫秒之间 700 毫秒到 5 秒之间 超过 5 秒
可见用户影响区域
  • RecyclerView 滚动行为突然变化
  • 在具有复杂动画的屏幕上动画显示不正常
  • 在应用启动期间
  • 从一个屏幕移动到另一个屏幕——例如,屏幕过渡
  • 当您的 Activity 位于前台时,您的应用在五秒内未响应输入事件或 BroadcastReceiver——例如按键或屏幕点击事件。
  • 当您没有 Activity 位于前台时,您的 BroadcastReceiver 未在相当长的时间内完成执行。

分别跟踪缓慢帧和冻结帧

在应用启动期间或转换到不同的屏幕时,初始帧渲染时间超过 16 毫秒是正常的,因为应用必须加载视图、布局屏幕并从头开始执行初始绘制。

优化和解决卡顿的最佳实践

在寻求解决应用中的卡顿时,请牢记以下最佳实践。

  • 识别并解决最容易重现的卡顿实例。
  • 优先处理 ANR。虽然缓慢或冻结的帧可能会使应用看起来反应迟钝,但 ANR 会导致应用停止响应。
  • 缓慢渲染很难重现,但您可以从消除 700 毫秒的冻结帧开始。这在应用启动或更改屏幕时最为常见。

修复卡顿

要修复卡顿,请检查哪些帧未在 16 毫秒内完成,并查找问题所在。检查某些帧中 Record View#drawLayout 是否花费了异常长的时间。有关这些问题和其他问题的详细信息,请参阅 常见的卡顿源

要避免卡顿,请在 UI 线程之外异步运行长时间运行的任务。始终注意代码在哪个线程上运行,并在将非平凡的任务发布到主线程时谨慎操作。

如果您的应用有一个复杂且重要的主要 UI——例如中央滚动列表——请考虑 编写检测工具测试,这些测试可以自动检测缓慢的渲染时间并频繁运行这些测试以防止回归。

常见的卡顿源

以下部分解释了使用 View 系统的应用中常见的卡顿源以及解决这些问题的最佳实践。有关修复 Jetpack Compose 性能问题的更多信息,请参阅 Jetpack Compose 性能

可滚动列表

ListView——尤其是 RecyclerView——通常用于复杂的滚动列表,这些列表最容易出现卡顿。它们都包含 Systrace 标记,因此您可以使用 Systrace 来查看它们是否导致了应用中的卡顿。传递命令行参数 -a <您的包名> 以获取 RecyclerView 中的跟踪部分——以及您添加的任何跟踪标记——以显示。如果可用,请遵循 Systrace 输出中生成的警报的指导。在 Systrace 中,您可以单击 RecyclerView 跟踪的部分以查看 RecyclerView 正在执行的工作的说明。

RecyclerView: notifyDataSetChanged()

如果您发现您的 RecyclerView 中的每个项目都在一帧内重新绑定(从而重新布局和重新绘制),请确保您没有为少量更新调用 notifyDataSetChanged()setAdapter(Adapter)swapAdapter(Adapter, boolean)。这些方法表示列表内容发生了整体变化,并在 Systrace 中显示为 RV FullInvalidate。相反,请使用 SortedListDiffUtil 在内容更改或添加时生成最小的更新。

例如,考虑一个从服务器接收新闻内容列表的新版本的应用程序。当您将此信息发布到 Adapter 时,可以调用 notifyDataSetChanged(),如下例所示

Kotlin

fun onNewDataArrived(news: List<News>) {
    myAdapter.news = news
    myAdapter.notifyDataSetChanged()
}

Java

void onNewDataArrived(List<News> news) {
    myAdapter.setNews(news);
    myAdapter.notifyDataSetChanged();
}

这样做的缺点是,如果存在微不足道的更改,例如在顶部添加一个项目,RecyclerView 不会察觉。因此,它被告知放弃其所有缓存的项目状态,因此需要重新绑定所有内容。

我们建议您使用 DiffUtil,它可以为您计算和分派最小的更新

Kotlin

fun onNewDataArrived(news: List<News>) {
    val oldNews = myAdapter.items
    val result = DiffUtil.calculateDiff(MyCallback(oldNews, news))
    myAdapter.news = news
    result.dispatchUpdatesTo(myAdapter)
}

Java

void onNewDataArrived(List<News> news) {
    List<News> oldNews = myAdapter.getItems();
    DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news));
    myAdapter.setNews(news);
    result.dispatchUpdatesTo(myAdapter);
}

要告知 DiffUtil 如何检查您的列表,请将您的 MyCallback 定义为 Callback 实现。

RecyclerView:嵌套的 RecyclerViews

嵌套多个 RecyclerView 实例很常见,尤其是在垂直列表中包含水平滚动的列表时。例如,Play 商店主页上的应用程序网格。这可以很好地工作,但它也会导致很多视图四处移动。

如果您在第一次向下滚动页面时看到很多内部项目正在加载,您可能需要检查您是否在内部(水平)RecyclerView 实例之间共享了 RecyclerView.RecycledViewPool。默认情况下,每个 RecyclerView 都有自己的项目池。但是,如果屏幕上同时显示十几种 itemViews,那么当所有行都显示类似类型的视图时,itemViews 无法由不同的水平列表共享,就会出现问题。

Kotlin

class OuterAdapter : RecyclerView.Adapter<OuterAdapter.ViewHolder>() {

    ...

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // Inflate inner item, find innerRecyclerView by ID.
        val innerLLM = LinearLayoutManager(parent.context, LinearLayoutManager.HORIZONTAL, false)
        innerRv.apply {
            layoutManager = innerLLM
            recycledViewPool = sharedPool
        }
        return OuterAdapter.ViewHolder(innerRv)
    }
    ...

Java

class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
    RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool();

    ...

    @Override
    public void onCreateViewHolder(ViewGroup parent, int viewType) {
        // Inflate inner item, find innerRecyclerView by ID.
        LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(),
                LinearLayoutManager.HORIZONTAL);
        innerRv.setLayoutManager(innerLLM);
        innerRv.setRecycledViewPool(sharedPool);
        return new OuterAdapter.ViewHolder(innerRv);

    }
    ...

如果您想进一步优化,还可以对内部 RecyclerViewLinearLayoutManager 调用 setInitialPrefetchItemCount(int)。例如,如果您始终在一行中可见 3.5 个项目,请调用 innerLLM.setInitialItemPrefetchCount(4)。这会向 RecyclerView 发出信号,表明当水平行即将出现在屏幕上时,如果 UI 线程有空闲时间,它必须尝试预取其中的项目。

RecyclerView:加载过多或创建耗时过长

在大多数情况下,RecyclerView 中的预取功能可以通过提前完成工作(在 UI 线程空闲时)来帮助解决加载的成本。如果您在帧期间(而不是在标记为 RV Prefetch 的部分中)看到加载,请确保您在受支持的设备上进行测试并使用最新版本的 支持库。预取仅在 Android 5.0 API 级别 21 及更高版本上受支持。

如果您经常看到加载导致卡顿,因为新的项目出现在屏幕上,请验证您是否没有比需要的更多的视图类型。RecyclerView 内容中的视图类型越少,当新的项目类型出现在屏幕上时,需要进行的加载就越少。如果可能,在合理的情况下合并视图类型。如果只有图标、颜色或文本片段在类型之间发生变化,您可以在绑定时进行更改,并避免加载,这同时减少了应用程序的内存占用。

如果您的视图类型看起来不错,请考虑降低加载的成本。减少不必要的容器和结构化视图可以有所帮助。考虑使用 ConstraintLayout 构建 itemViews,这有助于减少结构化视图。

如果您想进一步优化性能,并且您的项目层次结构很简单,并且不需要复杂的主题和样式功能,请考虑自己调用构造函数。但是,通常不值得牺牲 XML 的简单性和功能。

RecyclerView:绑定耗时过长

绑定(即 onBindViewHolder(VH, int))必须简单明了,并且对于除最复杂的项目之外的所有项目,花费的时间都必须少于 1 毫秒。它必须从适配器的内部项目数据中获取普通的 Java 对象 (POJO) 项目,并调用 ViewHolder 中视图的 setter。如果 RV OnBindView 耗时过长,请验证您在绑定代码中是否只执行了最少的工作。

如果您使用基本的 POJO 对象在适配器中保存数据,则可以通过使用 数据绑定库 完全避免在 onBindViewHolder 中编写绑定代码。

RecyclerView 或 ListView:布局或绘制耗时过长

有关绘制和布局的问题,请参阅 布局性能渲染性能 部分。

ListView:加载

如果您不小心,可能会意外禁用 ListView 中的回收。如果您每次看到项目出现在屏幕上时都进行加载,请检查您 Adapter.getView() 的实现是否正在重复使用、重新绑定并返回 convertView 参数。如果您的 getView() 实现始终进行加载,则您的应用程序无法获得 ListView 中回收带来的好处。您的 getView() 的结构几乎总是类似于以下实现

Kotlin

fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    return (convertView ?: layoutInflater.inflate(R.layout.my_layout, parent, false)).apply {
        // Bind content from position to convertView.
    }
}

Java

View getView(int position, View convertView, ViewGroup parent) {

    if (convertView == null) {
        // Only inflate if no convertView passed.
        convertView = layoutInflater.inflate(R.layout.my_layout, parent, false)
    }
    // Bind content from position to convertView.
    return convertView;
}

布局性能

如果 Systrace 显示 Choreographer#doFrameLayout 部分工作过多或过于频繁,则表示您遇到了布局性能问题。应用程序的布局性能取决于视图层次结构的哪一部分具有更改的布局参数或输入。

布局性能:成本

如果这些部分的持续时间超过几毫秒,则可能是您遇到了 RelativeLayouts加权 LinearLayout 的最坏情况嵌套性能。这些布局中的每一个都可能触发其子项的多次测量和布局传递,因此嵌套它们会导致在嵌套深度上出现 O(n^2) 的行为。

尝试避免在层次结构中除最低叶子节点之外的所有节点使用 RelativeLayoutLinearLayout 的加权功能。以下是一些您可以执行此操作的方法

  • 重新组织您的结构化视图。
  • 定义自定义布局逻辑。请参阅 优化布局层次结构 以获取特定示例。您可以尝试转换为 ConstraintLayout,它提供了类似的功能,但没有性能缺点。

布局性能:频率

当新的内容出现在屏幕上时,预计会发生布局,例如,当新的项目在 RecyclerView 中滚动到视图中时。如果每一帧都发生了大量的布局,则可能是您正在对布局进行动画处理,这可能会导致帧丢失。

通常,动画必须在 View 的绘制属性上运行,例如以下属性

您可以比布局属性(例如填充或边距)更廉价地更改所有这些属性。通常,通过调用触发下一帧中 invalidate(),然后 draw(Canvas) 的 setter 来更改视图的绘制属性也便宜得多。这会重新记录被设为无效的视图的绘制操作,并且通常也比布局便宜得多。

渲染性能

Android UI 分为两个阶段

  • 在 UI 线程上 **记录 View#draw**,它对每个无效的视图运行 draw(Canvas),并且可以调用自定义视图或代码中的调用。
  • RenderThread 上 **绘制帧**,它在原生 RenderThread 上运行,但基于 **记录 View#draw** 阶段生成的工作进行操作。

渲染性能:UI 线程

如果 **记录 View#draw** 耗时过长,则通常是在 UI 线程上绘制位图。绘制到位图使用 CPU 渲染,因此通常应尽可能避免这种情况。您可以使用 Android CPU 分析器的 方法跟踪 来查看这是否是问题所在。

当应用程序想要在显示位图之前对其进行装饰时,通常会绘制到位图上——有时装饰会像添加圆角一样

Kotlin

val paint = Paint().apply {
    isAntiAlias = true
}
Canvas(roundedOutputBitmap).apply {
    // Draw a round rect to define the shape:
    drawRoundRect(
            0f,
            0f,
            roundedOutputBitmap.width.toFloat(),
            roundedOutputBitmap.height.toFloat(),
            20f,
            20f,
            paint
    )
    paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
    // Multiply content on top to make it rounded.
    drawBitmap(sourceBitmap, 0f, 0f, paint)
    setBitmap(null)
    // Now roundedOutputBitmap has sourceBitmap inside, but as a circle.
}

Java

Canvas bitmapCanvas = new Canvas(roundedOutputBitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
// Draw a round rect to define the shape:
bitmapCanvas.drawRoundRect(0, 0,
        roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 20, 20, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
// Multiply content on top to make it rounded.
bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint);
bitmapCanvas.setBitmap(null);
// Now roundedOutputBitmap has sourceBitmap inside, but as a circle.

如果这是您在 UI 线程上执行的这类工作,则可以改为在后台的解码线程上执行此操作。在某些情况下,例如前面的示例,您甚至可以在绘制时执行此操作。因此,如果您的 DrawableView 代码如下所示

Kotlin

fun setBitmap(bitmap: Bitmap) {
    mBitmap = bitmap
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawBitmap(mBitmap, null, paint)
}

Java

void setBitmap(Bitmap bitmap) {
    mBitmap = bitmap;
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawBitmap(mBitmap, null, paint);
}

您可以将其替换为以下代码

Kotlin

fun setBitmap(bitmap: Bitmap) {
    shaderPaint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawRoundRect(0f, 0f, width, height, 20f, 20f, shaderPaint)
}

Java

void setBitmap(Bitmap bitmap) {
    shaderPaint.setShader(
            new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint);
}

您也可以对背景保护执行此操作,例如在位图顶部绘制渐变,以及使用 ColorMatrixColorFilter 进行图像滤镜——另外两个常见的修改位图的操作。

如果您出于其他原因绘制到位图上(可能将其用作缓存),请尝试直接绘制到传递给您的 ViewDrawable 的硬件加速 Canvas 上。如有必要,还可以考虑使用 LAYER_TYPE_HARDWARE 调用 setLayerType() 来缓存复杂的渲染输出,并继续利用 GPU 渲染。

渲染性能:RenderThread

某些 Canvas 操作记录起来成本很低,但在 RenderThread 上会触发昂贵的计算。Systrace 通常会使用警报来指明这些操作。

动画大型路径

当在传递给View的硬件加速Canvas上调用Canvas.drawPath()时,Android 会先在 CPU 上绘制这些路径,然后将其上传到 GPU。如果路径很大,请避免在帧与帧之间对其进行编辑,以便可以对其进行缓存并高效地绘制。即使使用更多绘制调用,drawPoints()drawLines()drawRect/Circle/Oval/RoundRect()也更有效率,更适合使用。

Canvas.clipPath

clipPath(Path) 会触发代价高昂的裁剪行为,通常应避免使用。如果可能,选择绘制形状而不是裁剪到非矩形。它性能更好并支持抗锯齿。例如,以下clipPath调用可以用不同的方式表达

Kotlin

canvas.apply {
    save()
    clipPath(circlePath)
    drawBitmap(bitmap, 0f, 0f, paint)
    restore()
}

Java

canvas.save();
canvas.clipPath(circlePath);
canvas.drawBitmap(bitmap, 0f, 0f, paint);
canvas.restore();

相反,请按如下方式表达前面的示例

Kotlin

paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
// At draw time:
canvas.drawPath(circlePath, mPaint)

Java

// One time init:
paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
// At draw time:
canvas.drawPath(circlePath, mPaint);
位图上传

Android 将位图显示为 OpenGL 纹理,并且位图第一次在帧中显示时,它会被上传到 GPU。您可以在 Systrace 中将其显示为**Texture upload(id) width x height**。这可能需要几毫秒,如图形 2 所示,但这是使用 GPU 显示图像所必需的。

如果这些操作花费的时间过长,首先检查跟踪中的宽度和高度数字。确保显示的位图的大小不会明显大于屏幕上显示的区域。如果是,这会浪费上传时间和内存。通常,位图加载库提供了一种请求适当大小位图的方法。

在 Android 7.0 中,位图加载代码(通常由库完成)可以调用prepareToDraw()以在需要之前触发早期上传。这样,上传就会在RenderThread空闲时尽早发生。您可以在解码后或将位图绑定到视图时执行此操作,只要您知道位图即可。理想情况下,您的位图加载库会为您执行此操作,但如果您正在管理自己的库或希望确保在较新的设备上不会发生上传,则可以在自己的代码中调用prepareToDraw()

An app spends significant time in a
  frame uploading a large bitmap
图 2. 应用程序在一个帧中花费大量时间上传大型位图。请减小其大小,或者在使用prepareToDraw()解码时尽早触发它。

线程调度延迟

线程调度程序是 Android 操作系统的一部分,负责决定系统中的哪些线程必须运行、何时运行以及运行多长时间。

有时,卡顿发生是因为应用程序的 UI 线程被阻塞或未运行。Systrace 使用不同的颜色(如图形 3 所示)来指示线程何时处于睡眠状态(灰色)、可运行状态(蓝色:它可以运行,但尚未被调度程序选中运行)、主动运行状态(绿色)或不可中断睡眠状态(红色或橙色)。这对于调试由线程调度延迟引起的卡顿问题非常有用。

Highlights a period when the UI thread
  is sleeping
图 3. UI 线程处于睡眠状态期间的突出显示。

通常,Binder 调用(Android 上的进程间通信 (IPC) 机制)会导致应用程序执行出现长时间暂停。在较新版本的 Android 上,这是导致 UI 线程停止运行的最常见原因之一。通常,解决方法是避免调用执行 Binder 调用的函数。如果不可避免,请缓存值或将工作移至后台线程。随着代码库的增大,如果您不小心,可能会通过调用某些低级方法意外添加 Binder 调用。但是,您可以使用跟踪找到并修复它们。

如果您有 Binder 事务,可以使用以下adb命令捕获其调用堆栈

$ adb shell am trace-ipc start
… use the app - scroll/animate ...
$ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
$ adb pull /data/local/tmp/ipc-trace.txt

有时,看似无害的调用(如getRefreshRate())可能会触发 Binder 事务,并在频繁调用时导致重大问题。定期跟踪可以帮助您发现和修复这些问题,因为它们会显示出来。

Shows the UI thread sleeping due to binder
  transactions in a RV fling. Keep your bind logic focused, and use trace-ipc to
  track down and remove binder calls.
图 4. 由于 RV 抛掷中的 Binder 事务,UI 线程处于睡眠状态。保持绑定逻辑简单,并使用trace-ipc来跟踪和删除 Binder 调用。

如果您没有看到 Binder 活动,但仍然没有看到 UI 线程运行,请确保您没有等待来自另一个线程的锁或其他操作。通常,UI 线程不必等待来自其他线程的结果。其他线程必须将信息发布到它。

对象分配和垃圾回收

自从 ART 在 Android 5.0 中作为默认运行时引入以来,对象分配和垃圾回收 (GC) 已成为一个不太重要的问题,但仍然有可能通过此额外工作来减轻线程的负担。响应不常发生的事件(例如用户点击按钮)进行分配是可以的,但请记住每次分配都会带来成本。如果它位于频繁调用的紧密循环中,请考虑避免分配以减轻 GC 的负担。

Systrace 显示 GC 是否频繁运行,而Android 内存分析器可以显示分配来自何处。如果可能,避免分配,尤其是在紧密循环中,您就不太可能遇到问题。

Shows a 94ms GC on the HeapTaskDaemon
图 5. HeapTaskDaemon 线程上的 94 毫秒 GC。

在最近版本的 Android 上,GC 通常在名为**HeapTaskDaemon**的后台线程上运行。大量的分配可能意味着在 GC 上花费更多的 CPU 资源,如图形 5 所示。