UI 渲染是指从您的应用生成帧并将其显示在屏幕上的过程。为了确保用户与您的应用的交互流畅,您的应用必须在 16 毫秒内渲染帧以达到每秒 60 帧 (fps)。要了解为什么首选 60 fps,请参阅 Android 性能模式:为什么是 60fps?。如果您尝试达到 90 fps,则此窗口缩短至 11 毫秒,而对于 120 fps,则为 8 毫秒。
如果您超过此窗口 1 毫秒,并不意味着帧延迟 1 毫秒,而是 Choreographer
完全丢弃该帧。如果您的应用出现 UI 渲染缓慢,则系统将被迫跳过帧,用户会感觉到应用卡顿。这称为 *卡顿*。此页面介绍如何诊断和修复卡顿。
如果您正在开发不使用 View
系统的游戏,则您将绕过 Choreographer
。在这种情况下,帧速率调整库 帮助 OpenGL 和 Vulkan 游戏在 Android 上实现流畅渲染和正确的帧速率调整。
为了帮助提高应用质量,Android 会自动监控您的应用是否存在卡顿,并在 Android 核心指标信息中心中显示相关信息。有关如何收集数据的详细信息,请参阅 使用 Android 核心指标监控应用的技术质量。
识别卡顿
查找导致应用卡顿的代码可能很困难。本节介绍三种识别卡顿的方法
视觉检查 允许您在几分钟内浏览应用中的所有用例,但它提供的细节不如 Systrace。Systrace 提供更多详细信息,但如果您对应用中的所有用例都运行 Systrace,可能会产生大量难以分析的数据。视觉检查和 Systrace 都可以在本地设备上检测卡顿。如果无法在本地设备上重现卡顿,则可以构建自定义性能监控来测量设备现场运行的应用的特定部分。
视觉检查
视觉检查可以帮助您识别导致卡顿的用例。要执行视觉检查,请打开您的应用并手动浏览应用的不同部分,查找 UI 中的卡顿。
以下是执行视觉检查的一些提示
- 运行应用的发布版——或至少是非可调试版。ART 运行时禁用了几个重要的优化以支持调试功能,因此请确保您查看的内容与用户看到的内容类似。
- 启用配置文件 GPU 渲染。配置文件 GPU 渲染在屏幕上显示条形,直观地显示渲染 UI 窗口帧所需的时间相对于每帧 16 毫秒的基准时间。每个条形都有彩色组件,映射到渲染管道中的一个阶段,因此您可以看到哪个部分花费的时间最长。例如,如果帧花费大量时间处理输入,请查看处理用户输入的应用代码。
- 浏览常见卡顿来源的组件,例如
RecyclerView
。 - 从冷启动启动应用。
- 在较慢的设备上运行您的应用以加剧问题。
当您找到产生卡顿的用例时,您可能对应用中导致卡顿的原因有了一个很好的了解。如果您需要更多信息,可以使用 Systrace 进一步查找原因。
Systrace
虽然 Systrace 是一种显示整个设备正在执行的操作的工具,但它对于识别应用中的卡顿很有用。Systrace 的系统开销最小,因此您可以在检测过程中体验真实的卡顿现象。
在设备上执行卡顿用例时,使用Systrace 记录跟踪。有关如何使用 Systrace 的说明,请参阅在命令行上捕获系统跟踪。Systrace 按进程和线程拆分。在 Systrace 中查找应用的进程,它看起来像图 1。
图 1 中的 Systrace 示例包含以下用于识别卡顿的信息
- Systrace 显示每帧何时绘制,并为每帧着色以突出显示渲染缓慢的时间。这有助于您比视觉检查更准确地找到单个卡顿帧。有关更多信息,请参阅检查 UI 帧和警报。
- Systrace 检测应用中的问题,并在单个帧和警报面板中显示警报。最好按照警报中的说明操作。
- 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 秒 |
可见的用户影响区域 |
|
|
|
分别跟踪缓慢帧和冻结帧
在应用启动或切换到其他屏幕期间,初始帧渲染时间超过 16 毫秒是正常的,因为应用必须加载视图、布局屏幕并从头开始执行初始绘制。
优先级排序和解决卡顿的最佳实践
在查找解决应用中卡顿问题时,请牢记以下最佳实践
- 识别并解决最容易重现的卡顿实例。
- 优先解决 ANR。虽然缓慢或冻结帧可能会使应用看起来反应迟钝,但 ANR 会导致应用停止响应。
- 渲染缓慢很难重现,但您可以先解决 700 毫秒的冻结帧。这在应用启动或更改屏幕时最常见。
修复卡顿
要修复卡顿,请检查哪些帧未在 16 毫秒内完成,并查找问题所在。检查某些帧中 Record View#draw
或 Layout
是否花费了异常长的时间。有关这些问题和其他问题的更多信息,请参阅常见卡顿来源。
为避免卡顿,请在 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**。相反,请使用 SortedList
或 DiffUtil
在内容更改或添加时生成最小的更新。
例如,考虑一个从服务器接收新闻内容列表的新版本的应用。当您将此信息发布到 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); } ...
如果您想进一步优化,您还可以对内部 RecyclerView
的 LinearLayoutManager
调用 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 对象在适配器中保存数据,则可以通过使用 数据绑定库 完全避免在 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#doFrame
的 **Layout** 部分工作量过大或过于频繁,则表示您遇到了布局性能问题。应用的布局性能取决于视图层次结构的哪一部分具有变化的布局参数或输入。
布局性能: 成本
如果这些部分的时长超过几毫秒,则可能表示您遇到了 RelativeLayouts
或 加权 LinearLayout
的最坏情况嵌套性能。这些布局中的每一个都可能触发其子项的多次测量和布局传递,因此嵌套它们会导致在嵌套深度上出现 O(n^2)
的行为。
尝试避免在层次结构中除最低叶子节点之外的所有节点中使用 RelativeLayout
或 LinearLayout
的权重功能。以下是一些您可以执行此操作的方法
- 重新组织您的结构视图。
- 定义自定义布局逻辑。有关特定示例,请参阅 优化布局层次结构。您可以尝试转换为
ConstraintLayout
,它提供了类似的功能,但没有性能缺点。
布局性能: 频率
当新内容出现在屏幕上时,预计会发生布局,例如当新项目在 RecyclerView
中滚动到视图中时。如果每一帧都发生大量的布局,则可能表示您正在对布局进行动画处理,这很可能会导致帧丢失。
通常,动画必须在 View
的绘图属性上运行,例如以下属性
您可以比布局属性(如填充或边距)更廉价地更改所有这些属性。通常,通过调用触发 invalidate()
的 setter 来更改视图的绘图属性,然后在下一帧中调用 draw(Canvas)
也要便宜得多。这会重新记录被声明无效的视图的绘图操作,并且通常也比布局便宜得多。
渲染性能
Android UI 分两个阶段工作
- 在 UI 线程上 **记录 View#draw**,它会对每个无效的视图运行
draw(Canvas)
,并且可以调用自定义视图或代码中的调用。 - 在
RenderThread
上 **绘制帧**,它在原生RenderThread
上运行,但基于 **记录 View#draw** 阶段生成的工作进行操作。
渲染性能: UI 线程
如果 **记录 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 线程上执行的这类工作,则可以在后台的解码线程上执行此操作。在某些情况下,例如前面的示例,您甚至可以在绘制时完成此工作。因此,如果您的 Drawable
或 View
代码如下所示
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
进行图像滤镜——这两个是修改位图的另外两个常见操作。
如果您出于其他原因绘制到位图——可能将其用作缓存——请尝试直接绘制到传递给您的 View
或 Drawable
的硬件加速 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()
。
线程调度延迟
线程调度程序是 Android 操作系统中负责决定系统中哪些线程必须运行、何时运行以及运行多长时间的部分。
有时,卡顿的发生是因为您的应用程序的 UI 线程被阻塞或未运行。Systrace 使用不同的颜色(如图 3 所示)来指示线程何时处于睡眠状态(灰色)、可运行状态(蓝色:它可以运行,但尚未被调度程序选中运行)、正在积极运行状态(绿色)或处于不可中断睡眠状态(红色或橙色)。这对于调试由线程调度延迟引起的卡顿问题非常有用。
通常,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 事务,并在频繁调用时造成重大问题。定期跟踪可以帮助您在这些问题出现时找到并修复它们。
如果您没有看到 Binder 活动,但仍然没有看到 UI 线程运行,请确保您没有等待来自另一个线程的锁或其他操作。通常,UI 线程无需等待来自其他线程的结果。其他线程必须将信息发布到 UI 线程。
对象分配和垃圾回收
自 ART 在 Android 5.0 中作为默认运行时引入以来,对象分配和垃圾回收 (GC) 的问题已大大减少,但仍然有可能使您的线程因这额外的工作而减慢速度。响应不常发生的事件(例如用户点击按钮)进行分配是可以的,但请记住,每次分配都会产生一定的成本。如果它位于频繁调用的紧密循环中,请考虑避免分配以减轻 GC 的负载。
Systrace 会显示 GC 是否频繁运行,而Android 内存分析器可以显示分配的来源。如果可能,避免分配,尤其是在紧密循环中,您就不太可能遇到问题。
在最近版本的 Android 上,GC 通常在名为**HeapTaskDaemon**的后台线程上运行。大量的分配可能意味着在 GC 上花费更多的 CPU 资源,如图 5 所示。
推荐内容
- 注意:当 JavaScript 关闭时,会显示链接文本
- 对您的应用进行基准测试
- 测量应用程序性能概述
- 应用程序优化的最佳实践