性能分析
性能分析是分析应用程序性能、资源使用和行为以识别潜在瓶颈或低效率的过程。值得利用性能分析工具来确保您的应用程序在不同设备和条件下流畅运行。
对于 iOS,Instruments 是一个非常宝贵的工具,而在 Android 上,您应该学习使用 Android Studio Profiler。
但首先,请确保开发模式已关闭! 您应该在应用程序日志中看到 __DEV__ === false, development-level warning are OFF, performance optimizations are ON
。
使用系统跟踪分析 Android UI 性能
Android 支持 10k+ 种不同的手机,并且普遍支持软件渲染:框架架构以及需要跨许多硬件目标进行通用化,不幸地意味着相对于 iOS,您免费获得的优势更少。但有时,您可以进行改进——很多时候这根本不是原生代码的错!
调试这种卡顿的第一步是回答每个 16 毫秒帧期间时间花费在何处的基本问题。为此,我们将使用 Android Studio 内置的系统跟踪分析器。
1. 收集跟踪数据
首先,通过 USB 将您想要调查出现卡顿的设备连接到您的计算机。在 Android Studio 中打开您的项目 android
文件夹,在右上角窗格中选择您的设备,然后以可分析模式运行您的项目。
当您的应用以可分析模式构建并在设备上运行时,将您的应用带到您想要分析的导航/动画之前,并在 Android Studio Profiler 窗格中开始“捕获系统活动”任务。
一旦跟踪开始收集,执行您关心的动画或交互。然后按下“停止录制”。您现在可以直接在 Android Studio 中检查跟踪数据。或者,您可以在“过去的录制”窗格中选择它,按下“导出录制”,并在 Perfetto 等工具中打开它。
2. 读取跟踪数据
在 Android Studio 或 Perfetto 中打开跟踪数据后,您应该会看到类似这样的内容
使用 WASD 键进行平移和缩放。
确切的 UI 可能有所不同,但以下说明无论您使用何种工具都适用。
勾选屏幕右上角的此复选框以高亮显示 16 毫秒帧边界
您应该会看到如上图所示的斑马条纹。如果没有,请尝试在不同的设备上进行性能分析:已知三星在显示 vsyncs 时存在问题,而 Nexus 系列通常相当可靠。
3. 查找您的进程
滚动直到您看到(部分)您的包名。在此示例中,我正在分析 com.facebook.adsmanager
,由于内核中线程名称的愚蠢限制,它显示为 book.adsmanager
。
在左侧,您将看到一组与右侧时间轴行对应的线程。出于我们的目的,我们关注几个线程:UI 线程(具有您的包名或名称 UI Thread)、mqt_js
和 mqt_native_modules
。如果您在 Android 5+ 上运行,我们还会关注渲染线程(Render Thread)。
-
UI 线程。这是标准 Android 测量/布局/绘制发生的地方。右侧的线程名称将是您的包名(在我的示例中是 book.adsmanager)或 UI Thread。您在此线程上看到的事件应该看起来像这样,并且与
Choreographer
、traversals
和DispatchUI
有关 -
JS 线程。这是 JavaScript 执行的地方。线程名称将是
mqt_js
或<...>
,具体取决于您设备上的内核配合程度。如果它没有名称,要识别它,请查找诸如JSCall
、Bridge.executeJSCall
等内容 -
原生模块线程。这是执行原生模块调用(例如
UIManager
)的地方。线程名称将是mqt_native_modules
或<...>
。在后一种情况下要识别它,请查找诸如NativeCall
、callJavaModuleMethod
和onBatchComplete
等内容 -
额外提示:渲染线程。如果您使用 Android L (5.0) 及更高版本,您的应用程序中还将有一个渲染线程。此线程生成用于绘制 UI 的实际 OpenGL 命令。线程名称将是
RenderThread
或<...>
。在后一种情况下要识别它,请查找诸如DrawFrame
和queueBuffer
等内容
识别罪魁祸首
流畅的动画应该看起来像这样
颜色的每次变化都是一个帧——请记住,为了显示一个帧,我们所有的 UI 工作都需要在该 16 毫秒周期结束前完成。请注意,没有线程在靠近帧边界的地方工作。像这样渲染的应用程序以 60 FPS 渲染。
但是,如果您注意到卡顿,您可能会看到类似这样的情况
请注意,JS 线程几乎一直在执行,并且跨越了帧边界!此应用程序未以 60 FPS 渲染。在这种情况下,问题出在 JS 中。
您可能还会看到类似这样的情况
在这种情况下,UI 线程和渲染线程是工作跨越帧边界的线程。我们试图在每个帧上渲染的 UI 需要完成的工作太多。在这种情况下,问题出在正在渲染的原生视图中。
此时,您将拥有一些非常有用的信息来指导您的后续步骤。
解决 JavaScript 问题
如果您识别出 JS 问题,请在您正在执行的特定 JS 中寻找线索。在上述场景中,我们看到 RCTEventEmitter
在每帧被多次调用。这是上面跟踪数据中 JS 线程的放大图
这看起来不对劲。为什么它被调用得如此频繁?它们实际上是不同的事件吗?这些问题的答案可能取决于您的产品代码。很多时候,您会想研究 shouldComponentUpdate。
解决原生 UI 问题
如果您识别出原生 UI 问题,通常有两种情况
- 您试图每帧绘制的 UI 涉及 GPU 上过多的工作,或者
- 您正在动画/交互期间构建新的 UI(例如,在滚动期间加载新内容)。
过多的 GPU 工作
在第一种情况下,您将看到 UI 线程和/或渲染线程的跟踪数据如下所示
请注意,DrawFrame
中花费了大量时间并跨越了帧边界。这是等待 GPU 清空上一帧的命令缓冲区所花费的时间。
为了缓解这种情况,您应该
- 研究对正在动画/转换的复杂静态内容使用
renderToHardwareTextureAndroid
(例如Navigator
滑动/alpha 动画) - 确保您没有使用
needsOffscreenAlphaCompositing
(默认禁用),因为它在大多数情况下会大大增加 GPU 每帧的负载。
在 UI 线程上创建新视图
在第二种情况下,您将看到类似这样的情况
请注意,首先 JS 线程思考了一会儿,然后您会看到原生模块线程上完成了一些工作,接着是 UI 线程上昂贵的遍历。
目前没有快速的方法来缓解这种情况,除非您能够推迟创建新 UI 直到交互之后,或者能够简化您正在创建的 UI。React Native 团队正在为此开发一个基础设施级别的解决方案,该方案将允许在新 UI 脱离主线程创建和配置,从而使交互能够流畅进行。
查找原生 CPU 热点
如果问题似乎出在原生侧,您可以使用 CPU 热点分析器来获取更多详细信息。打开 Android Studio Profiler 面板并选择“查找 CPU 热点 (Java/Kotlin 方法记录)”。
确保您选择“查找 CPU 热点 (Java/Kotlin 记录)”,而不是“查找 CPU 热点 (调用栈采样)”。它们有相似的图标但做不同的事情。
执行交互并按下“停止录制”。录制是资源密集型的,因此请保持交互简短。然后您可以直接在 Android Studio 中检查结果跟踪数据,或导出并在 Firefox Profiler 等在线工具中打开。
与系统跟踪不同,CPU 热点分析速度较慢,因此不会给您精确的测量结果。但是,它应该能让您了解正在调用哪些原生方法,以及在每个帧期间时间是如何按比例花费的。