背景:
我们的引擎是Egret,使用的是原生的EUI,转微信小游戏;
工程第一版出来后使用PerfDog测试一波数据。结果发现很多问题,本文主要分两部分
- 第一部分主要介绍通过PerfDog发现问题,
- 第二部分主要介绍通过PerfDog的数据定位并解决问题。
PerfDog具体操作可以看文档PerfDog使用说明
第一部分————数据分析
本次的案例多见于游戏第一版时的情况,比较常见,所以拿出来做个分析。
这里强调一点。分析问题需要整体数据联动分析,单独看某单一信息是没是意义的
第一次测试数据
FPS:
内存:
CPU:
结论:
1.我们发现在战斗时FPS波动较大
2.内存呈现持续上升的趋势
3.CPU的APP Usage太小,仅占1%左右
首先针对问题3的说明:
我之前选择测试的是微信app,而小游戏是作为子进程而存在的,所以应该选择PerfDog的子进程进行测试,这样得到的数据会更加的精准;下图的深色进程表示正在运行的顶层进程
针对这种多进程的应用测试:
为了判断是什么导致的FPS波动较大,也为了判断是否存在OOM,现在我们来选择子进程进行第二次测试;
第二次测试数据
测试数据组成:
为了验证我的一些猜想,也为了更细致的定位问题,我们在测试过程中做了一些特殊操作:
1.战斗挂机 【为了判断是否是战斗过程中触发的内存泄露】
2.反复打开关闭UI 【为了判断UI创建与销毁是否存在内存泄露】
3.静止在某一UI页面 【为了与其他场景作区分】
4.息屏挂机 【为了判断是否是由图像资源引起的内存泄露还是代码资源引起的泄露】
FPS数据:
CPU数据:
内存数据:
GPU压力山大
FPS与GPU分析:
内存分析:
这里额外说下,看是否存在OOM不能只看PSS(PerfDog默认的memory是PSS),同样要注意VSS,有的游戏可能会存在PSS一般大小,VSS不断增大的情况,这也是不科学的。
简单分享下常见内存指标关系
这里再稍微介绍下安卓的LMK(Low memory killer),详细信息就不多赘述了。
现在综合两次测试数据得出结论
结论:
1.FPS波动过于剧烈,很不稳定,尤其是在uI创建与关闭时候;
2.存在内存泄露,由于不管什么操作内存都一直涨,大概率是公共组件部分引起的
3.其实还有一些其他小问题,不过优先解决这两个
第二部分————问题定位
内存泄露问题分析
有了PerfDog以上的数据,接下来我们就要开始定位排查问题啦,
项目局部架构:
1.我们的项目的基础架构是所有的基础功能都调用的同一份基础class(祖传代码),例如通信类等等;
2.我们发现内存在一直上升,无论是角色在什么环境下,甚至是在息屏的时候内存也在上升,那么我们其实可以大概率定位是项目内部的基础class内部出了问题;
接下来开始细细排查;
内存泄露排查
首先要先了解一些JS的内存管理机制
我这里使用的谷歌浏览器的Head Profiling,或者你也可以使用白鹭引擎的profiler:
使用很简单:
我们可以每隔一段时间来拍一次快照(由于公司项目原因,我就不展示真实项目了,此处仅作为教学):
我们可以打开谷歌浏览器的内存分析工具后有三个选项,我们可以根据自己的调试方式交替使用;
这里举例使用堆快照分析,
右侧查看详细信息
可见rect对象一直在增高,那么我们可以查看一下导致rect对象未被释放的原因:
是由于Rect对象中存在一个属性rect一直被引用导致内存无法释放,那么我们到代码对应的位置去找,就可以较快的定位原因;最终我们发现是因为在自定义的一个全局事件监听器中实例化了一个对象,但是这个对象的一些属性会持续被这个事件监听器所引用而不会被回收
当然为了更快的定位哪个函数,我们也可以使用
一般结果是这个样子
主要关注第三个的JS堆内存、节点数量、监听器数量。鼠标移到曲线上,可以在左下角显示具体数据。这些数据若有一个在持续上涨,没有下降趋势,都有可能是泄漏。
由于篇幅原因,这里不过多介绍这些工具的使用,网上有很多相关教程;
卡顿优化
我们通过PerfDog的数据发现GPU压力很大,游戏来说,渲染画面久一般是drawcall过多,或者每次draw的时间较长。
而我们的游戏在查看在drawcall后确定是由于游戏运行时drawcall过多,导致每帧的渲染耗时比较长,所以会呈现一种卡顿的现象;
关于查看drawcall等可以通过白鹭自身的FPS面板查看 白鹭debug文档
在优化前首先要了解egret在渲染的一帧里做了什么工作内容
细分的话又可以分成
每一帧的工作内容:
1.执行一次EnterFrame,此时,引擎会执行游戏中的逻辑。并且抛出EnterFrame事件
2.引擎会执行一个clear。将上一帧的画面全部擦除
3.Egret内核会遍历游戏场景中的所有DisplayObject,并重新计算所有显示对象的transform
4.所有的图像全部draw到画布
现在来优化一下:
首先要降低drawcall:
1.把小图全都换成图集
2.实现文字合批,通过自定义字体,使用图片字体的方式代替原生的字体
3.动静分离,将需要变化的和不变的分别放在不同的层级下,比如背景层、图标层和动态变化层
4.动画尽量使用dragon bones帧动画而不是spine 动画
5.使用cacheAsBitmap,把矢量图在运行时以位图形式进行计算
降低帧事件的开销:
1.不要的DisplayObject,直接removeChild 而不是设置他的visible属性为false,否则在第三步还会参与计算
2.不在主循环里创建任何对象,游戏中的人物、怪物、技能特效统统做成对象池
3.不在EnterFrame事件中做过多的操作,非要用可以自定义一些事件
我们可以用以下的函数统计创建的gameobject的数量
它是显示了每一秒钟去拿一个hashCount跟上一个hashCount作对比,这个hashCount是由白鹭引擎内部 API,用于统计引擎对象的创建数量。如果游戏静止放置不动,理论上hashCount diff的结果应该是0,实际上要尽可能控制在120以下,如果超标,只需要在引擎的 HashObject 的构造函数这里添加一个断点,在运行时去检查调用堆栈就排查就可以了。