您可以配置 系统跟踪 以在短时间内捕获应用的 CPU 和线程分析。然后,您可以使用系统跟踪生成的输出报告来提高游戏的性能。
设置基于游戏的系统跟踪
Systrace 工具可以通过两种方式获得
Systrace 是一种低级工具,它
- 提供真实数据。Systrace 直接从内核捕获输出,因此它捕获的指标与一系列系统调用报告的指标几乎相同。
- 消耗的资源很少。Systrace 对设备引入的开销非常低,通常低于 1%,因为它将数据流式传输到内存缓冲区中。
最佳设置
为工具提供合理的参数集非常重要
- 类别:为基于游戏的系统跟踪启用的最佳类别集为:{
sched
、freq
、idle
、am
、wm
、gfx
、view
、sync
、binder_driver
、hal
、dalvik
}。 缓冲区大小:一般规则是,每个 CPU 内核 10 MB 的缓冲区大小允许大约 20 秒的跟踪。例如,如果设备有两个四核 CPU(总共 8 个内核),则传递到
systrace
程序的适当值为 80,000 KB(80 MB)。如果你的游戏执行大量上下文切换,请将缓冲区增加到每个 CPU 内核 15 MB。
自定义事件:如果你定义自定义事件以在你的游戏中捕获,请启用
-a
标志,这允许 Systrace 在输出报告中包含这些自定义事件。
如果你正在使用 systrace
命令行程序,请使用以下命令捕获应用最佳实践类别集、缓冲区大小和自定义事件的系统跟踪
python systrace.py -a com.example.myapp -b 80000 -o my_systrace_report.html \ sched freq idle am wm gfx view sync binder_driver hal dalvik
如果你正在设备上使用 Systrace 系统应用,请完成以下步骤以捕获应用最佳实践类别集、缓冲区大小和自定义事件的系统跟踪
启用跟踪可调试应用选项。
要使用此设置,设备必须有 256 MB 或 512 MB 可用(取决于 CPU 是否有 4 个或 8 个内核),并且每个 64 MB 的内存块必须作为连续块可用。
选择类别,然后启用以下列表中的类别
am
:活动管理器binder_driver
:Binder 内核驱动程序dalvik
:Dalvik VMfreq
:CPU 频率gfx
:图形hal
:硬件模块idle
:CPU 空闲sched
:CPU 调度sync
:同步view
:视图系统wm
:窗口管理器
启用记录跟踪。
加载你的游戏。
执行游戏中与你想要衡量其设备性能的游戏玩法相对应的交互。
在游戏中遇到不良行为后不久,关闭系统跟踪。
你已捕获进一步分析问题所需的性能统计信息。
为了节省磁盘空间,设备上的系统跟踪将文件保存为压缩的跟踪格式(*.ctrace
)。生成报告时要解压缩此文件,请使用命令行程序并包含 --from-file
选项
python systrace.py --from-file=/data/local/traces/my_game_trace.ctrace \ -o my_systrace_report.html
改进特定性能领域
本节重点介绍移动游戏中的一些常见性能问题,并介绍如何识别和改进游戏的这些方面。
加载速度
玩家希望尽快进入游戏的行动,因此尽可能提高游戏的加载时间非常重要。以下措施通常有助于缩短加载时间
- 执行延迟加载。如果在游戏中的连续场景或关卡中使用相同的资源,则仅加载一次这些资源。
- 减小资源的大小。这样,你就可以将这些资源的未压缩版本与游戏的 APK 捆绑在一起。
- 使用磁盘高效的压缩方法。此类方法的一个示例是 zlib。
- 使用 IL2CPP 而不是 mono。(仅适用于你使用 Unity 的情况。)IL2CPP 为你的 C# 脚本提供了更好的执行性能。
- 使你的游戏支持多线程。有关更多详细信息,请参阅帧速率一致性部分。
帧速率一致性
游戏体验中最重要的因素之一是实现一致的帧速率。为了使这一目标更容易实现,请遵循本节中讨论的优化技巧。
多线程
在开发面向多个平台的游戏时,自然而然地会将所有游戏活动都放在单个线程中执行。虽然这种执行方式在许多游戏引擎中易于实现,但在 Android 设备上运行时,它远非最佳选择。因此,单线程游戏通常加载速度缓慢且帧率不稳定。
图 1 中所示的 Systrace 显示了游戏一次只在一个 CPU 上运行的典型行为。
为了提高游戏性能,请将游戏设计为多线程。通常,最佳模型是使用 2 个线程。
- 游戏线程,其中包含游戏的主要模块并发送渲染命令。
- 渲染线程,接收渲染命令并将其转换为设备 GPU 可以用来显示场景的图形命令。
鉴于 Vulkan API 能够并行推送 2 个常用缓冲区,它扩展了此模型。使用此功能,您可以将多个渲染线程分布到多个 CPU 上,从而进一步缩短场景的渲染时间。
您还可以进行一些引擎特定的更改来增强游戏的 多线程性能。
- 如果您使用 Unity 游戏引擎开发游戏,请启用多线程渲染和GPU 骨骼蒙皮选项。
- 如果您使用自定义渲染引擎,请确保渲染命令管道和图形命令管道正确对齐;否则,可能会导致显示游戏场景时出现延迟。
应用这些更改后,您应该会看到游戏同时占用至少 2 个 CPU,如图 2 所示。
UI 元素加载
在创建功能丰富的游戏时,很容易希望同时向玩家展示许多不同的选项和操作。但是,为了保持稳定的帧率,务必考虑移动设备显示屏的相对较小尺寸,并使 UI 尽可能简单。
图 3 中所示的 Systrace 报告是一个 UI 帧的示例,该帧尝试渲染相对于移动设备功能而言过多的元素。
一个好的目标是将 UI 更新时间减少到 2-3 毫秒。您可以通过执行类似于以下内容的优化来实现如此快速的更新。
- 仅更新屏幕上已移动的元素。
- 限制 UI 纹理和图层的数量。考虑组合使用相同材质的图形调用,例如着色器和纹理。
- 将元素动画操作延迟到 GPU。
- 执行更积极的视锥体剔除和遮挡剔除。
- 如果可能,请使用 Vulkan API 执行绘制操作。Vulkan 上的绘制调用开销较低。
功耗
即使在进行了上一节中讨论的优化之后,您也可能会发现游戏的帧率在游戏开始后的 45-50 分钟内下降。此外,设备可能会随着时间的推移开始发热并消耗更多电量。
在许多情况下,这种不良的热量和功耗情况与游戏的工作负载如何在设备的 CPU 上分配有关。为了提高游戏的功耗效率,请应用以下各节中显示的最佳实践。
将内存密集型线程保留在一个 CPU 上
在许多移动设备上,L1 缓存驻留在特定的 CPU 上,而 L2 缓存驻留在共享时钟的 CPU 集上。为了最大限度地提高 L1 缓存命中率,通常最好将游戏的主线程以及任何其他内存密集型线程保持在一个 CPU 上运行。
将短时工作延迟到低功耗 CPU
包括 Unity 在内的多数游戏引擎都知道将工作线程操作延迟到与游戏主线程不同的 CPU 上。但是,引擎并不知道设备的具体架构,也无法像您一样预测游戏的工作负载。
大多数片上系统设备至少有 2 个共享时钟,一个用于设备的快速 CPU,另一个用于设备的慢速 CPU。此架构的一个结果是,如果一个快速 CPU 需要以最大速度运行,则所有其他快速 CPU 也将以最大速度运行。
图 4 中所示的示例报告显示了一个利用快速 CPU 的游戏。但是,这种高活动水平会很快产生大量的能量和热量。
为了降低整体功耗,最好建议调度程序将较短时间的工作(例如加载音频、运行工作线程和执行协调器)延迟到设备上的慢速 CPU 集。在保持所需帧率的同时,尽可能地将此类工作转移到慢速 CPU 上。
大多数设备将慢速 CPU 列在快速 CPU 之前,但您不能假设设备的 SOC 使用此顺序。要检查,请在 GitHub 上运行类似于此CPU 拓扑发现代码中显示的命令。
了解设备上的哪些 CPU 是慢速 CPU 后,您可以为短时线程声明关联性,设备的调度程序会遵循此关联性。为此,请在每个线程中添加以下代码。
#include <sched.h> #include <sys/types.h> #include <unistd.h> pid_t my_pid; // PID of the process containing your thread. // Assumes that cpu0, cpu1, cpu2, and cpu3 are the "slow CPUs". cpu_set_t my_cpu_set; CPU_ZERO(&my_cpu_set); CPU_SET(0, &my_cpu_set); CPU_SET(1, &my_cpu_set); CPU_SET(2, &my_cpu_set); CPU_SET(3, &my_cpu_set); sched_setaffinity(my_pid, sizeof(cpu_set_t), &my_cpu_set);
热应力
当设备过热时,它们可能会限制 CPU 和/或 GPU 的性能,这可能会以意想不到的方式影响游戏。包含复杂图形、繁重计算或持续网络活动的游戏更有可能遇到问题。
使用热 API 监控设备上的温度变化,并采取措施以保持较低的功耗和较低的设备温度。当设备报告热应力时,减少正在进行的活动以降低功耗。例如,降低帧率或多边形细分。
首先,在 onCreate()
方法中声明 PowerManager
对象并进行初始化。向对象添加热状态侦听器。
Kotlin
class MainActivity : AppCompatActivity() { lateinit var powerManager: PowerManager override fun onCreate(savedInstanceState: Bundle?) { powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager powerManager.addThermalStatusListener(thermalListener) } }
Java
public class MainActivity extends AppCompatActivity { PowerManager powerManager; @Override protected void onCreate(Bundle savedInstanceState) { ... powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); powerManager.addThermalStatusListener(thermalListener); } }
定义侦听器检测状态更改时要采取的操作。如果您的游戏使用 C/C++,请在 onThermalStatusChanged()
中向热状态级别添加代码,以使用 JNI 调用您的原生游戏代码或使用原生的Thermal API。
Kotlin
val thermalListener = object : PowerManager.OnThermalStatusChangedListener() { override fun onThermalStatusChanged(status: Int) { when (status) { PowerManager.THERMAL_STATUS_NONE -> { // No thermal status, so no action necessary } PowerManager.THERMAL_STATUS_LIGHT -> { // Add code to handle light thermal increase } PowerManager.THERMAL_STATUS_MODERATE -> { // Add code to handle moderate thermal increase } PowerManager.THERMAL_STATUS_SEVERE -> { // Add code to handle severe thermal increase } PowerManager.THERMAL_STATUS_CRITICAL -> { // Add code to handle critical thermal increase } PowerManager.THERMAL_STATUS_EMERGENCY -> { // Add code to handle emergency thermal increase } PowerManager.THERMAL_STATUS_SHUTDOWN -> { // Add code to handle immediate shutdown } } } }
Java
PowerManager.OnThermalStatusChangedListener thermalListener = new PowerManager.OnThermalStatusChangedListener () { @Override public void onThermalStatusChanged(int status) { switch (status) { case PowerManager.THERMAL_STATUS_NONE: // No thermal status, so no action necessary break; case PowerManager.THERMAL_STATUS_LIGHT: // Add code to handle light thermal increase break; case PowerManager.THERMAL_STATUS_MODERATE: // Add code to handle moderate thermal increase break; case PowerManager.THERMAL_STATUS_SEVERE: // Add code to handle severe thermal increase break; case PowerManager.THERMAL_STATUS_CRITICAL: // Add code to handle critical thermal increase break; case PowerManager.THERMAL_STATUS_EMERGENCY: // Add code to handle emergency thermal increase break; case PowerManager.THERMAL_STATUS_SHUTDOWN: // Add code to handle immediate shutdown break; } } };
触控到显示延迟
尽可能快地渲染帧的游戏会创建 GPU 绑定场景,其中帧缓冲区会变得过度填充。CPU 需要等待 GPU,这会导致玩家输入和输入在屏幕上生效之间出现明显的延迟。
要确定是否可以改善游戏的帧速率,请完成以下步骤。
- 生成包含
gfx
和input
类别的 Systrace 报告。这些类别包含确定触控到显示延迟的特别有用的度量。 检查 Systrace 报告的
SurfaceView
部分。过度填充的缓冲区会导致挂起的缓冲区绘制数量在 1 和 2 之间振荡,如图 5 所示。图 5. 显示周期性地过于满而无法接受绘制命令的过度填充缓冲区的 Systrace 报告
为了减轻帧速率中的这种不一致性,请完成以下各节中描述的操作。
将 Android 帧速率 API 集成到您的游戏中
Android 帧速率 API 可帮助您执行帧交换并定义交换间隔,以便游戏保持更稳定的帧率。
降低游戏非 UI 资产的分辨率
现代移动设备上的显示屏包含比玩家可以处理的更多的像素,因此可以进行下采样,以便 5 个甚至 10 个像素的运行都包含一种颜色。鉴于大多数显示缓存的结构,最好只沿一个维度降低分辨率。
但是,不要降低游戏 UI 元素的分辨率。务必保留这些元素上的线条粗细,以便为所有玩家维持足够大的触控目标大小。
渲染流畅度
当 SurfaceFlinger 锁定显示缓冲区以显示游戏中的场景时,CPU 活动会暂时增加。如果 CPU 活动的这些峰值不均匀地出现,则可能会在游戏中看到卡顿。图 6 中的图表描述了发生这种情况的原因。
如果帧开始绘制得太晚,即使只有几毫秒,它也可能会错过下一个显示窗口。然后,帧必须等到下一个 Vsync 才能显示(以 30 FPS 运行游戏时为 33 毫秒),这会导致从玩家的角度来看出现明显的延迟。
为了解决这种情况,请使用Android 帧速率 API,它始终在 VSync 波前上呈现新帧。
内存状态
在长时间运行游戏时,设备可能会遇到内存不足错误。
在这种情况下,请检查 Systrace 报告中的 CPU 活动,并查看系统多久调用一次 kswapd
守护程序。如果在游戏的执行过程中有很多调用,最好仔细查看游戏如何管理和清理内存。
有关更多信息,请参阅在游戏中有效管理内存。
线程状态
在浏览 Systrace 报告的典型元素时,您可以通过在报告中选择线程(如图 7 所示)来查看给定线程在每个可能的线程状态中花费的时间。
如图 7 所示,您可能会发现游戏的线程不像预期的那样经常处于“运行”或“可运行”状态。以下列表显示了给定线程可能会周期性地转换到异常状态的几个常见原因。
- 如果线程睡眠时间过长,可能是由于锁争用或等待 GPU 活动。
- 如果线程持续被 I/O 阻塞,要么是每次从磁盘读取的数据量过大,要么是游戏出现抖动。
其他资源
要了解有关提高游戏性能的更多信息,请参阅以下其他资源
视频
- Systrace for Games 来自 2018 年 Android 游戏开发者峰会的演示文稿