Android 遊戲最佳化的最佳做法

Stay organized with collections Save and categorize content based on your preferences.

遊戲的載入時間短、影格速率穩定且輸入回應可靠,可提高玩家玩遊戲的意願。

如果您有針對電腦或遊戲主機開發遊戲的經驗,可能會發現,相對於行動裝置,這些裝置在 GPU 大小和快閃記憶體效能上大不相同。系統結構的這個差異在於難以預測 Android 遊戲的效能。

本指南可協助您最佳化遊戲,讓多種執行遊戲的 Android 裝置能盡量發揮效能。本指南特別說明如何設定 Android 遊戲的系統追蹤功能。接著,本指南會說明如何使用系統追蹤中的輸出報表,協助您將遊戲效能的特定方面納入考量。

設定遊戲型系統追蹤

Systrace 工具既是指令列程式,也是裝置端服務,能在短時間內擷取應用程式的 CPU 和執行緒設定檔。您可以運用 Systrace 報表中顯示的結果,進一步瞭解您的遊戲在 Android 上的表現,以及找出改善遊戲效率和回應速度的最佳方式。

Systrace 是一種非常低階的工具,具備以下優點:

  • 提供真值。Systrace 直接從核心擷取輸出內容,因此擷取的指標會與一系列系統呼叫回報的結果幾乎相同。
  • 耗用的資源較少。Systrace 在裝置上引入極低的負載,通常不到 1%,因為會將資料串流至記憶體內緩衝區。

最佳設定

無論您以何種方式擷取系統追蹤記錄,請務必向工具提供合理的引數集:

  • 類別:最適合用來進行遊戲型系統追蹤的類別,包括:{schedfreqidleamwmgfxviewsyncbinder_driverhaldalvik}。
  • 緩衝區大小:一般而言,每個 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. 啟用「Trace debuggable applications」選項。
  2. 在「Buffer size」下方,選取「65536」 (約 64 MB)。請注意,如要採用這項設定,裝置必須有 256 MB 或 512 MB 的可用空間 (視 CPU 有 4 或 8 個核心而定),且每個 64 MB 的記憶體容量都必須可用做連續的區塊。
  3. 選擇「Categories」,然後啟用下列清單中的類別:

    • am:活動管理員
    • binder_driver:Binder Kernel 驅動程式
    • dalvik:Dalvik VM
    • freq:CPU 頻率
    • gfx:圖形
    • hal:硬體模組
    • idle:CPU 閒置
    • sched:CPU 排程
    • sync:同步處理
    • view:查看系統
    • wm:視窗管理員
  4. 啟用「Record tracing」

  5. 載入遊戲。

  6. 在遊戲中針對要評估裝置效能的遊戲體驗,執行相應的互動。

  7. 在遊戲中遇到不良行為後,請立即關閉系統追蹤功能。現在,您已擷取必要的效能統計資料,以便進一步分析問題。

如要節省磁碟空間,裝置端系統追蹤記錄會以壓縮追蹤格式 (*.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 上執行遊戲的典型行為:

系統追蹤中的執行緒圖表

圖 1. 單一執行緒遊戲的 Systrace 報表

如要提升遊戲的效能,請將遊戲置於多執行緒。一般而言,最佳模型是使用 2 個執行緒:

  • 「遊戲執行緒」,包含遊戲的主要模組,並傳送轉譯指令。
  • 「轉譯執行緒」,用於接收轉譯指令,並將其轉譯成裝置 GPU 可用於顯示情境的圖形指令。

Vulkan API 在這個模型上擴充,因為系統能夠同時推送 2 個常見的緩衝區。這項功能可將多個轉譯執行緒分散於多個 CPU,進一步縮短了情境的轉譯時間。

您也可以進行一些引擎專屬的變更,以改善遊戲的多執行緒效能:

  • 如果您是使用 Unity 遊戲引擎開發遊戲,請啟用「Multithreaded Rendering」「GPU Skinning」選項。
  • 如果您使用自訂轉譯引擎,請確認轉譯指令管線和圖形指令管線皆正確對齊;否則可能會延遲顯示遊戲情境。

套用這些變更後,您的遊戲至少會同時占用 2 個 CPU,如圖 2 所示:

系統追蹤中的執行緒圖表

圖 2. 多執行緒遊戲的 Systrace 報表

載入 UI 元素

系統追蹤中的影格堆疊圖表
圖 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 的遊戲。然而,這種高活動量很快就會產生大量電力和熱源。

系統追蹤中的執行緒圖表

圖 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 進行節流措施,進而以非預期方式影響遊戲。如果遊戲使用複雜的圖形、大量計算或持續的網路活動,就更有可能引發問題。

使用 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,這樣會導致玩家的輸入以及輸入在螢幕上生效之間出現明顯延遲。

如要確定您是否能改善遊戲的影格使用速度,請完成下列步驟:

  1. 產生包含 gfxinput 類別的 Systrace 報表。這些類別包含特別實用的測量結果,以確定觸控顯示延遲時間。
  2. 查看 Systrace 報表的 SurfaceView 部分。過度填充的緩衝區會導致待處理緩衝區繪圖的數量在 1 到 2 之間波動,如圖 5 所示:

    系統追蹤中的緩衝區佇列圖表

    圖 5. 顯示一個過度填充的緩衝區並會定期填充過滿而無法接受繪圖指令的 Systrace 報表

為減少影格使用速度的不一致,請完成下列各節所述的動作:

將 Android Frame Pacing API 整合至遊戲中

Android Frame Pacing API 可協助您執行影格交換並定義交換時間間隔,讓您的遊戲維持一致的影格速率。

降低遊戲的非 UI 資產的解析度

現代行動裝置的螢幕含有的像素數量超出玩家可處理的像素數量,因此可以縮減像素採樣,以使 5 甚至 10 像素的執行都包含一種顏色。以大部分螢幕快取的結構而言,建議您僅仰賴一個維度來降低解析度

不過,請避免降低遊戲 UI 元素的解析度。請務必讓這些元素保留線條粗細,為所有玩家維持足夠大的觸控目標尺寸

轉譯流暢度

當 SurfaceFlinger 鎖定在螢幕緩衝區中以顯示遊戲中的情境時,CPU 活動會立即增加。如果 CPU 活動的用量突然暴增,遊戲就可能會出現延遲。圖 6 中的圖表說明了發生這個問題的原因:

由於開始繪圖的時間太遲而錯過 Vsync 視窗的影格圖表

圖 6. 顯示影格如何錯過 Vsync 的 Systrace 報表

如果影格開始繪圖的時間太遲 (即使延遲數毫秒),也可能會錯過下一個顯示視窗。接著,影格必須等待下一個 Vsync 狀態顯示 (以 30 FPS 執行遊戲時為 33 毫秒),這樣玩家才能看見明顯延遲的情況。

如要解決這個問題,請使用 Android Frame Pacing API,而這個 API 一律會在 VSync 波形上顯示新影格。

記憶體狀態

如果執行遊戲的時間較長,可能導致裝置發生記憶體不足錯誤。

在這種情況下,請查看 Systrace 報表中的 CPU 活動,瞭解系統呼叫 kswapd Daemon 的頻率。如果遊戲在執行時有許多呼叫,建議您進一步瞭解遊戲的管理和清理記憶體方式。

詳情請參閱「有效管理遊戲記憶體」。

執行緒狀態

瀏覽 Systrace 報表的一般元素時,您可以在報表中選取執行緒,藉此查看指定執行緒在每個可能的執行緒狀態上花費的時間,如圖 7 所示:

Systrace 報表的圖表

圖 7. 選取執行緒如何導致報表顯示該執行緒的狀態摘要的 Systrace 報表

如圖 7 所示,遊戲中的執行緒未按預期頻率處於「執行中」或「可執行」狀態。下方清單顯示指定執行緒定期轉移至異常狀態的幾個常見原因:

  • 如果執行緒長時間處於休眠狀態,則可能是鎖定動作爭用或等待 GPU 活動所致。
  • 如果執行緒持續在 I/O 上遭到封鎖,可能是因為您從磁碟一次讀取過多資料,或者您的遊戲遭到輾轉。

其他資源

如要進一步瞭解如何改善遊戲效能,請參閱下列其他資源:

影片