背景交代
最近 QC 同学在跑游戏的过程中发现玩的时间久了游戏会发生闪退,经过和开发人员讨论后又搜集了一些信息,最后排除了功能性 bug 的原因。

一.判断是否是内存泄露

拿到真机,USB 连接,杀掉多余后台进程,打开 Perfdog,接下来一顿操作猛如虎,Perfdog 具体操作不在赘述,有关 perfdog 怎么使用的教程可以参考
Perfdog 使用教程

拿到内存趋势图
使用手机
在这里插入图片描述
在这里插入图片描述
此图是正常玩游戏,游戏运行了 30 分钟的内存趋势图,通过这张图实际上基本就可以断定存在内存泄露了。
结论:内存持续上升,存在内存泄露。
一个优秀的游戏通常情况内存是有上升有回落,多次运行同一个功能也不会导致内存持续上升,内存趋势会呈现出起伏状态,比如:
在这里插入图片描述
知道了存在内存泄露,下面就要开始分析有可能是哪里导致的内存泄露。

二.分析泄露原因

一般针对 unity 游戏来说,内存瓶颈有两部分:==资源==和==Mono==堆内存
以下是 unity 游戏程序在运行时的内存分配概况

先简单介绍下 Mono,unity 使用 Mono 机制来完成跨平台的操作,就好像 JAVA 使用虚拟机来完成跨平台操作一样,Mono 也是一种跨平台的实现。跨平台其实现原理在于使用了叫 CIL(Common Intermediate Language 通用中间语言,也叫做 MSIL 微软中间语言)的一种代码指令集,CIL 可以在任何支持 CLI(通用语言基础结构)的环境中运行,就像.NET 是微软对这一标准的实现,Mono 则是对 CLI 的又一实现。由于 CIL 能运行在所有支持 CLI 的环境中,例如刚刚提到的.NET 运行时以及 Mono 运行时,也就是说和具体的平台或者 CPU 无关。
一般对于 unity 开发的游戏来说,内存的开销都是围绕下面的三个方面:
1.资源内存的占用。
2.引擎模块自身内存占用。
3.托管堆内存占用。

Mono 通过垃圾回收机制(GarbageCollect,简称 GC)对内存进行管理,可以自动地改变堆的大小来适应你所需要的内存,并且是可以适时地调用垃圾回收(GarbageCollection)操作来释放已经不需要的内存。也就是说 Mono 会自动释放一些内存,但要注意的是 GC 释放的内存只会留给 mono 使用,并不会交还给操作系统,因此 mono 堆内存是只增不减的。
这里简单介绍下 Mono 回收原理:
Mono 会跟踪每次内存分配的动作,并维护一个分配对象表,当 GC 的时候,以全局数据区和当前寄存器中的对象为根节点,按照引用关系进行遍历,对于遍历到的每一个对象,将其标记为活的(alive)。所有对象的被标记意味着该对象可以通过全局对象或者当前上下文访问到,而没有被标记的对象则意味着该对象无法通过任何途径访问到,即该对象 “失联” 了,GC 最终会将所有 “失联” 的对象内存进行回收。
内存泄露定义
我们把对象已经不再需要使用却没有被 GC 回收的情况称为 mono 内存泄漏。Mono 内存泄漏会使空闲内存减少,GC 频繁,mono 堆不断扩充,最终导致游戏内存占用的升高。最终导致内存过高,进程被操作系统 Kill 或者崩溃。简单来说,也就是一些对象被实例化出来后没有被释放掉,一种保存在内存中,新的对象又需要申请新的内存空间,导致内存不断上升。
重点关注点
配置文件的使用、纹理、网格、RenderTexture 和粒子系统;
比如频繁的创建销毁对象是否使用对象池,或者粒子,纹理等资源显示过后是否被及时从内存中释放,等等;

三.测试手段

1.首先通跑测试,确定问题确定原因,比如我上面通过通跑游戏确定存在内存泄露;
2.缩小范围,由于一个游戏在运行的过程中场景比较复杂,上面的同跑并不能准确定位问题,所以我们要划分场景测试,例如我在上面的通跑游戏过程中包括以下场景,打开关闭 UI 界面,战斗场景,切换地图,升级武器等,如果没有比较明显的数据,那就要分别针对以下场景进行测试。比如 UI 场景可以反复打开关闭 UI 界面,战斗场景可以持续战斗挂机,反复切换地图等等,总之是把游戏内进行的行为减少,细化要检测的场景;
在这里插入图片描述
3.定位问题
如果某个场景发生内存泄露,边定位到那个场景运行游戏,而在游戏运行时,相应的引擎也有一些工具可以查看具体的代码使用情况,比如 unity 的 Profiler。
如果多个场景都出现内存泄露,那就要查找这些场景所交叉的部分,比如通信框架等;而本次经过多个场景的测试发现都存在泄露,最后经过排查发现是使用的通信框架存在泄露问题。

四,Perfdog 内存相关简介

通常情况下安卓可以轻松获取到的内存有 4 种数据,我们也可以通过 ADB 来获取。
VSS - Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)
RSS - Resident Set Size 实际使用物理内存(包含共享库占用的内存)
PSS - Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
USS - Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)
一般来说内存占用大小有如下规律:VSS >= RSS >= PSS >= USS

而 Perfog 的Memory也就是 Android PSS Memory,也是我们通常来用作代表内存的数据,是实际使用的物理内存大小。
Swap Memory (Swap Memory,部分设备支持 Swap 功能,在启用 Swap 功能后,系统会对 PSS 内存进行压缩,Swap 增加,PSS 会相应减少,由于压缩会占用 CPU 资源,同时相应会导致 FPS 降低)
Virtual Memory(VSS) 虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换

五、Perfdog 新功能初探

PerfDog 3.5 版本刚刚推出,新增一个最新的数值,CPU Usage(Normalized):规范化 CPU 利用率
官方给出的解释为:

传统计算方法:当前时刻 CPU 频率下,CPU Usage = CPU 执行时间/CPU 总时间。
由于移动设备 CPU 频率时刻变化,用传统 CPU 利用率计算方法,假定在低频率时刻计算出 CPU 利用率=30%,和在 CPU 高频时刻计算出 CPU 利用率=30%。同样都是 30% 但性能消耗是完全不样的,明显高频消耗更高。传统 CPU 利用率已无法真实反映性能消耗。
所以我们需要一种规范化 (可量化) 的统计方式。将频率因素考虑进去。
CPU Usage(Normalized)= (CPU 执行时间/CPU 总时间) * (当前时刻所有 CPU 频率之和/所有 CPU 频率最大值之和)。
PerfDog 两种统计方式都有。CPU Usage 默认为规范化 CPU 利用率。建议使用规范化 CPU 利用率作为衡量性能指标。

具体的描述可以看这里:规范化 CPU 利用率
这里尝鲜体验下,测试使用过程和之前的一样,来看看新增的数据对比:
title:
在这里插入图片描述
CPU Usage 趋势图对比:
在这里插入图片描述
在这里插入图片描述
CPU Core Usage 趋势图对比:
在这里插入图片描述
在这里插入图片描述
从趋势图来看的话,实际上两种算法并无太大差异,但是精确到具体帧的使用率,差异会比较明显,单纯从性能的角度来说,传统 CPU 利用率仅能从数值的角度体现手机的 CPU 使用程度,但是无法从性能使用程度的角度表达手机的 CPU 使用效率,就像前文所说,低频率时刻计算出 CPU 利用率=30%,和在 CPU 高频时刻计算出 CPU 利用率=30%。同样都是 30% 但性能消耗是完全不样的规范化 CPU 利用率数值可以弥补这一缺点。目前的测试行业良莠不齐,规范指标较少,如果真的可以做到统一行业标准不失为一件好事。


↙↙↙阅读原文可查看相关链接,并与作者交流