【问题标题】:Is there a way to stop Web Audio API decodeAudioData method memory leak?有没有办法阻止 Web Audio API decodeAudioData 方法内存泄漏?
【发布时间】:2019-06-25 03:35:20
【问题描述】:

问题

使用 Web Audio API 创建音频缓冲区时,decodeAudioData 方法会创建一些缓冲区,这些缓冲区驻留在内存中,显然无法通过 JavaScript 访问。它们似乎在浏览器选项卡的整个生命周期中都存在,并且从不收集垃圾。

问题的可能原因

我知道这些缓冲区是从主线程中分离出来的,并设置在另一个线程上进行异步解码。我也知道 API 规范说 decodeAudioData 不应该被允许对同一个输入缓冲区进行两次解码,我认为这就是为什么要保留解码缓冲区和/或编码输入缓冲区的副本的原因。但是,在 Chromecast 等内存有限的设备上,这会导致大量内存累积并导致 Chromecast 崩溃。

再现性

在我的示例代码中,我使用 Ajax 获取 mp3,然后将 arraybuffer 传递给 decodeAudioData 函数。通常在该函数中有一个 onsuccess 回调,它可以将解码的 AudioBuffer 作为参数。但是在我的代码中,我什至没有将其传递进去。因此,在解码后我也不对解码后的缓冲区做任何事情。我的代码中的任何地方都没有引用它。它完全留在本机代码中。但是,每次调用此函数都会增加内存分配并且它永远不会被释放。例如,在 Firefox 中,about:memory 会在选项卡的整个生命周期内显示音频缓冲区。非引用应该足以让垃圾收集器摆脱这些缓冲区。

然后我的主要问题是,是否有对这些解码音频缓冲区的引用,比如在 audiocontext 对象中,或者我可以尝试从内存中删除它们的其他地方?或者有没有其他方法可以使这些存储的和无法访问的缓冲区消失?

我的问题与目前关于 decodeAudioData 的所有其他问题不同,因为我表明即使用户没有存储任何参考,甚至没有使用返回的解码音频缓冲区,也会发生内存泄漏。

复制代码

function loadBuffer() {
    // create an audio context
    var context = new (window.AudioContext || window.webkitAudioContext)();

    // fetch mp3 as an arraybuffer async
    var url = "beep.mp3";
    var request = new XMLHttpRequest();
    request.open("GET", url, true);
    request.responseType = "arraybuffer";

    request.onload = function () {

        context.decodeAudioData(
                request.response,
                function () {// not even passing buffer into this function as a parameter
                    console.log("just got tiny beep file and did nothing with it, and yet there are audio buffers in memory that never seem to be released or gc'd");
                },
                function (error) {
                    console.error('decodeAudioData error', error);
                }
        );
    };

    request.onerror = function () {
        console.log('error loading mp3');
    }
    request.send();
}

预测一些可能的反应。

  1. 我必须使用 Web Audio API,因为我在 Chromecast 上播放四个音频文件的四个部分和声,并且 html 音频元素不支持在 Chromecast 上同时播放多个。
  2. 可能是您可以引用的任何 JS 库 [例如Howler.js、Tone.js、Amplitude.js 等] 是建立在 Web Audio API 之上的,因此它们都会共享这个内存泄漏问题。
  3. 我知道 WAA 的实现取决于每个浏览器。目前我主要关心的是 Chromecast,但我尝试过的每个浏览器都存在这个问题。
  4. 因此,我认为这是与规范相关的问题,规范需要非重复编码规则,因此实施者将缓冲区的副本保留在浏览器级线程上,以便他们可以根据新的 xhr 输入检查它们。如果规范作者碰巧读到了我的问题,是否有一种方法可以让用户选择这种行为,如果他们愿意,可以选择退出它以防止移动和薄内存平台上的内部缓冲存储?
  5. 我无法在任何 JS 对象中找到对这些缓冲区的任何引用。
  6. 我知道我可以使用 audio_context.close() 然后希望对 audio_context 持有的所有资源进行垃圾收集,然后希望我可以用一个新的资源重新实例化 audio_context,但是从经验上讲这还不够及时对于我的申请。 Chromecast 在 GC 取出垃圾之前崩溃。

【问题讨论】:

  • 看起来几年前有一个类似的问题也没有得到解答...stackoverflow.com/q/45896791/215552
  • 是的,我见过很多类似的问题。但是,他们的所有情况都包括使用解码缓冲区。我的问题表明,这些 audibuffers 是在用户没有以任何方式引用它们的情况下存储的。
  • 其实参考我上面的(6)。我在几个小时前调用了 audio_context.close() 并打开了选项卡,当我检查 about:memory 时,音频缓冲区仍然存在。所以它们似乎超出了普通垃圾收集器的范围。即使在我单击 Firefox 的最小化内存使用按钮后,它们仍然存在。只有关闭选项卡,然后运行 ​​Minimize Memory Usage 才能删除它们。
  • 根据我的经验,浏览器不会在内部保留解码后的音频,但它们也不会在您期望的时候进行垃圾收集。你试过用开发工具强制垃圾回收吗?
  • 是的,您可以根据需要多次运行垃圾回收,并且它不会删除由 decodeAudioData 创建的音频缓冲区。 decodeAudioData 的规范说您的输入缓冲区将被中性或与主线程分离,并制作它的副本,然后传递给执行解码的线程。这一切都是在本机浏览器代码中完成的,而不是在 JS 空间中完成的,所以没有对它的引用,JS 垃圾收集也看不到这一点。浏览器自己的 GC 机制会保留这些副本,直到选项卡完全关闭。

标签: javascript web-audio-api


【解决方案1】:

务实的解决方法

我找到了一种方法来解决 Web Audio API 音频缓冲区无限期传递和崩溃 Chromecast 和其他移动平台的问题。 [[我没有在所有浏览器上测试过——你的里程可能会有所不同。 ]]

加载阶段

  1. 在 iFrame 中使用 Web Audio API 加载文档。
  2. 加载您的音频缓冲区并执行任何操作来播放它们。

清理阶段

  1. 在您引用的所有播放节点上调用 sourceNode.stop。
  2. 调用 source.disconnect();在所有源节点上。
  3. 调用 gainNode.disconnect();在所有与这些源节点相关联的增益节点上(以及您可能使用的具有断开方法的任何其他类型的 WAA 节点)
  4. 将所有引用的gainNodes和sourceNodes设置为null;
  5. 清空您引用的所有已解码和 xhr 提取的编码音频缓冲区的缓冲区;
  6. KEY:在WAA页面调用audio_context.close();然后设置 audio_context=null; (这可以使用 contentWindow 从 iFrame 的父级完成)。
  7. 注意:其中一些归零步骤可能不是绝对必要的,但是这种方法对我很有效。

重新加载阶段

  1. 从父页面重新加载 iframe。这将导致所有音频缓冲区在下一轮 GC 中被垃圾回收,包括内存隐藏(非 JS)区域中的音频缓冲区。
  2. 您的 iframe 将不得不重新实例化 Web 音频上下文并加载其缓冲区并创建节点等,就像您第一次加载它时所做的那样。

注意事项:您必须决定何时使用此清除方法(例如,在加载和播放了这么多缓冲区之后)。您可以在没有 iframe 的情况下执行此操作,但您可能需要重新加载页面一次或两次才能触发垃圾收集。对于那些需要在 Chromecast 或其他移动设备等内存薄平台上加载大量 Web Audio API 音频缓冲区的人来说,这是一个实用的解决方法。

来自父母

  function hack_memory_management() {
                var frame_player = document.getElementById("castFrame");
                //sample is the object which holds an audio_context
               frame_player.contentWindow.sample.clearBuffers();
                 setTimeout(function () {
                    frame_player.contentWindow.location.reload();
                }, 1000);
            }

在 WAA IFRAME 内

CrossfadeSample.prototype.clearBuffers = function () {
    console.log("CLEARING ALL BUFFERS -IT'S UP TO GC NOW'");
    // I have four of each thing because I am doing four part harmony

    // these are the decoded audiobuffers used to be passed to the source nodes
    this.soprano = null;
    this.alto = null;
    this.tenor = null;
    this.bass = null;
    if (this.ctl1) {

        //these are the control handles which hold a source node and gain node 
        var offName = 'stop';
        this.ctl1.source[offName](0);
        this.ctl2.source[offName](0);
        this.ctl3.source[offName](0);
        this.ctl4.source[offName](0);

        // MAX GARGABE COLLECTION PARANOIA

        //disconnect all source nodes
        this.ctl1.source.disconnect();
        this.ctl2.source.disconnect();
        this.ctl3.source.disconnect();
        this.ctl4.source.disconnect();

        //disconnect all gain nodes
        this.ctl1.gainNode.disconnect();
        this.ctl2.gainNode.disconnect();
        this.ctl3.gainNode.disconnect();
        this.ctl4.gainNode.disconnect();

        // null out all source and gain nodes
        this.ctl1.source = null;
        this.ctl2.source = null;
        this.ctl3.source = null;
        this.ctl4.source = null;

        this.ctl1.gainNode = null;
        this.ctl2.gainNode = null;
        this.ctl3.gainNode = null;
        this.ctl4.gainNode = null;
    }

    // null out the controls
    this.ctl1 = null;
    this.ctl2 = null;
    this.ctl3 = null;
    this.ctl4 = null;

    // close the audio context
    if (this.audio_context) {
        this.audio_context.close();
    }
    // null the audio context
    this.audio_context = null;

};

更新:

遗憾的是,即使这样也不能可靠地工作,Chromecast 仍然会在一些清晰的新 mp3 负载下崩溃。请参阅本页其他地方的“我目前的解决方案”。

【讨论】:

  • 这是一个合理的解决方法。但我同意这不应该是必需的。只要您删除对源缓冲区和音频缓冲区的引用,浏览器就应该能够在您没有任何帮助的情况下收集这些内容。
【解决方案2】:

当您将每个音频标签路由到 Web 音频图(通过使用 MediaElementAudioSourceNode)时,您是否可以在 Chromecast 上使用多个音频标签?

【讨论】:

  • 不,据我所知,Chromecast 不支持一次播放多个音频元素。我在 Chromecast 中尝试了this,它只播放第一个音频元素。
  • 啊,好的。只是检查:)
【解决方案3】:

我目前的解决方案

我找不到最终令人满意的 Chromecast 解决方案,它使用 Web Audio API 并同时播放四个 mp3 - 用于四声部和声。第二代似乎根本没有足够的资源来保存音频缓冲区并使用 decodeAudioData 同时解码四个 mp3 文件,而不会留下太多垃圾并最终崩溃。我决定使用建立在 Web Audio API 之上的 surikov 的 webaudiofont,并使用 midi 文件。我在桌面浏览器或其他资源更多的设备上从来没有遇到过问题,但我必须让它在 Chromecast 上运行。我现在使用 webaudiofont 完全没有问题。

【讨论】:

    【解决方案4】:

    我也遇到了同样的问题。最终对我有用的是断开并删除所有连接的资源:

        if (this.source) {
          this.source.disconnect()
          delete this.source
        }
    
        if (this.gain) {
          this.gain.disconnect()
          delete this.gain
        }
    
        await this.audioContext.close()
    
        delete this.audioContext
        delete this.audioBuffer
    

    仅仅关闭 audioContext 是不够的。似乎引用将继续存在,从而阻止垃圾收集。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-07-11
      • 2020-09-29
      • 1970-01-01
      • 2013-04-30
      • 2011-02-28
      • 1970-01-01
      • 1970-01-01
      • 2015-04-09
      相关资源
      最近更新 更多