分析
分析是分析应用的性能、资源使用情况和行为以识别潜在瓶颈或低效率的过程。值得使用分析工具来确保你的应用在不同设备和条件下顺利运行。
¥Profiling is the process of analyzing an app's performance, resource usage, and behavior to identify potential bottlenecks or inefficiencies. It's worth making use of profiling tools to ensure your app works smoothly across different devices and conditions.
对于 iOS,Instruments 是一种非常有价值的工具,而在 Android 上,你应该学习使用 Android Studio Profiler。
¥For iOS, Instruments is an invaluable tool, and on Android you should learn to use the Android Studio Profiler.
但首先,确保开发模式关闭! 你应该在应用日志中看到 __DEV__ === false, development-level warning are OFF, performance optimizations are ON
。
¥But first, make sure that Development Mode is OFF! You should see __DEV__ === false, development-level warning are OFF, performance optimizations are ON
in your application logs.
使用系统跟踪分析 Android UI 性能
¥Profiling Android UI Performance with System Tracing
Android 支持 10k+ 不同的手机,并普遍支持软件渲染:不幸的是,框架架构和需要跨许多硬件目标进行推广意味着相对于 iOS,你免费获得的资源更少。但有时,有些事情是可以改进的 - 而且很多时候这根本不是原生代码的错!
¥Android supports 10k+ different phones and is generalized to support software rendering: the framework architecture and need to generalize across many hardware targets unfortunately means you get less for free relative to iOS. But sometimes, there are things you can improve -- and many times it's not native code's fault at all!
调试此卡顿的第一步是回答每个 16 毫秒帧期间你的时间都花在哪里的基本问题。为此,我们将使用 Android Studio 中的内置系统跟踪分析器。
¥The first step for debugging this jank is to answer the fundamental question of where your time is being spent during each 16ms frame. For that, we'll be using the built-in System Tracing profiler in the Android Studio.
1. 收集踪迹
¥ Collecting a trace
首先,通过 USB 将出现你要调查的卡顿的设备连接到你的计算机。在 Android Studio 中打开项目的 android
文件夹,在右上方窗格中选择你的设备,然后选择 以可分析的形式运行你的项目。
¥First, connect a device that exhibits the stuttering you want to investigate to your computer via USB. Open your project's android
folder in Android Studio, select your device in the top right pane, and run your project as profileable.
当你的应用构建为可分析的并在设备上运行后,将你的应用移至你想要分析的导航/动画之前的点,然后在 Android Studio Profiler 窗格中启动 "捕获系统活动" 任务。
¥When your app is built as profileable and is running on the device, get your app to the point right before the navigation/animation you want to profile and start the "Capture System Activities" task in the Android Studio Profiler pane.
一旦跟踪开始收集,就执行你关心的动画或交互。然后按 "停止录音"。你现在可以 直接在 Android Studio 中检查跟踪。或者,你可以在 "过去的录音" 窗格中选择它,按 "导出记录",然后在 Perfetto 等工具中打开它。
¥Once the trace starts collecting, perform the animation or interaction you care about. Then press "Stop recording". You can now inspect the trace directly in the Android Studio. Alternatively, you can select it in the "Past Recordings" pane, press "Export recording", and open it in a tool like Perfetto.
2. 读取踪迹
¥ Reading the trace
在 Android Studio 或 Perfetto 中打开跟踪后,你应该会看到类似以下内容:
¥After opening the trace in Android Studio or Perfetto, you should see something like this:
使用 WASD 键进行平移和缩放。
¥Use the WASD keys to strafe and zoom.
确切的 UI 可能不同,但无论你使用哪种工具,以下说明都将适用。
¥The exact UI might be different but the instructions below will apply regardless of the tool you're using.
选中屏幕右上角的此复选框以高亮 16ms 帧边界:
¥Check this checkbox at the top right of the screen to highlight the 16ms frame boundaries:
你应该看到斑马条纹,如上面的屏幕截图所示。如果不这样做,请尝试在其他设备上进行分析:众所周知,三星在显示垂直同步方面存在问题,而 Nexus 系列通常相当可靠。
¥You should see zebra stripes as in the screenshot above. If you don't, try profiling on a different device: Samsung has been known to have issues displaying vsyncs while the Nexus series is generally pretty reliable.
3. 找到你的进程
¥ Find your process
滚动直到看到封装名称(部分)。在本例中,我正在分析 com.facebook.adsmanager
,由于内核中愚蠢的线程名称限制,它显示为 book.adsmanager
。
¥Scroll until you see (part of) the name of your package. In this case, I was profiling com.facebook.adsmanager
, which shows up as book.adsmanager
because of silly thread name limits in the kernel.
在左侧,你将看到一组与右侧时间线行相对应的线程。为了我们的目的,我们关心一些线程:UI 线程(具有你的包名称或名称 UI 线程)、mqt_js
和 mqt_native_modules
。如果你在 Android 5+ 上运行,我们还关心渲染线程。
¥On the left side, you'll see a set of threads which correspond to the timeline rows on the right. There are a few threads we care about for our purposes: the UI thread (which has your package name or the name UI Thread), mqt_js
, and mqt_native_modules
. If you're running on Android 5+, we also care about the Render Thread.
-
用户界面线程。这是标准 android 测量/布局/绘制发生的地方。右侧的线程名称将是你的包名称(在我的案例中为 book.adsmanager)或 UI 线程。你在此线程上看到的事件应类似于以下内容,并且与
Choreographer
、traversals
和DispatchUI
有关:¥UI Thread. This is where standard android measure/layout/draw happens. The thread name on the right will be your package name (in my case book.adsmanager) or UI Thread. The events that you see on this thread should look something like this and have to do with
Choreographer
,traversals
, andDispatchUI
:
-
JS 线程。这是 JavaScript 执行的地方。线程名称将为
mqt_js
或<...>
,具体取决于设备上内核的协作程度。如果它没有名称,要识别它,请查找诸如JSCall
、Bridge.executeJSCall
等的内容:¥JS Thread. This is where JavaScript is executed. The thread name will be either
mqt_js
or<...>
depending on how cooperative the kernel on your device is being. To identify it if it doesn't have a name, look for things likeJSCall
,Bridge.executeJSCall
, etc:
-
原生模块线程。这是执行原生模块调用(例如
UIManager
)的地方。线程名称将为mqt_native_modules
或<...>
。要在后一种情况下识别它,请查找诸如NativeCall
、callJavaModuleMethod
和onBatchComplete
之类的内容:¥Native Modules Thread. This is where native module calls (e.g. the
UIManager
) are executed. The thread name will be eithermqt_native_modules
or<...>
. To identify it in the latter case, look for things likeNativeCall
,callJavaModuleMethod
, andonBatchComplete
:
-
奖金:渲染线程。如果你使用的是 Android L (5.0) 及更高版本,你的应用中还将有一个渲染线程。该线程生成用于绘制 UI 的实际 OpenGL 命令。线程名称将为
RenderThread
或<...>
。要在后一种情况下识别它,请查找诸如DrawFrame
和queueBuffer
之类的内容:¥Bonus: Render Thread. If you're using Android L (5.0) and up, you will also have a render thread in your application. This thread generates the actual OpenGL commands used to draw your UI. The thread name will be either
RenderThread
or<...>
. To identify it in the latter case, look for things likeDrawFrame
andqueueBuffer
:
找出罪魁祸首
¥Identifying a culprit
流畅的动画应该如下所示:
¥A smooth animation should look something like the following:
每次颜色变化都是一帧 - 请记住,为了显示一帧,我们所有的 UI 工作都需要在 16 毫秒周期结束时完成。请注意,没有线程在靠近帧边界的地方工作。像这样的应用渲染速度为 60 FPS。
¥Each change in color is a frame -- remember that in order to display a frame, all our UI work needs to be done by the end of that 16ms period. Notice that no thread is working close to the frame boundary. An application rendering like this is rendering at 60 FPS.
但是,如果你注意到了 chop,你可能会看到类似这样的内容:
¥If you noticed chop, however, you might see something like this:
请注意,JS 线程几乎一直在执行,并且跨帧边界!此应用不以 60 FPS 渲染。这样的话,问题就出在 JS 上。
¥Notice that the JS thread is executing almost all the time, and across frame boundaries! This app is not rendering at 60 FPS. In this case, the problem lies in JS.
你可能还会看到类似这样的内容:
¥You might also see something like this:
在这种情况下,UI 和渲染线程是跨帧边界工作的线程。我们尝试在每一帧上渲染的 UI 需要完成太多工作。在这种情况下,问题在于正在渲染的原生视图。
¥In this case, the UI and render threads are the ones that have work crossing frame boundaries. The UI that we're trying to render on each frame is requiring too much work to be done. In this case, the problem lies in the native views being rendered.
此时,你将获得一些非常有用的信息来指导你的后续步骤。
¥At this point, you'll have some very helpful information to inform your next steps.
解决 JavaScript 问题
¥Resolving JavaScript issues
如果你发现了 JS 问题,请在你正在执行的特定 JS 中寻找线索。在上面的场景中,我们看到 RCTEventEmitter
每帧被调用多次。下面是上面跟踪中 JS 线程的放大图:
¥If you identified a JS problem, look for clues in the specific JS that you're executing. In the scenario above, we see RCTEventEmitter
being called multiple times per frame. Here's a zoom-in of the JS thread from the trace above:
这似乎不对。为什么它被如此频繁地调用?它们实际上是不同的事件吗?这些问题的答案可能取决于你的产品代码。很多时候,你会想要研究 shouldComponentUpdate。
¥This doesn't seem right. Why is it being called so often? Are they actually different events? The answers to these questions will probably depend on your product code. And many times, you'll want to look into shouldComponentUpdate.
解决原生 UI 问题
¥Resolving native UI Issues
如果你发现原生 UI 问题,通常有两种情况:
¥If you identified a native UI problem, there are usually two scenarios:
-
你尝试绘制每一帧的 UI 涉及 GPU 的太多工作,或者
¥the UI you're trying to draw each frame involves too much work on the GPU, or
-
你在动画/交互期间构建新的 UI(例如,在滚动期间加载新内容)。
¥You're constructing new UI during the animation/interaction (e.g. loading in new content during a scroll).
GPU 工作过多
¥Too much GPU work
在第一个场景中,你将看到具有 UI 线程和/或渲染线程的跟踪,如下所示:
¥In the first scenario, you'll see a trace that has the UI thread and/or Render Thread looking like this:
请注意 DrawFrame
中花费的大量时间跨越了帧边界。这是等待 GPU 耗尽前一帧的命令缓冲区所花费的时间。
¥Notice the long amount of time spent in DrawFrame
that crosses frame boundaries. This is time spent waiting for the GPU to drain its command buffer from the previous frame.
为了缓解这种情况,你应该:
¥To mitigate this, you should:
-
研究使用
renderToHardwareTextureAndroid
来处理正在动画/转换的复杂静态内容(例如Navigator
幻灯片/Alpha 动画)¥investigate using
renderToHardwareTextureAndroid
for complex, static content that is being animated/transformed (e.g. theNavigator
slide/alpha animations) -
确保你没有使用
needsOffscreenAlphaCompositing
,该选项默认处于禁用状态,因为在大多数情况下它会大大增加 GPU 上的每帧负载。¥make sure that you are not using
needsOffscreenAlphaCompositing
, which is disabled by default, as it greatly increases the per-frame load on the GPU in most cases.
在 UI 线程上创建新视图
¥Creating new views on the UI thread
在第二种情况下,你会看到类似这样的内容:
¥In the second scenario, you'll see something more like this:
请注意,首先 JS 线程会思考一下,然后你会看到原生模块线程上完成了一些工作,然后是 UI 线程上昂贵的遍历。
¥Notice that first the JS thread thinks for a bit, then you see some work done on the native modules thread, followed by an expensive traversal on the UI thread.
没有快速的方法可以缓解这种情况,除非你能够将创建新 UI 推迟到交互之后,或者你能够简化正在创建的 UI。React Native 团队正在为此开发基础设施级别的解决方案,该解决方案将允许在主线程之外创建和配置新的 UI,从而使交互能够顺利地继续。
¥There isn't a quick way to mitigate this unless you're able to postpone creating new UI until after the interaction, or you are able to simplify the UI you're creating. The react native team is working on an infrastructure level solution for this that will allow new UI to be created and configured off the main thread, allowing the interaction to continue smoothly.
查找原生 CPU 热点
¥Finding native CPU hotspots
如果问题似乎出在原生方面,你可以使用 CPU 热点分析器 来获取有关正在发生的事情的更多详细信息。打开 Android Studio Profiler 面板并选择 "查找 CPU 热点(Java/Kotlin 方法记录)"。
¥If the problem seems to be on the native side, you can use the CPU hotspot profiler to get more details on what's happening. Open the Android Studio Profiler panel and select "Find CPU Hotspots (Java/Kotlin Method Recording)".
确保你选择 "查找 CPU 热点(Java/Kotlin 记录)" 而不是 "查找 CPU 热点(调用堆栈示例)"。它们有相似的图标,但做不同的事情。
¥Make sure you select "Find CPU Hotspots (Java/Kotlin Recording)" rather than "Find CPU Hotspots (Callstack Sample)". They have similar icons but do different things.
执行交互并按 "停止录音"。记录占用大量资源,因此请保持交互简短。然后,你可以在 Android Studio 中检查生成的跟踪,也可以将其导出并在 Firefox Profiler 等在线工具中打开。
¥Perform the interactions and press "Stop recording". Recording is resource-intensive, so keep the interaction short. You can then either inspect the resulting trace in the Android Studio or export it and open it in an online tool like Firefox Profiler.
与 System Trace 不同,CPU 热点分析速度很慢,因此无法为你提供准确的测量结果。但是,它应该让你了解正在调用哪些原生方法,以及在每帧中时间按比例花费在哪里。
¥Unlike System Trace, CPU hotspot profiling is slow so it won't give you accurate measurements. However, it should give you an idea of what native methods are being called, and where the time is being spent proportionally during each frame.