最近这阵子,兄弟们在群里骂得可凶了,说我们的新版本跑起来那叫一个卡。我一开始不信邪,觉得是他们手机不行,结果自己跑了一圈,还真卡。特别是打团战,技能特效一多,画面直接定住一秒,然后才慢吞吞地继续跑。我当时就火了,这不砸招牌吗?立马 钻进日志堆 里开始扒,到底是谁给我搞的鬼。
发现问题:谁在偷偷搞破坏?
我先是看渲染,以为是批次太多,或者光照太复杂。折腾了半天,改了一堆LOD和材质,效果还是老样子。卡顿依旧,每次卡住,就是那么固执的一秒钟。我把性能分析器打开,盯着看了五分钟,终于看明白了。
不是CPU不行,也不是GPU崩了,是 GC在不停地抽筋。每次卡顿发生的时候,那红色的GC柱子都快顶到屏幕外面去了,说明程序在疯狂回收垃圾。这就意味着,有大量的临时内存正在被生成,然后又被立即丢弃。我立马明白了,这玩意儿就是我们项目的GC义父,隔三差五就出来搞你一下,教你做人。我决定,这回必须把它按住,让它彻底消停。
我的实践:如何驯服GC义父
要治这病,就得 抓住那些临时变量。我把代码库里所有看起来可疑的地方全部翻了一遍,尤其是循环里头那些频繁创建的字符串、数组和列表。每发现一个,我就想办法给它优化掉。这个过程,那叫一个痛苦,像是在做外科手术,一点一点抠出来。
我主要做了下面几件事:
- 所有要频繁使用的临时容器(比如列表、字典),全部改用 对象池。用完了不销毁,而是收回池子里,下次直接拿出来用。
- 字符串拼接这种杀手,能少用就少用。很多日志打印和UI更新,我 直接把字符串提前格式化好,而不是在运行时疯狂地拼接。
- 重点排查了游戏里那些粒子特效和音效触发逻辑。很多地方的 回调函数 习惯性地新建了一个委托,一帧下来能生成几十个小垃圾。全部改成静态引用或者缓存起来。
- 改了物理系统的触发判定方式,以前是每次都申请新的碰撞列表,现在改成 复用那个列表,只清空内容不重新创建。
- 对核心战斗逻辑中的一些迭代器操作进行了修改,避免了隐式的装箱操作,这玩意儿最阴险,不仔细看根本发现不了。
我记得有一天为了找一个藏在深处的小数组分配,我盯着内存快照看了四个小时,眼睛都快瞎了。当时差点想掀桌子,但一想到玩家在等着,又忍住了。
最终实现:更新日志里的稳定
这一套流程跑下来,我把GC的触发频率 硬生生压下去了80%。再跑团战场景,虽然帧率不是最高的那一档,但是卡顿现象彻底消失了,画面非常顺滑,再也没有那种恶心的停顿感了。
我把这个优化结果打了个包,作为这回的更新日志发了出去。虽然更新日志上只写了“优化了游戏运行稳定性”,但只有我知道,为了这几个字,我跟GC义父 搏斗了整整一周。我写这些东西,不是为了炫技,就是想记录下来,以后谁再遇到这种内存分配的鬼东西,可以直接抄作业,少走弯路。毕竟我们搞实践的,经验才是最值钱的。