3.6.1 BUG反馈(音效内存泄露、2D合批极端情况下可能导致卡死、Spine 组件不销毁的情况下替换动画存在内存泄露、音乐播放问题)

1.音效内存泄露的问题
应该是3.x 好几个版本都有,因为我们项目开始选的3.x,一直有类似的问题
3.6.1的时候做了比较多的内存分析,最后定位问题是:
audioSource.playOneShot(…)时,内部有一个最大同时播放的音效数量。目前小游戏上看值定义为10.如果同时播放的音效数量超过这个,cocos 就会停掉一个。然后播放新的音效。问题就出在这里,cocos在停掉的音效的时候,是非自然停止,所有不会走end 回调,而是走的 stop 回调。但是销毁音效是在 end 回调中,这样一来,这个音效就一直没有销毁,在音效频繁播放的时候,就会明显导致内存增长。

AudioSource.ts:381行

public playOneShot (clip: AudioClip, volumeScale = 1) {
    if (!clip._nativeAsset) {
        console.error('Invalid audio clip');
        return;
    }
    AudioPlayer.loadOneShotAudio(clip._nativeAsset.url, this._volume * volumeScale, {
        audioLoadMode: clip.loadMode,
    }).then((oneShotAudio) => {
        audioManager.discardOnePlayingIfNeeded();
        oneShotAudio.onPlay = () => {
            audioManager.addPlaying(oneShotAudio);
        };
        oneShotAudio.onEnd = () => {
            audioManager.removePlaying(oneShotAudio);
        };
        oneShotAudio.play();
    }).catch((e) => {});
}

这个问题在浏览器测试的时候基本没发现,但是微信小游戏实现就很明显。
最终我们的临时解决方案是在OneShotAudioMinigame的构造函数中添加一段代码解决。

private constructor (nativeAudio: InnerAudioContext, volume: number) {
    this._innerAudioContext = nativeAudio;
    nativeAudio.volume = volume;
    nativeAudio.onPlay(() => {
        this._onPlayCb?.();
    });
    nativeAudio.onEnded(() => {
        this._onEndCb?.();
        nativeAudio.destroy();
        // @ts-expect-error Type 'null' is not assignable to type 'InnerAudioContext'.
        this._innerAudioContext = null;
    });
    //KB start
    nativeAudio.onStop(()=>{
        nativeAudio.destroy();
        // @ts-expect-error Type 'null' is not assignable to type 'InnerAudioContext'.
        this._innerAudioContext = null;
    })
//KB end
}

上面的 KB 块验证后确实修复了泄露问题。本来不想自定义引擎的,但是因为这些类都是未导出的类,没法写 Polyfill 打补丁。。

2.2D合批极端情况下可能导致卡死
这个情况,错误是:offset is out of bounds。是合批的 MeshBuffer的索引数组长度不够了。其实这个我看论坛很多人说,可以改StaticVBAccessor.IB_SCALE增大索引和定点的比例绕开问题。但是呢,如果项目中使用 spine 比较多的话,其实定点和索引的比例是不太确定的,可能还是会出问题。尤其是渲染 spine 的数量不少,且经常切换渲染内容的情况。最后发现了一个地方的逻辑可能没有考虑索引长度,然后导致了这个错误。
看代码:

static-vb-accessors.ts:160

    // Loop buffers
    for (let i = 0; i < this._buffers.length; ++i) {
        buf = this._buffers[i];
        freeList = this._freeLists[i];
        // Loop entries
        for (let e = 0; e < freeList.length; ++e) {
            // Found suitable free entry
            if (freeList[e].length >= byteLength) {
                entry = freeList[e];
                bid = i;
                eid = e;
                break;
            }
        }
        if (entry) break;
    }

问题在

if (freeList[e].length >= byteLength) {

这里只判断了IFreeEntry的顶点数据字段长度,是否还有一种可能,顶点字段长度是够的,但是索引已经不够了?

3.Spine 组件复用,只替换显示的动画数据时,内存一直增加无法释放的问题,直到释放这个 spine 组件。
这个我们做过测试,几十个 spine 资源循环在同一个 spine 组件上替换展示,内存一直增加。
从最初的100m,可以到几个 G 的占用。
所以我们目前项目在 spine 渲染这块,只能每一次都新建一个 spine 组件避免这个问题。

4.复用AudioSource音乐播放时,停掉之前的音乐,然后播放一个新的,大概率会出现2个音乐都在播放
这个问题我们主要是在3.5中发现,解决方案是,每次都用不同的 AudioSouce。。。3.6更新后,没有特意再去试有没有问题(反正我们自己的机制已经改成不复用 AudioSource 播放背景音乐了。)

最后还有一些建议,渲染健壮性这块个人觉得,cocos还可以提升一下。
感受比较明显一点是和asset 释放的配合不是很友好。在手动管理内存的场景,如果资源被释放掉了,会导致 cocos 整个渲染都出问题,因为渲染的时候因为资源取不到。渲染直接中断了,画面就卡住了,这样的结果也不说好坏,但是确实很不利于定位问题,因为没法直观知道那个资源被释放掉了导致错误,只能找到报错的地方,在合适的时机启用调试断点,然后看调用上下文,找到最终对应的节点,才能确认是哪个资源。建议可以考虑直接不渲染错误资源,这样的话,起码其他逻辑还可以继续跑,并且,也更方便知道哪一个资源意外被释放了。

10赞

感谢反馈,已告知相关人员

我也遇到播放音效内存上涨的问题。 现在有解决的方法了吗?

我们当时是临时加入代码解决。
后来就一直是自定义的引擎,后续的版本没有专门去验证引擎自身是否已经修复。你可以对比下那段代码。

好的。 我试试。 谢谢

您好,第二点您这边现在是怎么处理的呢?可以参考下吗

音频泄漏的问题,我这边通过重截 AudioSource.playOneShot 接口来修复,自行维护一个 lru 队列来管理同时播放音频的数量,代码如下:

第二个问题,3.7 是有处理的,你那边还是有问题么?fix 2d module mesh buffer indices out of range by zhakesi · Pull Request #13715 · cocos/cocos-engine · GitHub

1赞

合批的这个问题,我们升级3.7以后,貌似确实没出现过了

告知了没改啊,音效不是还这样吗

音效我加了个防并发处理:

音效我们目前用的修改方案,大家可以参考一下。直接代码修正,不用自定义引擎。
这个还得感谢 @cx0cx2001 的代码给的启发。

import { AudioClip, AudioSource, __private } from "cc";
import { MINIGAME, WECHAT } from "cc/env";
import { remove } from "../../hagice_cc/tsext/Array.ext";

console.log("patch cocos audio bug!")

class PolyfillAudioManager {

    private _playingAudios: __private._pal_audio__OneShotAudio[] = []

    removePlaying(oneShotAudio: __private._pal_audio__OneShotAudio) {
        this._playingAudios[remove](oneShotAudio)
    }
    addPlaying(oneShotAudio: __private._pal_audio__OneShotAudio) {
        this._playingAudios.push(oneShotAudio)
    }
    discardOnePlayingIfNeeded(maxAudioChannel: number) {
        if (maxAudioChannel == 0 || maxAudioChannel == undefined) {
            maxAudioChannel = 10
        }
        if (this._playingAudios.length < maxAudioChannel) {
            return
        }
        let removed = this._playingAudios.splice(0, 1)[0]
        removed.stop()
    }
}

let audioManager = new PolyfillAudioManager()

AudioSource.prototype.playOneShot = function (clip: AudioClip, volumeScale = 1) {
    if (!clip._nativeAsset) {
        console.error("Invalid audio clip");
        return;
    }
    let AudioPlayer: any = Object.getPrototypeOf(clip._nativeAsset!.player).constructor;

    // @ts-ignore

    AudioPlayer.loadOneShotAudio(clip._nativeAsset.url, this._volume * volumeScale, {

        audioLoadMode: clip.loadMode,

    }).then((oneShotAudio: __private._pal_audio__OneShotAudio) => {
        //@ts-ignore
        audioManager.discardOnePlayingIfNeeded(AudioPlayer.maxAudioChannel);
        oneShotAudio.onPlay = () => {
            audioManager.addPlaying(oneShotAudio);
        };
        oneShotAudio.onEnd = () => {
            audioManager.removePlaying(oneShotAudio);
        };
        if (WECHAT && MINIGAME) {
            // @ts-ignore
            if (oneShotAudio._audio && oneShotAudio._audio._innerAudioContext) {
                // @ts-ignore
                oneShotAudio._audio._innerAudioContext.onStop(() => {
                    // @ts-ignore
                    if (oneShotAudio._audio._innerAudioContext) {
                        // @ts-ignore
                        oneShotAudio._audio._innerAudioContext.destroy();
                    }
                    // @ts-expect-error Type 'null' is not assignable to type 'InnerAudioContext'.
                    oneShotAudio._audio._innerAudioContext = null;

                });
            } else {
                // @ts-ignore
                console.warn("oneShotAudio._audio:" + oneShotAudio._audio)
                // @ts-ignore
                console.warn("oneShotAudio._audio?._innerAudioContext:" + oneShotAudio._audio?._innerAudioContext)
            }
        }
        oneShotAudio.play();

    }).catch((e: any) => {
        console.error(e)
    });

};

array[remove](…)方法是我们自己封装的一个扩展方法,代码如下

function removeImpl<T>(this: Array<T>, o: T): boolean {
    let indexOf = this.indexOf(o)
    if (indexOf < 0) {
        return false
    }
    this.splice(indexOf, 1)
    return true
}

只要在启动脚本中引入这个脚本(import "PatcherForAudio.ts")就行了。
目前我们在小游戏中测试暂时没发现问题,有问题的话,欢迎大家指正。

可以参考下#12的代码
https://forum.cocos.org/t/topic/142032/12


该pr修复 小游戏播放音效导致的内存泄漏

请问2.4有处理吗?