如何改造MemoryProfiler为你所用

导语

作为一个Unity项目开发者,对MemoryProfiler可以说是又爱又恨,一方面它为内存优化提供了很大的帮助,另一方面也被折磨,因为过于耗时以及卡顿,甚至导致无法正常运行我们的项目。因此基于当前面临的困境,我决定改造下现有的工具。

unity引擎中MemoryProfiler抓取内存快照流程

在Unity引擎中,使用MemoryProfiler抓取内存快照的流程可以分为以下几个主要步骤。这个流程涉及到从调用API到最终生成内存快照文件的整个过程。以下是详细的流程说明:

1. 调用内存快照捕获函数

用户代码调用MemoryProfiler.TakeSnapshot函数来开始内存快照捕获过程。这个函数通常需要指定快照文件的保存路径和回调函数。

using UnityEngine;
using UnityEngine.Profiling.Memory.Experimental;
using System.IO;

public class MemorySnapshotExample : MonoBehaviour
{
    void Start()
    {
        // 捕获内存快照并保存到文件
        string path = Path.Combine(Application.persistentDataPath, "MemorySnapshot.snap");
        MemoryProfiler.TakeSnapshot(path, OnSnapshotFinished, OnSnapshotProgress);
    }

    void OnSnapshotFinished(string path, bool success)
    {
        if (success)
        {
            Debug.Log("Memory snapshot saved to: " + path);
        }
        else
        {
            Debug.LogError("Failed to capture memory snapshot.");
        }
    }

    void OnSnapshotProgress(string path, float progress, bool finished)
    {
        Debug.Log($"Snapshot progress: {progress * 100}%");
    }
}

2. 开始捕获内存快照

MemoryProfiler开始捕获当前的内存状态。这一步涉及到收集当前内存中的所有数据,包括堆内存、栈内存、GC信息等。

3. 收集内存数据

MemoryProfiler会收集以下几类内存数据:

  • 托管堆内存:包括所有托管对象的内存使用情况。
  • 本地堆内存:包括所有本地对象的内存使用情况。
  • GC信息:包括垃圾回收器的状态和统计信息。
  • 其他内存数据:如纹理、音频、网格等资源的内存使用情况。

4. 生成快照文件

根据收集到的内存数据,MemoryProfiler生成内存快照文件。这个文件通常是一个二进制文件,包含了所有收集到的内存数据。

5. 保存快照文件

将生成的内存快照文件保存到指定的文件系统路径中。

6. 捕获完成

MemoryProfiler通知捕获过程完成,并调用用户指定的回调函数。回调函数可以处理捕获完成后的逻辑,例如分析内存使用情况或上传快照文件。

7. 通知用户

用户代码接收到捕获完成的通知,可以进一步处理快照文件。例如,可以使用Unity的Memory Profiler工具加载和分析生成的内存快照文件。

流程图

以下是上述流程的图示:

+-------------------+       +-------------------+       +-------------------+       +-------------------+
|     User Code     |       |  Memory Profiler  |       |  File System      |       |  Unity Editor     |
+-------------------+       +-------------------+       +-------------------+       +-------------------+
        |                           |                           |                           |
        | 1. 调用捕获函数           |                           |                           |
        |-------------------------->|                           |                           |
        |                           | 2. 开始捕获内存快照       |                           |
        |                           |-------------------------->|                           |
        |                           |                           | 3. 收集内存数据           |
        |                           |                           |-------------------------->|
        |                           |                           | 4. 生成快照文件           |
        |                           |                           |<--------------------------|
        |                           | 5. 保存快照文件           |                           |
        |                           |-------------------------->|                           |
        |                           |                           |                           |
        |                           | 6. 捕获完成               |                           |
        |<--------------------------|                           |                           |
        |                           |                           |                           |
        | 7. 通知用户               |                           |                           |
        |-------------------------->|                           |                           |
        |                           |                           |                           |

示例代码

using UnityEngine;
using UnityEngine.Profiling.Memory.Experimental;
using System.IO;

public class MemorySnapshotExample : MonoBehaviour
{
    void Start()
    {
        // 捕获内存快照并保存到文件
        string path = Path.Combine(Application.persistentDataPath, "MemorySnapshot.snap");
        MemoryProfiler.TakeSnapshot(path, OnSnapshotFinished, OnSnapshotProgress);
    }

    void OnSnapshotFinished(string path, bool success)
    {
        if (success)
        {
            Debug.Log("Memory snapshot saved to: " + path);
            // 在这里可以进一步处理快照文件,例如上传到服务器或进行本地分析
        }
        else
        {
            Debug.LogError("Failed to capture memory snapshot.");
        }
    }

    void OnSnapshotProgress(string path, float progress, bool finished)
    {
        Debug.Log($"Snapshot progress: {progress * 100}%");
        if (finished)
        {
            Debug.Log("Snapshot capture finished.");
        }
    }
}

详细说明

  1. 调用捕获函数

    • Start方法中,调用MemoryProfiler.TakeSnapshot函数,指定快照文件的保存路径和两个回调函数:OnSnapshotFinishedOnSnapshotProgress
  2. 开始捕获内存快照

    • MemoryProfiler开始捕获当前的内存状态。这一步是异步进行的,Unity会在后台收集内存数据。
  3. 收集内存数据

    • MemoryProfiler会收集各种内存数据,包括托管堆内存、本地堆内存、GC信息和其他资源的内存使用情况。
  4. 生成快照文件

    • 根据收集到的内存数据,MemoryProfiler生成一个内存快照文件。这个文件通常是一个二进制文件,包含了所有收集到的内存数据。
  5. 保存快照文件

    • 将生成的内存快照文件保存到指定的文件系统路径中。在示例代码中,文件路径是Application.persistentDataPath目录下的MemorySnapshot.snap
  6. 捕获完成

    • MemoryProfiler通知捕获过程完成,并调用用户指定的回调函数OnSnapshotFinished。在这个回调函数中,可以处理捕获完成后的逻辑,例如分析内存使用情况或上传快照文件。
  7. 通知用户

    • 用户代码接收到捕获完成的通知,可以进一步处理快照文件。在示例代码中,OnSnapshotFinished回调函数会输出快照文件保存路径,并可以在这里添加进一步的处理逻辑。

回调函数说明

  • OnSnapshotFinished

    • 这个回调函数在内存快照捕获完成后被调用。它有两个参数:path表示快照文件的保存路径,success表示捕获是否成功。
    • 在这个函数中,可以根据success参数的值来判断捕获是否成功,并进行相应的处理。
  • OnSnapshotProgress

    • 这个回调函数在内存快照捕获过程中被多次调用,用于报告捕获进度。它有三个参数:path表示快照文件的保存路径,progress表示当前的捕获进度(0到1之间的浮点数),finished表示捕获是否完成。
    • 在这个函数中,可以输出当前的捕获进度,并在捕获完成时输出相应的日志信息。

结论

通过上述流程和示例代码,我们可以清晰地了解如何使用Unity的MemoryProfiler API来捕获内存快照。这个过程包括调用捕获函数、收集内存数据、生成和保存快照文件,以及处理捕获完成后的逻辑。了解这些步骤有助于我们更好地进行内存分析和优化。

unity引擎中MemoryProfiler抓取内存快照是什么地方导致卡顿

在Unity引擎中使用MemoryProfiler抓取内存快照时,可能会导致卡顿的原因主要有以下几个方面:

1. 内存数据收集

收集内存数据是一个复杂且耗时的过程。MemoryProfiler需要遍历整个内存空间,收集托管堆、本地堆、GC信息以及其他资源(如纹理、音频、网格等)的内存使用情况。这一步骤会占用大量的CPU资源,导致游戏帧率下降,出现卡顿现象。

2. 内存快照生成

生成内存快照文件需要将收集到的内存数据进行序列化,并写入到一个二进制文件中。这个过程同样需要大量的计算和I/O操作,可能会导致游戏主线程被阻塞,从而引起卡顿。

3. 文件写入

将内存快照数据写入文件系统是一个I/O密集型操作,尤其是在快照文件较大时。文件写入操作可能会占用磁盘I/O带宽,导致其他I/O操作(如资源加载)变慢,从而引起卡顿。

4. GC(垃圾回收)

在捕获内存快照的过程中,可能会触发垃圾回收(GC)操作。GC操作会暂停所有托管线程,进行内存清理和压缩,这会导致明显的卡顿。

5. 内存占用

捕获内存快照会占用额外的内存资源。如果当前内存使用已经接近系统或设备的上限,额外的内存占用可能会导致内存不足,从而引起卡顿或崩溃。

如何减少卡顿

  1. 在非关键时刻捕获快照

    • 尽量在游戏的非关键时刻(如加载场景时或暂停时)进行内存快照捕获,避免在游戏高负载时进行捕获操作。
  2. 分阶段捕获

    • 如果可能,将内存快照捕获过程分阶段进行,避免一次性收集和写入大量数据。
  3. 优化内存使用

    • 优化游戏的内存使用,减少不必要的内存分配和释放,降低GC频率。
  4. 异步操作

    • 尽量使用异步I/O操作,将文件写入操作放在后台线程中进行,减少对主线程的阻塞。
  5. 减少快照频率

    • 降低内存快照的捕获频率,只在必要时进行捕获,避免频繁的内存快照操作。

示例代码优化

以下是一个优化后的示例代码,展示了如何在非关键时刻捕获内存快照,并使用异步操作减少卡顿:

using UnityEngine;
using UnityEngine.Profiling.Memory.Experimental;
using System.IO;
using System.Threading.Tasks;

public class MemorySnapshotExample : MonoBehaviour
{
    void Update()
    {
        // 在非关键时刻(如按下特定键)捕获内存快照
        if (Input.GetKeyDown(KeyCode.S))
        {
            CaptureMemorySnapshot();
        }
    }

    async void CaptureMemorySnapshot()
    {
        string path = Path.Combine(Application.persistentDataPath, "MemorySnapshot.snap");
        await Task.Run(() => MemoryProfiler.TakeSnapshot(path, OnSnapshotFinished, OnSnapshotProgress));
    }

    void OnSnapshotFinished(string path, bool success)
    {
        if (success)
        {
            Debug.Log("Memory snapshot saved to: " + path);
            // 在这里可以进一步处理快照文件,例如上传到服务器或进行本地分析
        }
        else
        {
            Debug.LogError("Failed to capture memory snapshot.");
        }
    }

    void OnSnapshotProgress(string path, float progress, bool finished)
    {
        Debug.Log($"Snapshot progress: {progress * 100}%");
        if (finished)
        {
            Debug.Log("Snapshot capture finished.");
        }
    }
}

结论

在Unity引擎中使用MemoryProfiler抓取内存快照时,可能会导致卡顿的原因主要包括内存数据收集、快照生成、文件写入、GC操作和内存占用等。通过在非关键时刻捕获快照、分阶段捕获、优化内存使用、使用异步操作和减少快照频率等方法,可以有效减少卡顿现象,提高游戏的性能和用户体验。

MemoryProfiler的问题就是所有步骤都有问题:

抓取快照耗费大量内存,很容易抓不到内存,直接OutOfMemory崩溃
对象数量上去之后,抓取速度极其缓慢,体验很差
写快照数据和发送到Editor时会分配大内存,容易崩溃
MemoryProfiler解析快照数据,又是漫长的等待
快照内容保存本地,序列化过程无敌缓慢,从未成功过

TakeSnapshot的主函数如下图:

void MemorySnapshots::CaptureMemorySnapshot(dynamic_array<uint8_t>& buffer)
{
    MemorySnapshot* snapshot;
//首先Take C#脚本层托管内存,    121M内存分配
#if ENABLE_SCRIPTING_MEMORY_SNAPSHOTS
    snapshot = scripting_capture_memory_snapshot();
#else
    ALLOC_TEMP_AUTO(snapshot, 1);
    memset(snapshot, 0, sizeof(MemorySnapshot));
#endif

    NativeSnapshot nativeSnapshot;
    snapshot->nativeSnapshot = &nativeSnapshot;
//Take Native对象,并扫描引用关系,    380M内存分配
    CaptureNativeObjects(*snapshot);
//将Take到的Snapshot数据写入buffer,    512M内存分配
    SerializeSnapshot(*snapshot, buffer);
//释放snapshot临时内存,除了512M buffer,其它内存被释放
    FreeCapturedNativeObjects(nativeSnapshot);
#if ENABLE_SCRIPTING_MEMORY_SNAPSHOTS
    scripting_free_captured_memory_snapshot(snapshot);
#endif
}

因此针对内存不足的问题可以优化的策略

ileStream类来代替内存buffer来记录snapshot数据,只保留1M的buffer,超过就写到本地文件,然后优化snapshot抓取过程,不再产生中间数据,而是直接写FileStream

解决完Crash,然后开始优化性能

性能问题其实就是找到最耗时的方法修复他,

我采用了iphone8p在游戏开局时的数据,一次Take的耗时分布:
CaptureManageSnapshot 212.847 ms
CaptureNativeSnapshot 315.66 sec
SerializeSnapshot 1985.6564 ms
FreeMemory 98.48654 ms
SnapshotFileSize 577M
其中CaptureNativeSnapshot的耗时全部在对NativeObject的引用记录上,
NativeObject数量89501,耗时504.964秒

引用关系肯定不合理,也许算bug,需要解决完成工具可用性。具体方法不讲了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你一身傲骨怎能输

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值