导语
作为一个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.");
}
}
}
详细说明
-
调用捕获函数:
- 在
Start方法中,调用MemoryProfiler.TakeSnapshot函数,指定快照文件的保存路径和两个回调函数:OnSnapshotFinished和OnSnapshotProgress。
- 在
-
开始捕获内存快照:
MemoryProfiler开始捕获当前的内存状态。这一步是异步进行的,Unity会在后台收集内存数据。
-
收集内存数据:
MemoryProfiler会收集各种内存数据,包括托管堆内存、本地堆内存、GC信息和其他资源的内存使用情况。
-
生成快照文件:
- 根据收集到的内存数据,
MemoryProfiler生成一个内存快照文件。这个文件通常是一个二进制文件,包含了所有收集到的内存数据。
- 根据收集到的内存数据,
-
保存快照文件:
- 将生成的内存快照文件保存到指定的文件系统路径中。在示例代码中,文件路径是
Application.persistentDataPath目录下的MemorySnapshot.snap。
- 将生成的内存快照文件保存到指定的文件系统路径中。在示例代码中,文件路径是
-
捕获完成:
MemoryProfiler通知捕获过程完成,并调用用户指定的回调函数OnSnapshotFinished。在这个回调函数中,可以处理捕获完成后的逻辑,例如分析内存使用情况或上传快照文件。
-
通知用户:
- 用户代码接收到捕获完成的通知,可以进一步处理快照文件。在示例代码中,
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. 内存占用
捕获内存快照会占用额外的内存资源。如果当前内存使用已经接近系统或设备的上限,额外的内存占用可能会导致内存不足,从而引起卡顿或崩溃。
如何减少卡顿
-
在非关键时刻捕获快照:
- 尽量在游戏的非关键时刻(如加载场景时或暂停时)进行内存快照捕获,避免在游戏高负载时进行捕获操作。
-
分阶段捕获:
- 如果可能,将内存快照捕获过程分阶段进行,避免一次性收集和写入大量数据。
-
优化内存使用:
- 优化游戏的内存使用,减少不必要的内存分配和释放,降低GC频率。
-
异步操作:
- 尽量使用异步I/O操作,将文件写入操作放在后台线程中进行,减少对主线程的阻塞。
-
减少快照频率:
- 降低内存快照的捕获频率,只在必要时进行捕获,避免频繁的内存快照操作。
示例代码优化
以下是一个优化后的示例代码,展示了如何在非关键时刻捕获内存快照,并使用异步操作减少卡顿:
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,需要解决完成工具可用性。具体方法不讲了
4376

被折叠的 条评论
为什么被折叠?



