sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

Unity アプリケーションのメモリー使用量を可視化する

Unity でアプリケーションを作成するとき、アプリケーション自体が消費しているメモリー量は常に意識したほうがよいと思います。

詳しく調べるときは「Unity Profiler」を使って調べると思いますが、常に意識するには大仰なものかもしれません。

そんなわけなので、最低限のメモリー使用量を把握するポイントをまとめた記事になります。具体的には、下記のようなものを作りました。

f:id:shikaku_sh:20200701181918g:plain:w500
画面左上にメモリー使用量の要点をうすく表示する

f:id:shikaku_sh:20200706093429p:plain:h300
Android だとこんな感じ

対象は、Unity Editor (Windows), Android です。その他のプラットフォーム対応は参考を確認ください。

監視するメモリー量のポイント

アプリケーションが消費しているメモリー量を監視するポイントは図のように3段階に分かれています。

f:id:shikaku_sh:20200701182022p:plain
こんなイメージだと思います

  1. アプリケーション全体が使用しているメモリー
  2. Unity が管理(プール)しているメモリー
  3. Unity のアプリケーションが実際に消費しているメモリー

1と2の部分を Unity アプリケーション開発者がどうこうすることは難しいので、基本的には「3」の使用量を抑える(自分で決めた目標値以内に収める)ことで、アプリケーション全体のメモリー使用量を良い具合にする=アプリ動作を快適にする、ということだと思います。

話を難しくしているのは、「1」の取得方法が、端末ごとに異なるためややこしいのだと思います。

1「アプリケーション全体」の消費メモリーの取得方法

Unity Editor (Windows)

DLL を作成してメモリー使用量を取得することになります。詳細は「Windowsアプリのメモリ使用量を取得する」に詳しいです。

私が作成したものは「Sample - UnityNativeUtil」に公開しています。

読み込み部分は、次の C# スクリプトになります。

[DllImport("UnityNativeUtil")]
public extern static long GetWorkingSet();

[DllImport("UnityNativeUtil")]
public extern static long GetWorkingSetAndSwap();

entireAppUsedMemory = GetWorkingSet();

f:id:shikaku_sh:20200702090316p:plain:w500
DLL に正しく関数が追加されたか チェックしておくと間違いないです

下記のフォルダに「*.dll」を追加します。

Assets\Plugins

Android

aar モジュールを作成して、メモリー量を取得することになります。詳細は「標準Profilerだけに頼らないUnityのプロファイリングに挑んでみる」に詳しいです。

プラグインを作成する方法は「Unity向けAndroidネイティブプラグインの作り方」などを参考にしてみてください。

f:id:shikaku_sh:20200702090515p:plain
最後は release で aar を出力したほうがよいと思います

私が作成したコードは以下のとおりです。

package com.sh1.usedmemoryutil;
import com.unity3d.player.UnityPlayer;
import android.os.Debug;
import android.os.Process;
import android.app.ActivityManager;
import android.content.Context;

public class NativeMemoryWatcher
{

    /**
     * 現在使用しているメモリー量を取得します。
     * @return 現在使用しているメモリー量(byte 単位)
     */
    public static long GetUsedMemory()
    {
        final Context context = UnityPlayer.currentActivity.getApplication().getApplicationContext();
        final ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        final Debug.MemoryInfo[] memoryInfo = activityManager.getProcessMemoryInfo(new int[]{ Process.myPid() });
        long usedMemory = 0;

        for (Debug.MemoryInfo info : memoryInfo) {
            usedMemory += info.getTotalPss() * 1024; // KB 単位を byte 単位に変換
        }

        return usedMemory;
    }
}

読み込み部分は、次の C# スクリプトになります。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.UI;

public class MemoryAmountInfo : MonoBehaviour
{
    private AndroidJavaClass _NativeClass = null;
    private const string PACKAGE_NAME = "com.sh1.usedmemoryutil";
    private const string CLASS_NAME = "NativeMemoryWatcher";
    private const string METHOD_NAME = "GetUsedMemory";

    [DllImport("UnityNativeUtil")]
    public extern static long GetWorkingSet();

    [DllImport("UnityNativeUtil")]
    public extern static long GetWorkingSetAndSwap();

    // Start is called before the first frame update
    void Start()
    {
        _NativeClass = new AndroidJavaClass($"{PACKAGE_NAME}.{CLASS_NAME}");
    }

    void OnDestroy()
    {
        _NativeClass?.Dispose();
        _NativeClass = null;
    }

    // Update is called once per frame
    void Update()
    {
        // n秒ごとに更新するなどの処理を追加してください

        string text = "";
        float entireAppUsedMemory = -1;

        try
        {
#if UNITY_EDITOR
            entireAppUsedMemory = ToMB(GetWorkingSet());
#elif UNITY_ANDROID
            entireAppUsedMemory = ToMB(_NativeClass.CallStatic<long>(METHOD_NAME));
#endif
            var usedMB = ToMB(Profiler.GetTotalAllocatedMemoryLong());
            var poolMB = ToMB(Profiler.GetTotalReservedMemoryLong());
            var usedPercent = usedMB / poolMB * 100;

            if (entireAppUsedMemory > 0)
            {
                text = $"entire: {entireAppUsedMemory:0.00} MB" + "\n";
            }

            text += $"pool  : {usedMB:0.00} / {poolMB:0.00} MB ({usedPercent:0.00} %)";
        }
        catch (Exception e)
        {
            Debug.Log(e.Message);
        }

        Debug.Log(text);
    }

    private float ToKB(long memory) => memory / 1024F;
    private float ToMB(long memory) => memory / 1024F / 1024F;
}

下記のフォルダに「*.aar」を追加します。

Assets\Plugins\Android

2、3「Unity が管理する」メモリー使用量の取得

Unity が管理するパラメーターは、Unity の提供するメソッドから取得することができます。

  • プールとして用意してくれているメモリー使用量
    Profiler.GetTotalReservedMemoryLong()
  • 現在使用しているメモリー使用量
    Profiler.GetTotalAllocatedMemoryLong()

取得した値は、byte 単位なので感覚的に掴めないため、MB くらいに変換すると見やすくなります。

private float ToKB(long memory) => memory / 1024F;
private float ToMB(long memory) => memory / 1024F / 1024F;

消費メモリー量をどの程度に抑えるか?

Android 端末のメモリー使用量は、開発者モードにすることで詳細を確認することができます。このあたりは、他のアプリケーションのメモリー使用量を把握して、どの程度がよいのか目安を設けるのがよいと思います。

Simple System Monitor というアプリを入れることでも計測することができると思います。

f:id:shikaku_sh:20200702090911p:plain:w500
ADB で100MB

2012年ごろのアプリだと 100MB に抑えるといった、「Unityとスマートフォンアプリの最適化」の33ページの情報も見つかりました。しかし、現在だと Unity のアプリの全体消費量を 100MB に収めるのはかなり困難だと思います。

モリー消費量という言葉が指すのは、全体なのかプールのか留意したほうがよいと思います。

使用メモリー

スマートフォンに入っていたゲームを3つほど動かした結果を記録しておきます。どれも、1~2戦やっただけなので動作時間は数分です。

f:id:shikaku_sh:20200702090158p:plain:w500
アイギスだと田園の1戦だけ

例1(ダンジョンメーカー

f:id:shikaku_sh:20200702085840p:plain:h300
600MBとでかい

例2(お城プロジェクト:RE)

f:id:shikaku_sh:20200702085926p:plain:h300
350MB と納得感ある

例3(アイギス

f:id:shikaku_sh:20200702090004p:plain:h300
380MB だいぶキャラ増えてるしなー

参考

作って学べる Unity本格入門

作って学べる Unity本格入門

Unityサウンド エキスパート養成講座

Unityサウンド エキスパート養成講座