配置系统跟踪

您可以配置 系统跟踪 以在短时间内捕获应用的 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 系统应用,请完成以下步骤捕获应用最佳实践类别集、缓冲区大小和自定义事件的系统跟踪

  1. 启用**跟踪可调试应用**选项。

    要使用此设置,设备必须有 256 MB 或 512 MB 可用(取决于 CPU 具有 4 个内核还是 8 个内核),并且每个 64 MB 的内存块必须作为连续块可用。

  2. 选择**类别**,然后启用以下列表中的类别

    • am:活动管理器
    • binder_driver:Binder 内核驱动程序
    • dalvik:Dalvik VM
    • freq:CPU 频率
    • gfx:图形
    • hal:硬件模块
    • idle:CPU 空闲
    • sched:CPU 调度
    • sync:同步
    • view:视图系统
    • wm:窗口管理器
  3. 启用**记录跟踪**。

  4. 加载你的游戏。

  5. 执行游戏中的交互,这些交互对应于你想要测量其设备性能的游戏玩法。

  6. 在遇到游戏中不希望的行为后不久,关闭系统跟踪。

你已捕获了进一步分析问题的性能统计信息。

为了节省磁盘空间,设备上的系统跟踪会以压缩的跟踪格式 (*.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 上运行的游戏的典型行为

Diagram of threads
within a system trace

图 1. 单线程游戏的 Systrace 报告

要提高游戏的性能,使你的游戏多线程。通常,最佳模型是使用 2 个线程

  • 游戏线程,其中包含游戏的核心模块,并发送渲染命令。
  • 渲染线程,它接收渲染命令,并将它们转换为设备的 GPU 可以用来显示场景的图形命令。

考虑到 Vulkan API 可以并行推送 2 个公共缓冲区,它扩展了此模型。使用此功能,你可以将多个渲染线程分布在多个 CPU 上,从而进一步缩短场景的渲染时间。

您也可以进行一些引擎特定的更改,以增强游戏的 多线程性能。

  • 如果您使用 Unity 游戏引擎开发游戏,请启用 **多线程渲染** 和 **GPU 骨骼动画** 选项。
  • 如果您使用自定义渲染引擎,请确保渲染命令管道和图形命令管道正确对齐;否则,可能会导致游戏场景显示延迟。

应用这些更改后,您应该看到游戏同时占用至少 2 个 CPU,如图 2 所示。

Diagram of threads
within a system trace

图 2. 多线程游戏的 Systrace 报告

UI 元素加载

Diagram of a frame
  stack within a system trace
图 3. 同时渲染数十个 UI 元素的游戏的 Systrace 报告

在创建功能丰富的游戏时,可能会想同时向玩家展示许多不同的选项和操作。但是,为了保持一致的帧率,重要的是要考虑移动显示屏的相对较小尺寸,并尽可能简化 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 的游戏。但是,这种高活动水平会很快产生大量的能量和热量。

Diagram of threads
within a system trace

图 4. 显示将线程非最佳分配到设备 CPU 的 Systrace 报告

为了降低总体功耗,最好建议调度程序将较短的任务(例如加载音频、运行工作线程和执行协调器)推迟到设备上的慢速 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,这会导致玩家输入与输入在屏幕上生效之间出现明显的延迟。

要确定是否可以提高游戏的帧速率,请完成以下步骤。

  1. 生成包含 gfxinput 类别的 Systrace 报告。这些类别包含确定触控到显示延迟的特别有用的度量。
  2. 检查 Systrace 报告的 SurfaceView 部分。过载的缓冲区会导致挂起的缓冲区绘制数量在 1 和 2 之间振荡,如图 5 所示。

    Diagram of
buffer queue within a system trace

    图 5. 显示定期过载以至于无法接受绘制命令的缓冲区的 Systrace 报告

为了减轻帧速率中的这种不一致,请完成以下部分中描述的操作。

将 Android 帧速率 API 集成到您的游戏中

Android 帧速率 API 可帮助您执行帧交换并定义交换间隔,以便游戏保持更一致的帧率。

降低游戏非 UI 资产的分辨率

现代移动设备上的显示屏包含的像素数量远超玩家的处理能力,因此可以对其进行下采样,以便 5 个甚至 10 个像素的运行都包含一种颜色。考虑到大多数显示缓存的结构,最好 **只沿一个维度降低分辨率**。

但是,不要降低游戏 UI 元素的分辨率。对于所有玩家来说,保留这些元素的线宽以保持 足够大的触摸目标大小 非常重要。

渲染平滑度

当 SurfaceFlinger 挂接到显示缓冲区以在游戏中显示场景时,CPU 活动会暂时增加。如果 CPU 活动的这些峰值不均匀地发生,则可能会在游戏中看到卡顿。图 6 中的图表描述了发生这种情况的原因。

Diagram of frames
missing a Vsync window because they started drawing too late

图 6. 显示帧如何错过 Vsync 的 Systrace 报告

如果帧的绘制开始过晚,即使只有几毫秒,它也可能错过下一个显示窗口。然后,帧必须等到下一个 Vsync 才能显示(以 30 FPS 运行游戏时为 33 毫秒),这会导致玩家明显感觉到延迟。

为了解决这种情况,请使用 Android 帧速率 API ,它始终在 VSync 波前上呈现新帧。

内存状态

长时间运行游戏时,设备可能会遇到内存不足错误。

在这种情况下,请检查 Systrace 报告中的 CPU 活动,并查看系统调用 kswapd 守护程序的频率。如果在游戏执行期间有许多调用,最好仔细查看游戏如何管理和清理内存。

有关详细信息,请参阅 在游戏中有效地管理内存

线程状态

在浏览 Systrace 报告的典型元素时,您可以通过在报告中选择线程(如图 7 所示)来查看给定线程在 每个可能的线程状态 中花费的时间量。

Diagram of a
Systrace report

图 7. 显示选择线程会导致报告显示该线程的状态摘要的 Systrace 报告

如图 7 所示,您可能会发现游戏的线程不像应该的那样经常处于“运行”或“可运行”状态。以下列表显示了给定线程可能定期过渡到异常状态的几个常见原因。

  • 如果线程长时间休眠,它可能正在遭受锁争用或正在等待 GPU 活动。
  • 如果线程始终在 I/O 上被阻塞,则您要么一次从磁盘读取的数据过多,要么游戏正在发生抖动。

其他资源

要了解有关提高游戏性能的更多信息,请参阅以下其他资源。

视频