您可以配置系统跟踪,以捕获应用在短时间内的 CPU 和线程使用情况。然后,您可以使用系统跟踪的输出报告来提高游戏的性能。
设置基于游戏的系统跟踪
Systrace 工具以两种形式提供:
Systrace 是一款低层工具,能够:
- 提供真实数据。Systrace 直接从内核捕获输出,因此它捕获的指标几乎与一系列系统调用所报告的指标完全相同。
- 资源消耗少。Systrace 会将数据流式传输到内存缓冲区中,因此所造成的设备开销非常低,通常少于 1%。
最佳设置
请务必为该工具提供一组合理的参数:
- 类别:可为基于游戏的系统跟踪启用的最佳类别组合为 {
sched
,freq
,idle
,am
,wm
,gfx
,view
,sync
,binder_driver
,hal
,dalvik
}。 缓冲区空间:一般原则是,每个 CPU 内核分配 10 MB 缓冲区,可支持跟踪大约 20 秒。例如,如果设备配有两个 4 核 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
:activity 管理器binder_driver
:Binder 内核驱动程序dalvik
:Dalvik 虚拟机freq
: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 而不是单声道。(仅在使用 Unity 时适用)。IL2CPP 可为 C# 脚本提供更好的执行性能。
- 将游戏设为多线程模式。如需了解详情,请参阅帧速率稳定性一节。
帧速率稳定性
稳定的帧速率是影响游戏体验最重要的元素之一。为更加轻松地实现稳定的帧速率,请遵循本节中讨论的优化方法。
多线程处理
在面向多个平台进行开发时,通常会将游戏中的所有 activity 放在一个线程中。尽管这种执行方法很容易在许多游戏引擎中实现,但在 Android 设备上运行时,这却远不是最佳选择。因此,单线程游戏通常加载速度缓慢,并且帧速率不稳定。
图 1 所示的 Systrace 显示了一次仅在一个 CPU 上运行的游戏的典型行为:
如需提高游戏性能,请将游戏设为多线程模式。通常,最好的模型是采用 2 个线程:
- 一个游戏线程,该线程包含游戏的主要模块,并负责发送渲染命令。
- 一个渲染线程,该线程会接收渲染命令并将其转译为图形命令,供设备 GPU 用于显示场景。
Vulkan API 扩展了此模型,因为它能够并行推送 2 个公共缓冲区。使用此功能,您可以跨多个 CPU 分配多个渲染线程,从而进一步缩短场景渲染时间。
您也可以进行一些特定于引擎的更改,以提升游戏的多线程处理性能:
- 如果您要使用 Unity 游戏引擎开发游戏,请启用 Multithreaded Rendering 和 GPU Skinning 选项。
- 如果您要使用自定义渲染引擎,请确保渲染命令管道和图形命令管道正确对齐;否则,可能会导致游戏场景显示延迟。
应用这些更改之后,您将会看到游戏同时占用至少 2 个 CPU,如图 2 所示:
界面元素加载
在开发功能丰富的游戏时,很容易会想要同时向玩家展示许多不同的选项和操作。但是,为了保持帧速率的稳定,必须要考虑到移动屏幕的尺寸相对较小,因此应尽可能保持界面设计简单。
图 3 中所示的 Systrace 报告是一个界面帧的示例,该帧试图渲染的元素数量超出了移动设备的能力范围。
理想状态是将界面更新时间缩短至 2 到 3 毫秒。您可以通过执行类似以下的优化来实现这种快速更新:
- 仅更新屏幕上发生了移动的元素。
- 限制界面纹理和图层的数量。考虑将使用相同材质的图形调用合并起来,例如着色器和纹理。
- 将元素动画运算工作转移到 GPU 上。
- 执行更激进的视锥体和遮挡剔除。
- 若有可能,使用 Vulkan API 执行绘制操作。Vulkan 上的绘制调用开销较低。
功耗
即使在进行了上一节中讨论的优化之后,您可能仍会发现您的游戏在开始后的 45 至 50 分钟内帧速率会下降。此外,随着时间的推移,设备可能会开始发热并消耗更多电量。
在许多情况下,这种不良的发热和高功耗现象与游戏工作负载在设备 CPU 之间的分配方式有关。如需降低游戏功耗,请采用后面几节中介绍的最佳做法。
将内存占用量大的线程放在一个 CPU 上
在许多移动设备上,L1 缓存驻留在特定的 CPU 上,L2 缓存驻留在共享时钟的一组 CPU 上。为实现最高的 L1 缓存命中率,最好将游戏的主线程以及其他任何内存占用量大的线程放在单个 CPU 上运行。
将持续时间较短的工作转移到低功耗的 CPU 上
包括 Unity 在内的大多数游戏引擎都知道将工作器线程运算转移到与游戏主线程不同的 CPU 上。但是,引擎并不知道设备的具体架构,因此无法像您一样准确地预测游戏的工作负载。
大多数系统芯片设备都具有至少 2 个共享时钟,一个用于设备的快速 CPU,另一个用于设备的慢速 CPU。这种架构造成的一个结果就是,如果有一个快速 CPU 需要以最大速率运行,那么所有其他快速 CPU 也必须以最大速率运行。
图 4 中所示的示例报告就展示了一个利用快速 CPU 的游戏。不过,这种高活动水平会很快产生大量的功耗和热量。
为降低总体功耗,最好向调度器建议将持续时间较短的工作(如加载音频、运行工作器线程和执行 Choreographer)转移到设备上的一组慢速 CPU 上。在保持所需帧速率的前提下,将尽可能多的此类工作转移到慢速 CPU 上。
大多数设备将慢速 CPU 列在快速 CPU 之前,但您不能假定设备的 SOC 会使用这个顺序。如需检查这一点,请运行与上述命令类似的命令 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,从而可能以意想不到的方式影响游戏。如果游戏中包含复杂图形、大量计算或持续网络活动,则更有可能遇到问题。
您可以使用 Thermal API 来监控设备上的温度变化,并采取相应的措施以降低耗电量和设备温度。当设备报告热应力时,应减少正在进行的活动,以减少耗电量。例如,降低帧速率或减少多边形曲面细分。
首先,声明 PowerManager
对象,并在 onCreate()
方法中对其进行初始化。向该对象添加热状态监听器。
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 Frame Pacing API 集成到游戏中
Android Frame Pacing API 可帮助您执行帧交换并定义交换间隔,让您的游戏可以保持更稳定的帧速率。
降低游戏的非界面资源的分辨率
现代移动设备的屏幕所包含的像素远远超过了玩家可以识别的像素,因此可以降低采样率,使 5 个甚至 10 个连续的像素都包含同一种颜色。考虑到大多数显示缓存的结构,最好仅沿一个维度降低分辨率。
但是请不要降低游戏界面元素的分辨率。请务必保留这些元素的线宽,以便保持足够大的触摸目标尺寸来满足所有玩家的需求。
渲染平滑度
当 SurfaceFlinger 连接到某个显示缓冲区以显示游戏中的场景时,CPU 活动会瞬间增加。如果这些 CPU 活动峰值出现地不均匀,游戏就可能会发生卡顿。图 6 中的示意图描述了发生这种情况的原因:
如果帧开始绘制的时间太晚,即使仅晚了几毫秒,它也可能会错过下一个显示窗口。然后,帧必须等到下一个 Vsync 显示(以 30 FPS 运行游戏时为 33 毫秒),这会让玩家感觉到非常明显的延迟。
为了解决这个问题,可以使用 Android Frame Pacing API,它会始终在 VSync 波前显示一个新的帧。
内存状态
长时间运行游戏时,设备可能会出现内存不足的问题。
在这种情况下,请检查 Systrace 报告中的 CPU 活动,并查看系统调用 kswapd
守护进程的频率。如果在游戏执行过程中有许多调用,最好仔细查看一下游戏如何管理和清理内存。
如需了解详情,请参阅在游戏中高效管理内存。
线程状态
在 Systrace 报告中的典型元素间导航时,您可以从报告中选择某个线程,查看该线程在每种可能的线程状态下花费的时间,具体如图 7 中所示:
如图 7 所示,您可能会发现游戏线程处于“running”或“runnable”状态的频率低于应有的频率。下面列出了给定线程间歇性地变为异常状态的几种常见原因:
- 如果线程长时间处于休眠状态,则可能会遇到锁争用或等待 GPU 活动的情况。
- 如果某个线程在 I/O 上不断被阻塞,则表示您一次性从磁盘读取了太多数据,或者游戏正在抖动。
其他资源
如需详细了解如何提高游戏性能,请参阅下面列出的其他资源:
视频
- 2018 年 Android 游戏开发者峰会上的 Systrace for Games 演讲