您可以配置 系统跟踪 以在短时间内捕获应用的 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 调用本机游戏代码 或使用本机 热 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 游戏开发者峰会的演示