慢速渲染

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

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

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

为了帮助提高应用质量,Android 会自动监控您的应用是否出现卡顿,并在 Android Vitals 信息中心显示此信息。有关数据收集方式的信息,请参阅使用 Android Vitals 监控您的应用技术质量

识别卡顿

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

目视检查可让您在几分钟内运行应用中的所有用例,但它不如 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 Performance Monitoring 记录和分析数据。

要了解更多信息,请参阅开始使用适用于 Android 的性能监控

冻结帧

冻结帧是指渲染时间超过 700 毫秒的 UI 帧。这是一个问题,因为您的应用在帧渲染期间,会显得卡顿且对用户输入无响应近一秒钟。我们建议优化应用以在 16 毫秒内渲染一帧,以确保 UI 流畅。但是,在应用启动或切换到不同屏幕时,初始帧绘制时间超过 16 毫秒是正常的,因为您的应用必须从头开始膨胀视图、布局屏幕并执行初始绘制。这就是 Android 将冻结帧与慢速渲染分开跟踪的原因。您的应用中不应有任何帧的渲染时间超过 700 毫秒。

为了帮助您提高应用质量,Android 会自动监控您的应用是否存在冻结帧,并在 Android Vitals 信息中心显示相关信息。有关数据收集方式的信息,请参阅使用 Android Vitals 监控您的应用技术质量

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

跟踪卡顿

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 <your-package-name> 以在 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:嵌套的 RecyclerView

嵌套多个 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 的部分中,请确保您正在受支持的设备上进行测试并使用最新版本的 Support Library。预取仅在 Android 5.0 API 级别 21 及更高版本上受支持。

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

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

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

RecyclerView:绑定时间过长

绑定(即 onBindViewHolder(VH, int))必须直接且耗时远少于一毫秒,除了最复杂的项目。它必须从适配器的内部项目数据中获取普通旧 Java 对象 (POJO) 项目,并在 ViewHolder 中的视图上调用 setter。如果 RV OnBindView 耗时过长,请验证您在绑定代码中执行的工作是否最少。

如果您使用基本的 POJO 对象来保存适配器中的数据,则可以通过使用 Data Binding Library 完全避免在 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 部分工作过多或过于频繁,这意味着您遇到了布局性能问题。您的应用的布局性能取决于视图层次结构的哪个部分具有变化的布局参数或输入。

布局性能:成本

如果分段长于几毫秒,则可能是您遇到了 RelativeLayoutsweighted-LinearLayouts 的最坏情况嵌套性能。这些布局中的每一个都可以触发其子项的多次测量和布局传递,因此嵌套它们可能导致嵌套深度上的 O(n^2) 行为。

尝试在层次结构的最底层叶节点之外避免使用 RelativeLayoutLinearLayout 的权重功能。以下是您可以这样做的方法

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

布局性能:频率

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

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

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

渲染性能

Android UI 分为两个阶段工作

  • 在 UI 线程上进行 Record View#draw,它在每个无效视图上运行 draw(Canvas),并且可以调用自定义视图或您的代码。
  • RenderThread 上的 DrawFrame,它在原生 RenderThread 上运行,但根据 Record View#draw 阶段生成的工作进行操作。

渲染性能:UI 线程

如果 Record View#draw 耗时过长,通常是因为位图正在 UI 线程上绘制。绘制到位图使用 CPU 渲染,因此通常应尽可能避免这种情况。您可以使用 Android CPU Profiler 进行方法跟踪,以查看这是否是问题所在。

在显示位图之前,应用通常会对其进行装饰(有时是添加圆角等装饰),此时通常会绘制到位图

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。如果需要,还可以考虑调用 setLayerType() 并使用 LAYER_TYPE_HARDWARE 来缓存复杂的渲染输出并仍然利用 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 的后台线程上运行。大量的分配可能意味着更多的 CPU 资源用于 GC,如图 5 所示。