一文搞定手游内存优化

图片

图片

👉目录

1 整体思路

2 具体实践

3 总结

内存优化属于性能优化的一部分,内存优化没有功耗、卡顿和帧率等优化可以被玩家们直观的体验到,但是一旦发生Out Of Memory(OOM)问题,玩家将会直接从游戏中闪退。所以内存优化是一个重要的工作。 随着手游3A化、精品化的发展,越来越精美细致的画面表现,越来越丰富的游戏玩法都对内存带来了更大的压力。所以内存优化是一个常态化的工作。 业内的内存分享大多介绍平台侧、引擎侧、工具侧的内存实践。本文将从内存优化的整体思路到具体的实践细节,总结整理Unity手游客户端在内存优化方面所做的实践。

关注腾讯云开发者,一手技术干货提前解锁👇

01

整体思路

内存优化主要分为两个方面:增量内存优化和存量内存优化。

项目初期,优化重点集中在存量优化上。

此阶段主要优化引擎框架、业务代码框架及美术标准等基础内容,为项目打下坚实基础。有句话叫做“优化越早开始收益越大”。当然在项目初期玩法开发最为重要,需要集中力量赶项目进度,而内存优化往往会被忽略。

从后来人的角度往前看,很多内存难题都是因为项目初期赶进度产生的。当项目上线并发展到特定阶段,对一些基础模块进行调整的风险显著增加。有时就不得不在“玩家体验”和“项目质量”之间做出权衡。

所以“优化越早开始收益越大”是有道理的。

在项目发展阶段,工作重心在增量内存优化上。

在这个阶段需要对测试流程,内存分析工具和业务代码逻辑进行优化。到了这个阶段,项目已经在线上稳定运行,业务代码都有固定的框架来开发,所以进行一些大的、底层的、框架性的优化方法需要谨慎再谨慎。

并且到了这个阶段,项目复杂度已经大大提升,需要优化测试流程来覆盖丰富的场景,需要新的内存分析工具来提高内存优化效率。

内存优化从工作流程上可以分为以下3点:

  1. QA测试产生内存数据。

  2. 分析测试数据定位内存问题。

  3. 开发解决方案降低内存。

QA测试需要关注以下方面:

  • 主流程内存峰值。

  • 连续多次主流程后的内存峰值。

  • 特殊场景内存峰值,比如运营活动,第三方SDK。

  • 数据稳定性。

分析测试数据需要关注以下方面:

  • 工具效率。

  • 数据归档。

开发解决方案需要注意以下方面:

  • 预防大于兜底。

  • 优化核心增长点。

02

具体实践

   2.1 测试产生数据

测试需要遵循这几个要求:可重复,可对比,尽量全面。

Unity手游项目目前有以下类型的测试,每种测试类型针对的场景不同。

   2.1.1 每日冒烟测试

目的:检查每日新增内存。

方法:对游戏主玩法的主流程进行测试。

为了数据的可对比性和稳定性,每日冒烟测试会尽可能多的固定变量:固定阵容,固定皮肤,固定设备,固定账号,固定服务器。

固定以上条件之后,内存增量来自于:主流程逻辑变化,战场玩法变化,装备/符文/地图等通用模块的美术资源变化。

为了进一步提高测试数据的稳定性和全面性,每日冒烟测试还分为了自动化测试和技能流测试。

自动化测试:

战场的10个英雄皆由脚本按照既定规则运行,会根据对局的发展做出不同行为。自动化测试的优点在于可以覆盖战场新增玩法,但是内存数据会存在一定差异。

技能流测试:

战场的10个英雄的所有行为都有提前预制的技能流控制。技能流的优点在于英雄的行为完全受控所以内存数据会更加稳定,但是每个新版本都需要先更新技能流,以覆盖战场新玩法。

主流程冒烟测试:

   2.1.2 功能测试

目的:对新版本的新增功能进行内存测试。

方法主要有:

  • 新皮肤测试。

  • 子系统测试。

  • 运营活动测试。

   2.1.3 专项测试

专项测试是一些特殊测试,主要有:

  • 模拟玩家行为进行测试。

  • 特殊设备测试。

  • 业内竞品测试。

   2.2 分析数据

“分析数据”的最主要工作其实是开发内存分析工具。有效的工具能够让测试提供有效的数据,有效的数据能够提高分析效率,从而加快内存优化工作。

那么什么是有效的数据呢,什么工具能够提供有效数据呢?

   2.2.1 阶梯式数据

有效的数据应该是“总-分-细分”的阶梯式数据:

  • 整体数据 表示当前内存量级,判断OOM风险。

  • 分模块数据 表示大型模块的内存。在大型项目中,各团队负责的模块不同。有了分模块数据可以方便调动各管线力量进行内存优化。

  • 细分数据 提供具体的内存分配堆栈,具体的资源名,能够非常直观的表示哪行代码分配了多少内存。

   2.2.2 数据工具链

有了想要的数据之后,就可以去找相应的工具来获取数据。

下面是Unity手游项目的具体实践:项目的内存数据以iOS为主,联合XCode工具链+Unity工具链+第三方工具链实现内存数据采集分析工作。

工具链的整体工作流如图所示:

1、整体数据

对于整体数据,有3个工具可以获取:

  • PerfDog:公司级自研性能分析工具。

  • XUP:光子内部自研性能分析工具。

  • XCode MemGraph:Apple提供的工具。

这3个工具都可以获取项目占用的全部FootPrint内存,可以用来衡量游戏OOM的风险。这3款工具都提供了丰富的使用指引和开发文档,使用起来没有门槛。

业内还是Unity UPR和UWA Tool,但是这两个工具并不支持项目当前使用的引擎,而且都需要额外付费,所以没有被大规模采用。

2、分模块数据

对于分模块数据,这是内存工具遇到的第一个难点。

一般情况下大家会使用Unity提供的Unity Profiler工具。但是Unity Profiler统计的内存是少于游戏总内存的,而且在项目上是远少于游戏总内存的。

Unity Profiler统计不到的内存是:第三方SDK内存,项目 GameCore内存,Unity引擎框架自身运行使用的内存。那么这3块内存该如何统计呢?

  • 对于项目 GameCore内存,在框架设计的时候内存的分配都是统一接口管理的,所以这块内存在分配/释放函数上加统计就可以了。

  • 对于第三方SDK和Unity引擎框架自身的内存,一般的做法是使用hook的方式。

Android平台使用plt Hook方式进行hook。IEG有自研的工具LoliProfiler,使用起来比较方便。

iOS平台使用FishHook的方式进行统计,然后自己统计堆栈。

对于iOS平台,除了Fish Hook的方式,还有一套使用门槛更低的工具链:MemGraph + vmmap + malloc_history

MemGraph + vmmap + malloc_history工具链实践

1)先通过XCode工程截取内存MemGraph快照。

2)然后通过vmmap工具将快照文件按照VM ZONE进行分类。

这里我们关注以下VM Zone:

  • Malloc_Large/Small/Tiny,这部分 = Heap内存 = 所有通过new() / malloc()/ alloc() / align_alloc()分配的内存。

根据项目游戏特点,Malloc_* = Unity Native + 第三方SDK分配的内存:

  • VMALLOCATE,这部分 = 所有通过mmap()分配的内存。

根据项目游戏特点,VMALLOCATE = GameCore + IL2CPP Managed的内存:

  • IOAcclerator,这部分 = GPU占用的所有内存。

到这里,我们通过vmmap --summary命令就得到了一个初步的分模块内存。而且所有模块内存相加等于游戏整体内存,没有遗漏。

并且vmmap工具给出的数据很清晰表示出内存的Dirty Size 和 Swapped Size,而Dirty Size + Swapped Size = FootPrint内存。减少了clean内存的干扰。

3)还可以再进一步,将GameCore内存和IL2CPP Managed内存从VMALLOCATE中区分出来:

通过malloc_history工具可以将addr内存转为堆栈。具体做法是:

  • 先通过vmmap工具将VMALLOCATE中的所有内存地址打印出来。

  • 然后通过malloc_history工具将内存地址转为分配堆栈。

  • 最后再将堆栈按照GameCore和IL2CPP Managed进行归类,就可以统计出GameCore和IL2CPP Managed内存。

经过以上的数据处理,我们将游戏整体划分为了 Heap + GameCore + IL2CPP Managed + IOAccelerator 4个大模块。

3、细分数据

最后就是对Heap + GameCore + IL2CPP Managed + IOAccelerator 这4个模块进行细分。

1)Heap内存

Heap内存 = Unity引擎框架占用内存 + 第三方SDK内存 + Unity Native内存。Heap内存的分配堆栈可以由2种方式获取:

1是通过Heap命令打印出Heap区域的所有内存地址,然后通过malloc_history输出地址对应的堆栈。

但是这个对于Unity手游项目这种大型项目来说效率太低了,或者说根本无法实现。

2是使用Instrument Allocations工具。

Instrument Allocations工具可以通过堆栈来查看Heap的内存,并且已经将堆栈进行了聚合。

这部分堆栈可以完整的Copy出来,然后进一步按照关键字进行业务分类,或者直接按照堆栈进行对比找到2个版本堆栈相同但是内存占用不同的地方。

但是项目使用C#开发,因为协程+IL2CPP会让堆栈变得复杂。

我们在实践中总结出以下经验:

  • 将统计中的count类别取消掉,

  • 将limit限制为1KB甚至1MB。

这样子做有2个用处:1、这样子过滤之后堆栈的缩进是一致的,可以按照缩进处理堆栈的层次关系。2、可以减少堆栈数量,提高操作效率。

通过Instruments Allocation工具,我们可以得到Heap内存的所有堆栈。

2)GameCore内存

GameCore采用C++语言编写,内存申请和释放完全由业务代码控制。在开发GameCore的时候就在内存申请。

3)IL2CPP Managed内存

这块内存之前很难处理。

我们都知道Unity MemoryProfiler工具可以按照对象类别将IL2CPP Managed内存进行分类,并且可以查到内存对象的依赖关系。

但是Unity Memory Profiler的比较难用。难用主要体现在以下方面:

  • 对于大型项目,单单是打开Unity Memory Profiler的GUI就会非常卡顿。

  • 然后Unity Memory Profiler的对比功能非常鸡肋。

  • 最后就是最致命的地方,它对数据的GUI处理方式导致对于大型数据简直无法使用。

举一个实际例子,问:新版本比老版本多了2W+字符串,请回答这些字符串出自哪里?

首先从20W+的字符串中找出这2W字符串在GUI上就已经无法实现了,就更别说这些字符串来自哪里了。

再举一个实际例子,问:新版本比老版本多了2K个int[]数组,请回答它们的分配来源是什么?

但凡数据量上升到“百”这个单位后,靠人工肉眼来排查问题几乎不可能了。所以得另寻工具。

Unity MemoryClawer通过terminal的方式来处理数据,很方便对数据进行二次处理:

  • 对于上面讲到的2w个字符串问题,我们可以通过MemoryClawer将2个版本的所有字符串都打印出来,然后就可以对比出来增长的字符串是什么。

  • 对于上面提到的2K个int[]数组问题,我们扩展了MemoryClawer能力,根据内存对象的依赖关系将内存对象分类。这样分类之后,对于这些数量巨大的对象,我们直接分类,就可以很清晰的看到它来自哪些模块。

MemoryClawer本身还提供了很多能力,可以帮助定位小内存,内存碎片问题。MemoryClawer提供源码,方便接入和扩展。

4)IOAcclerator的内存

我们使用XCode的截帧工具来具体分析这部分内存。

XCode截帧工具GUI友好,使用简单,还有大量的使用指南。使用成本很低。

有了这套工具链就基本完成了总-分-细分的数据分析。

更进一步的,我们通过在资源加载函数上增加日志,结合这部分日志就可以找到具体的资源名。

这样子堆栈和资源名都被获得,而且是阶梯状数据。所以不论是排查增量问题还是分析存量内存,都可以提供准确的数据。

通过这套工具链,降低了数据分析工作的门槛,便于在特定时期投入更多人力进行内存分析工作。提高了内存分析效率,为内存优化提供了明确指引。

   2.3 具体优化

大部分优化方法都可以在各类分享中找到实现原理和实现方式。下面列出我们验证过效果比较明显的优化方法,以及具体实践中的经验。

   2.3.1 业务优化

  • 资源LOD

如果资源分的够细致,内存问题都不会太大。毕竟内存中占大头的是贴图,shader,音频。低内存设备加载低精度的资源,高内存设备加载高精度的资源这样子做就会比较合理。但是多一份LOD对于美术就多一份工作量,也会撑大包体。所以LOD的划分需要根据项目实际情况来制定。

  • 大数组防止扩容

对于一些很大的数组,初始化的时候就指定一个足够的空间,会比数组自己扩容要省内存。这一招虽然简单但是有效。

  • 避免字符串冗余

资源路径,文案,配置会使得游戏中存在大量的字符串。我们的做法是将字符串转成ID使用。这样可以避免存储重复字符串。

  • 压缩

ASTC使用合适的压缩比,动画使用ACL压缩,表格字符串压缩。尝试将资源压缩使用。

  • 图集

禁用图集的read/write。这个优化要做个工具进行校验。大型项目管线复杂,很容易出现低级错误。用工具来做校验可以避免这类问题。

限制图集的大小。超大的图集往往是为了制作方便,但是也往往导致内存浪费。

03

总结

内存优化的分享就是以上内容。感谢耐心阅读。

这里再总结一下,对于内存优化我们需要以下措施:

  • 可以稳定重复的测试流。

  • “总-分-细分”的阶梯式数据。

  • 防范重于兜底的优化思路。

下面分享一些业内比较优质的内存优化相关的文章,做一个知识汇总,常看常新。

也感谢各位大佬过往的分享,让我可以站在巨人们的肩膀上工作。

由于能力有限,可能在某些内容的理解上存在谬误,也请大家包涵指正。

拓展阅读:

《Profile and optimize your game's memory》https://developer.apple.com/videos/play/wwdc2022/10106/

《浅谈Unity内存管理》https://www.bilibili.com/video/BV1aJ411t7N6/?vd_source=b4ce29f7f4280b6a23c340a93abff9d4

-End-

原创作者|黄志恒

感谢你读到这里,不如关注一下?👇

图片

📢📢来领开发者专属福利!点击下方图片直达👇

图片

图片


你在资源管理方面有哪些内存优化的经验?欢迎评论留言补充。我们将选取1则优质的评论,送出腾讯云定制文件袋套装1个(见下图)。7月8日中午12点开奖。

图片

图片

图片

图片

图片

图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值