动物餐厅项目内存优化思路分享

【本文参与征文活动】


##写在前边
本文分享动物餐厅优化内存的经验,几乎无代码,仅阐述思路
##为什么要优化内存
老项目,可能已经上线一年半载了,随着系统越来越多,资源也越来越多,线上内存越来越吃紧,内存峰值触及红线就会收到内存警告,如果不做处理游戏可能就会被杀死

动物餐厅也面临着这样的窘境,作为爆款微信小游戏,App Store、Google Play 常年热门,更新频率非常高,各种资源日积月累,微信小游戏平台上低端机内存崩溃率曾经高达33%,从此开始了和内存崩溃的斗争

##优化效果

从上图可以看出,因为内存问题造成的闪退,在我们优化版本更新后,低端机的崩溃率有了非常明显的降低

##我们是怎么做的
动物餐厅客户端技术栈现状

编辑器、引擎用的是 Cocos Creator 2.0.10

语言使用 JavaScript

有两个 fire 场景,一个启动场景,一个游戏主场景

启动场景用来检查更新、加载资源、加载数据

游戏主场景就是游戏主场景 :slight_smile:

其他界面使用 prefab 实例化出节点挂载到游戏主场景上

历史原因,资源管理方面比较混乱

  1. 节点池

    这个我相信大家应该都有心得,无非就是把使用频次高的节点储藏起来便于复用,这里就不再赘述

  2. 延迟加载资源

    即不要一股脑的把资源全加载到内存里

    比如上边说到的,界面都是 prefab 实例化出来节点挂载到游戏主场景上

    使用这个界面的时候才会去加载 prefab,引擎会把 prefab

    和其引用的资源下载下来并加载到内存里

    题外话,加载期间最好避免玩家进行其他操作(血淋淋的教训),最简单的就是加个转菊花界面挡住

  3. 释放没用的资源

    这些资源包括图片,音频等等

    我们做过内存占用成分摸底,大部分内存是被图片资源吃掉的,音频也占了不小的一部分,但是一顿平衡以后没有动音频,仅释放 prefab 和其引用的资源

  4. 资源引用计数

    资源引用计数的概念大家能搜到很多优秀的文章,这里只说说动物餐厅的做法

    早期我们仅对部分动态加载的资源进行了引用计数,但是发现这样会错误的释放没有进行引用计数的资源,后来对所有资源进行了引用计数

    实现其实很简单

    正在使用的资源 uuid 容器 resMap 对象

    加载资源的方法 loadRes()

    实例化 prefab 的方法 autoReleaseInstantiate()

    节点销毁时自动释放引用的组件 AutoRelease

    loadRes() 内部调用引擎加载资源的方法 cc.loader.loadRes()

    禁止使用 node.parent = null,node.removeFromParent,node.removeChildren,node.removeAllChildren 等方法移除节点,使用 node.destroy() 替代

    禁止使用 cc.instantiate(),使用 autoReleaseInstantiate() 替代

    autoReleaseInstantiate() 方法内部调用 cc.instantiate(),同时为实例化出来的节点添加 AutoRelease 组件

  5. AutoRelease 组件

    init() 接收一个 prefab 参数保存下来,并获取 prefab 引用的所有资源的 uuid 挨个 resMap[uuid]++

    onDestroy() 被引擎调用时 resMap[uuid]- -

    禁止使用 autoReleaseInstantiate() 拷贝节点,采用实例化 prefab 的方式替代 当收到内存警告的时候,调用引擎 cc.loader.releaseRes()

    释放掉 resMap 中 value 为 0 的资源

    这样平衡了时间和空间,内存警告只在微信和 QQ 上有,APP 上动物餐厅的处理方式是定时清理

// AutoRelease.js
cc.Class({
    extends: cc.Component,

    properties: {
        _prefab: null,
    },

    init(prefab) {
        this._prefab = prefab;
        let deps = cc.loader.getDependsRecursivel(prefab);
        for (let i = 0; i < deps.length; i++) {
            let depUuid = deps[i];
            resMap[depUuid]++;
        }
    },

    onDestroy() {
        let deps = cc.loader.getDependsRecursivel(prefab);
        for (let i = 0; i < deps.length; i++) {
            let depUuid = deps[i];
            resMap[depUuid]--;
        }
    }
})
  1. 释放掉看不见的资源

    这些资源会占用内存但是玩家看不到

    例如另外一块地图上的设施,景物,怪物之类资源

  2. 遇到的一些问题

    问题1,我们知道 .prefab 引用的资源肯定不是同时全部加载完的,我们称他们为子资源

    假设这样一个场景,界面 Home 和界面 Garden 都使用了 A.png,Home已经加载完毕并且显示在了场景上,这时候加载 Garden ,然后 Home 用完了 A 就要释放掉,但是 Garden 还没加载完毕,当 Garden 全部加载成功就会发现 A 被释放掉了,引擎没有处理这种情况 :frowning: ,从而引起一系列奇奇怪怪的问题

    处理方式如下

    增加了一个加载过程中受保护的子资源 uuid 容器 protectMap 对象

    cc.loader.loadRes() 提供了进度回调,加载完一个子资源就计数一个子资源,全部资源成功加载后才释放这些计数

    封装了一个 releaseRes 方法,内部调用 cc.loader.releaseRes,protectMap 内没有且 resMap 中计数为 0 的资源才会被释放掉

    有加载成功就有加载失败,记得处理加载失败的资源计数哦

    题外话
    Uncaught TypeError: Cannot read property '__ONCE_FLAG:load' of null
    simple.js Object.fillBuffers Uncaught TypeError: Cannot read property '0' of null
    这就是这种报错的来源

    问题2,使用过程中可能会动态替换资源,比如 sprite.spriteFrame,这时候需要处理老资源计数–,新资源计数++

    问题3,可能会出现使用 loadRes() 加载完成后没有立即使用 autoReleaseInstantiate() 的情况,这时候资源是没有计数的,有被释放的风险,我们的做法是不允许这种写法

  3. 降低规格

    降低图片的分辨率,是最简单粗暴的优化内存方式

    动物餐厅没有采用这种方式
    ##最后一点点
    优化需要结合实际项目情况,平衡其他方面的损失,毕竟游戏指标不止内存一项,避免出现负优化

26赞

@snow_storm :kissing_heart:

在适当的位置配点图,赢定了

:+1: 感谢大佬分享 ,愿世界和平。

mark…

谢谢大佬 学到了新知识

原来大佬是 动物餐厅 的开发人员,膜拜

感谢大佬分享

从写作来看,从项目一开始都在吧,肯定拿项目奖金都拿了不少吧

:wink: 对大家有帮助就好

是老板带的好:wink:

感谢大佬分享,学到了!

感谢大佬分享!

顶卓航的大佬 感谢分享

感谢大佬分享!

膜拜大狍子,大狍子威武霸气,坐等未来更牛逼的分享!!

:kissing_heart:

这都是实战经验

嗯呢,每一步都是无数次测试修改上线

1赞

咋都不在群里了