
提到兔子换性能优化很多人第一反应是“一个兔子换装的休闲游戏能有什么性能压力”实际项目上线第一周就被在线玩家狠狠打脸。兔子换是我们团队做的一款以兔子为主角的换装社交手游主打大厅闲逛、换衣搭配、聊天互动美术走的是高饱和卡通渲染衣物和配饰细节多到离谱。中低端安卓机一进换装界面帧率直接掉到15帧左右连续试几套衣服还会闪退后台差评从表扬美术变成清一色的“烫手”“卡成PPT”。这篇文章就是当时调优的完整记录重点整理出三个最坑的问题从定位思路到填坑方案一条龙写清楚希望能给正在做移动端性能优化或者准备做同类休闲游戏的开发朋友一份能直接落地参考的开发避坑指南。1. 先搞清楚“兔子换”为什么卡1.1 项目背景与性能问题画像兔子换的核心玩法听起来很轻量兔子形象按头、眼、嘴、帽子、衣服、裤子、鞋子、配饰、尾巴九个部位自由换装玩家在大厅里走动、和其他玩家打招呼再开一个公共聊天频道。玩法不重但美术资源非常重每个部位都有几十个SKU同一个角色身上挂着多个网格和材质大厅里三五个玩家站一起整个渲染管线就开始告急。上线第一周我们收集到的数据相当难看中低端安卓机平均帧率只有14到17帧时间波动巨大一打开换装预览界面就明显掉帧内存方面页面停留时间越长占用越高低端机在换装界面切了二十几套衣服后直接闪退发热也非常明显玩十分钟机身就能当暖手宝。这些现象看起来五花八门但归到底层就三类问题渲染资源不合理、内存资源生命周期失控、逻辑层制造了太多没必要的计算和GC压力。我后来做性能优化有个习惯先不急着改代码而是给问题画个像哪个模块耗GPU、哪个模块耗CPU、哪里在快速消耗内存全部量化出来再动手。这个思路在兔子换这个项目里尤其管用因为问题不是单点而是好几座山压在一起。1.2 性能优化方法论先量化再动手拿到这个项目后我干的第一件事不是改Shader不是压贴图而是拉了一条性能基线。我们固定一台中低端测试机把游戏里最卡的标准流程跑一遍进入大厅、打开换装列表、快速切换10套衣服、再进聊天频道滑动5分钟全程用PerfDog和Unity Profiler记录数据。下面这张表就是当时记录的基线和目标指标优化前基线优化目标实际优化后平均帧率15 fps30 fps29~30 fpsDrawCall峰值380100 以下75每帧GC Alloc1.2 MB100 KB 以下60 KB内存峰值1.1 GB800 MB 以下750 MB设备温度明显发烫温热明显改善有了这张表优化节奏就很清楚了。我当时的判断是渲染问题最显眼先砍DrawCall这关系到最基础的出图速度内存问题紧随其后因为闪退直接赶跑玩家最后是逻辑层的GC和CPU空转属于“平时不显眼卡顿元凶却往往是它”的问题。再说个题外话性能优化这个逻辑在不同领域是真的相通。我早先接触过Julia程序的内存管理优化也做过Linux嵌入式驱动开发调设备树配置、裁剪系统功能核心套路都是“先量化瓶颈再针对瓶颈做减法”。前阵子有个朋友让我写Windows游戏性能优化的bat脚本我一看也无非是关后台服务、调电源模式、清理临时文件这几件事。道理全都一样把系统里拖后腿的部分减到最少。2. 坑一换装组合的渲染爆炸2.1 现象同屏3个玩家DrawCall直冲300先说最直观的渲染问题。兔子换里角色形象由九个部位拼出来美术为了细节丰富每个部位都是独立建模、独立材质。美术同学觉得很正常客户端这边就难受了一个完整角色要渲染九个部件每个部件有自己的一套材质和贴图同屏出现3个玩家等于同时要渲染27个“半自动”物体DrawCall轻轻松松破300。更离谱的是换装预览界面。玩家点一件衣服界面右上角会出现一个兔子试穿模型每次切换部位模型预览模型也要跟着重建网格、切换材质。用Frame Debugger抓一帧看合批失败的提示到处都是原因基本都是“材质不同”“贴图不同”“网格不能合并”。当时的帧率状况我到现在还记得中端机已经不能用卡来形容而是像幻灯片一样画面一顿一顿的。2.2 根因多材质、独立贴图与变体失控为什么合批会失败这里要理解引擎渲染合批的三个硬性条件第一参与合批的物体必须是同一个材质实例材质参数不能有差别第二贴图需要在同一张图集里如果各用各的贴图GPU采样器没法统一绑定第三Shader的关键字集合必须一致哪怕只是少了某个Feature引擎也会认为“这不是同一个Pass”。兔子换的情况是三个条件全不满足。每个部位独立建模已经是常态美术为了让衣服有层次感还给不同部位添加了不同的Shader功能比如金属感、透明描边、流光效果。于是Shader变体数量很快爆炸Build出来的Shader变体集合里有大量只为一个部件服务的无用变体运行时Shader.Parse和加载也消耗额外时间。这里想特别提醒一句骨骼动画模型想靠静态合批基本不可能因为骨骼模型每一帧都要驱动骨骼矩阵。动态合批对顶点数有限制骨骼模型的顶点数通常在几千个以上合批不划算CPU开销反而上涨。所以想靠一个“开关”就解决DrawCall问题方向本身就错了。2.3 填坑方案图集合并、Mesh合并与变体裁剪我们最终方案分三步。第一步是合并Mesh。在DCC工具里让美术把同一个角色的主要部位挂到同一套骨骼下运行时按需开关子Mesh。某些实在没法在DCC里合并的部件我用SkinnedMeshRenderer.CombineMeshes来合并。这里有个我踩过的坑合并后骨骼权重数组必须重新映射否则兔子动作会直接变成面条舞。网上很多合并且贴图又能跑的案例权重处理那步通常一笔带过实际写起来要很小心Bones数组要去重并按新索引重排BoneWeights也要跟着重排。第二步是合并贴图。我们把角色的身体、衣服、配饰尽量打在同一张图集里通过UV区域划分来区分各部位。好处是材质从九套变成一套GPU采样贴图更快内存也有节省毕竟图集可以减少重复的纹理边界和打包开销。但不建议把所有角色部位全塞一张图集因为不同SKU组合太灵活强塞会造成图集体积过大加载反而更慢。我们按“身体基础服装”一套图集、“配饰特殊道具”另一套图集来拆分。第三步是裁剪Shader变体。用Build Report查看变体数量和引用情况把没用的Shader Feature从平台构建列表中剔除功能相近的部件统一用同一组关键字不要每件新衣服都添加新的Shader开关。三步做完再配合大厅里的遮挡剔除和LODDrawCall从380降到75左右帧率从15帧提到27帧。后面再深入优化UI才稳定到30帧。3. 坑二换装UI的资源加载与内存失控3.1 现象切换20套衣服后低端机闪退渲染优化做完后游戏整体流畅度上了一个台阶但内存问题立刻浮出水面。兔子换的换装界面是每天玩家停留最久的地方玩家会不断往下滑动列表、点开每件衣服的试穿预览、再退出到大厅。问题特征是前几分钟一切正常随着试穿次数增加内存水位线一路爬升到低端机上切了二十几套衣服后系统直接杀死应用玩家体验从“有点卡”变成“完全不可用”。打开Memory Profiler后发现两个现象同一个Texture2D在内存里存在好几份同一张UI图集被不同界面重复加载AB包加载后没有任何释放机制页面关了资源还常驻。更让人头大的是UI滚动列表的Item在滑动过程中频繁Instantiate和Destroy这在Unity里会连续触发GC内存碎片化越来越严重最终把可用内存挤爆。3.2 根因资源生命周期一团乱麻这个问题拆开看每一层都不复杂但叠加起来就是灾难。第一AB包没有引用计数也没人负责卸载。打开换装界面加载一次资源关掉页面不去Unload下次再打开又加载一遍加载前也没有检查缓存于是同一个Asset出现多份实例。第二预览大图直接加载原尺寸UI贴图一张预览图可能几MB滑动列表时一次要显示很多张内存自然往上飙。第三UI列表没有对象池Item频繁创建销毁导致大量瞬时GC和内存碎片。我后来和朋友复盘时老开玩笑说这问题和Windows系统用久了变卡是一个道理后台服务越来越多、临时文件越积越多、内存被占满却没人清理。移动端游戏内存管理也一样缺乏一个清晰的资源生命周期机制再多的内存都不够用。3.3 填坑方案对象池、引用计数与压缩纹理第一步是给所有AB包和Asset建立引用计数。我们封装了一个资源管理器加载前先查引用池重复请求只增加引用计数不再重复实例化页面关闭时调用Unload(false)确保没有常驻UI引用后再在合适的时机Unload(true)彻底释放。关键资源如图集、角色基础模型常驻内存非核心资源比如表情包、活动道具按需加载用完全部释放。第二步是把换装列表的Item改成对象池。列表滑动时不再反复Instantiate和Destroy而是从池里取对象、设置数据、展示滑出屏幕后回收到池里。预览图也做了分级加载列表缩略图用低分辨率小图点开某件衣服时才异步加载高清图高清图显示之后如果不被查看立刻降级回缩略图并释放高清图引用。第三步是纹理压缩和参数调优。我们全面检查了UI和角色的纹理导入参数压缩格式统一调整为ASTC 6x6半透明相关贴图用ASTC 8x8部分不带Alpha的贴图切到ETC2 RGB。纹理用途压缩格式Mipmap备注角色身体/衣服ASTC 6x6开展示效果优先角色配饰/小物件ASTC 8x8关体积小、加载快UI图标/缩略图ETCC2 RGB关UI不需要Mipmap换装预览大图ASTC 6x6关只加载当前预览项这三步落地后内存峰值从1.1GB降到750MB左右中低端机闪退率明显降低。还要提醒一句UI Overdraw也别忽略。换装界面有多层半透明面板叠加尤其是预览框和列表同时存在时用Frame Debugger看Overdraw数据会吓一跳。我们通过减少不必要的全屏半透明遮罩、拆分层级把Overdraw降了不少。4. 坑三逻辑层GC与网络轮询的隐形耗电4.1 现象帧率稳定了GC尖峰却还在渲染和内存都优化完之后我一度以为项目已经能上了结果真机测试还是发现问题游戏表面看起来流畅但PerfDog上每过几秒就会出现一次GC尖峰GC发生的那一帧帧时间直接飙到200ms以上。玩家感知就是“偶尔卡一下像抽风”而且聊天频道和换装界面切换时特别明显设备发热也没完全解决。当时我判断事情还没完就开始抓CPU各模块的开销结果发现CPU占用并不高但有一堆任务在小规模高频执行尤其是一堆Update循环和网络轮询把CPU大小核全唤醒了还频繁触发GC。这就是移动端“隐形耗电”的典型场景不是某个功能太耗性能而是太多小任务同时空转把设备一直顶在满负荷状态。4.2 根因字符串、LINQ和扫描式刷新找到现象之后用Unity Profiler采样一下调用栈问题立刻现出原形。聊天频道每条消息进来都做一次字符串拼接消息多了频繁创建临时string对象换装列表的数据排序和筛选在Update里用LINQ实现查询过程产生大量委托和装箱网络层每5秒轮询一次无论服务器数据有没有变化客户端都全量拉取拉回来之后不管界面是否需要立刻刷新UI文本UGUI的Text一旦set_Text就会触发整个文本网格重建这个重建本身就是CPU和GC大户。还有一类容易被忽略的坑是常驻Timer。项目里有几个定时器哪怕是当前没有活动、没有弹窗也一直在跑逻辑每一帧都去检查时间判断要不要做点什么。这和Windows后台一直挂着常驻服务是一样的感觉看起来每个都不卡叠起来就在不停空转白耗电和性能。4.3 填坑方案缓存复用、UI节流与事件驱动逻辑层优化没有太多花样就是把“没必要的分配”和“没必要的刷新”全部去掉。先看代码层的字符串拼接优化前大概是这种写法string message ; for (int i 0; i data.Count; i) { message data[i].Name : data[i].Value ; }每一轮循环都new一个新的string数据一多GC马上爆炸。我统一改成StringBuilder并且给聊天频道和热更文本各自维护一个复用实例StringBuilder sb new StringBuilder(64); for (int i 0; i data.Count; i) { sb.Append(data[i].Name).Append(:).Append(data[i].Value).Append( ); } string message sb.ToString();改完之后字符串拼接不再产生中间临时对象。然后是LINQ能不用就不用能改成for循环就改成for循环尤其是列表筛选和排序这种高频操作改成普通循环后GC Alloc肉眼可见地往下掉。UI刷新这块做了节流。聊天消息进来后并不瞬间更新Text而是合并到0.5秒一次的刷新周期里并且刷新前先判断文本内容有没有变化没变化就跳过。网络层从全量拉取改成增量拉取服务器只在数据变更时推新数据客户端收到后只更新变化的部分。常驻Timer全部改成事件驱动活动面板没打开就不计算活动剩余时间聊天轮询只在大厅、聊天界面打开时才启动切到换装界面就关掉。这套组合拳打完之后每帧GC Alloc从1.2MB降到60KBGC尖峰完全消失低端机发热问题也缓解了很多。之前发热不全是CPU算力消耗还有频繁唤醒和GC带来的额外功耗把这些空转逻辑铲掉温度自然就下来了。5. 实操流程与工具链从定位到验证5.1 性能基线采集先让数据说话很多同学在做性能优化时会犯一个错误就是“感觉哪里卡就改哪里”改了半天不知道有没有生效。我的建议是准备一套标准测试流程和一台固定测试机每次优化前后跑同一套脚本用数据说结论。我们当时常用的工具和用途是这样的Unity Profiler看CPU各模块耗时、GC Alloc分配量、函数调用栈。Memory Profiler查重复资源、泄漏对象、检查各系统内存抽屉。Frame Debugger看DrawCall、Batch情况找合批失败的具体原因。PerfDog真机上的帧率、帧时间、CPU占用、耗电量、网络流量一条龙。Xcode Instruments或Android GPU Inspector做平台级分析查系统层面的异常。抓包工具看网络请求频率、包体大小配合确认轮询和增量拉取效果。标准测试流程一定要固定同一台真机同一个系统版本同一个账号跑同样的场景和操作路径。比如说从启动进大厅开始计时走到换装界面前台停留2分钟快速切10套衣服再切聊天频道刷消息整个流程跑3遍取中位数。如果不固定测试条件优化前后的数据就不具备可比性很容易白忙一场。这个习惯后来也被我带到了其他项目上。比如我之前做算法嵌入式部署时先在一台嵌入式板子上跑一轮分层性能分析再针对最耗时的算子做调优和兔子换的优化路径几乎一模一样。模拟器上看着再快到了真实设备上都可能翻车性能优化必须靠真机数据驱动。5.2 常见问题与排查技巧实录这里记录几个我们优化过程中实际遇到的问题希望能帮大家少走弯路。第一个问题把角色所有部位合并成一张图集后内存却没降多少。排查发现是图集把常驻UI资源和临时换装资源全打到了一起导致关掉换装界面也无法释放临时图集。办法是把图集拆成常驻和场景两类场景图集在离开对应界面时统一卸载。第二个问题SkinnedMeshRenderer合批后兔子动作变形。上面提过权重映射没做对。合并Bones数组时如果有相同的骨骼引用必须先合并去重再把BoneWeights里的骨骼索引同步重排否则权重会指向错误骨骼。这个Bug排查起来特别费劲因为不是一直崩只是部分动画帧出现扭曲。第三个问题纹理压缩成ETC2之后UI图标边缘出现色块。ETC2压缩半透明通道时对渐变边缘不够友好渐进色过渡会出现明显条纹。最后我们对UI图标单独用ASTC重要展示区域的角色纹理也用更高规格视觉和性能才勉强平衡。第四个问题改完Text节流后聊天消息偶发丢失。原因是合并刷新时用了一个错误的判断条件刷新周期内第一条消息进来后后来者直接覆盖但覆盖逻辑没处理好最后一条消息反而没显示。解决方案是缓存刷新期间的最新消息刷新时一次性渲染保证消息不丢。5.3 避坑速查表最后整理一份性能优化速查表直接把常犯的错和正确做法放一起方便日后对照。分类典型坑正确做法常用定位工具渲染角色各部位独立材质/贴图DrawCall爆炸合并Mesh、合并图集、裁剪Shader变体Frame Debugger资源AB包重复加载、从不卸载引用计数、按需加载、及时UnloadMemory ProfilerUI列表频繁Instantiate/Destroy对象池、缩略图优先、异步高清加载Unity Profiler纹理预览大图直接加载、不压缩ASTC/ETC2压缩、关闭无用MipmapTexture Analyzer逻辑字符串拼接、foreach、LINQ高频分配StringBuilder、for循环、减少装箱Unity Profiler刷新网络轮询Text频繁Re又叫重建增量拉取、UI节流、内容不变不刷新PerfDog、抓包工具定时器常驻Timer空转CPU频繁唤醒事件驱动按场景开关轮询和计算Unity Profiler这份表看起来简单实际执行时最花时间的不是“套公式”而是定位阶段。资源、渲染、逻辑三个维度会互相影响同一个卡顿现象可能由多个原因叠加导致所以不要指望改一处就彻底解决。我当时的经验是把每个坑的原因先记录成清单改一处重测一次慢慢把清单逐项划掉最终效果自然就出来了。还有一个个人心得想分享给各位性能优化做到后期收益最大的往往不是某个炫酷技巧而是把“少加载、少计算、少刷新”这三个原则贯彻到项目开发的每一天。刚接手兔子换的时候我以为要上什么黑科技最后发现真正解决问题的是把资源合并做好、把内存生命周期理顺、把逻辑GC掐干净。这些看起来不酷却实实在在地把游戏从PPT手感的边缘拉了回来。优化完那段时间我再看任何项目都会习惯性多问一句这个资源能不能少加载一点这段逻辑能不能少跑一点这帧UI能不能晚点再刷。这句话也一并送给读到这里的你。