使用Data URI快速更新图像会导致缓存,内存泄漏

时间:2012-03-28 18:44:46

标签: javascript html5 memory-leaks html5-canvas data-uri

我有一个网页可以快速从服务器流式传输JSON并显示其中的一些内容,大约10次/秒。一部分是base64编码的PNG图像。我发现了几种不同的显示图像的方法,但是所有这些方法都会导致无限的内存使用。它在几分钟内从50mb上升到2gb。适用于Chrome,Safari和Firefox。没试过IE。

我首先通过查看Activity Monitor.app发现了内存使用情况 - Google Chrome渲染器进程不断地占用内存。然后,我查看了Chrome的资源检查器(View> Developer> Developer ToolsResources),我看到它缓存图像。每次我更改img src或创建新的图片()并设置其src时,Chrome都会将其缓存。我只能想象其他浏览器也在做同样的事情。

有没有办法控制这个缓存?我可以把它关掉,或做一些鬼鬼祟祟的事情,所以它永远不会发生吗?

编辑:我希望能够在Safari / Mobile Safari中使用该技术。此外,如果有人有任何想法,我会对其他快速刷新图像的方法持开放态度。

以下是我尝试过的方法。每个都驻留在一个在AJAX完成时调用的函数。

方法1 - 直接在src代码

上设置img属性

快速。显示得很好。像疯了一样泄漏。

$('#placeholder_img').attr('src', 'data:image/png;base64,' + imgString);

方法2 - 将img替换为canvas,然后使用drawImage

显示正常但仍有泄漏。

var canvas = document.getElementById("placeholder_canvas");
var ctx = canvas.getContext("2d");
var img = new Image();
img.onload = function() {
    ctx.drawImage(img, 0, 0); 
}   
img.src = "data:image/png;base64," + imgString;

方法3 - 转换为二进制并替换canvas内容

我在这里做错了 - 图像显示小而且看起来像随机噪音。此方法使用受控量的内存(增长到100mb并停止),但速度很慢,尤其是在Safari中(那里的CPU使用率约为50%,Chrome中为17%)。这个想法来自这个类似的SO问题:Data URI leak in Safari (was: Memory Leak with HTML5 canvas)

var img = atob(imgString);
var binimg = [];
for(var i = 0; i < img.length; i++) {
    binimg.push(img.charCodeAt(i));
}
var bytearray = new Uint8Array(binimg);

// Grab the existing image from canvas
var ctx = document.getElementById("placeholder_canvas").getContext("2d");
var width = ctx.canvas.width, 
    height = ctx.canvas.height;
var imgdata = ctx.getImageData(0, 0, width, height);

// Overwrite it with new data
for(var i = 8, len = imgdata.data.length; i < len; i++) {
    imgdata.data[i-8] = bytearray[i];
}

// Write it back
ctx.putImageData(imgdata, 0, 0);

8 个答案:

答案 0 :(得分:4)

我知道这个问题已发布多年以来,但最近版本的Safari浏览器仍然存在这个问题。所以我有一个适用于所有浏览器的确定解决方案,我认为这可以节省工作或生活!。

在html页面的某处复制以下代码:

// Methods to address the memory leaks problems in Safari
var BASE64_MARKER = ';base64,';
var temporaryImage;
var objectURL = window.URL || window.webkitURL;

function convertDataURIToBlob(dataURI) {
    // Validate input data
    if(!dataURI) return;

    // Convert image (in base64) to binary data
    var base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length;
    var base64 = dataURI.substring(base64Index);
    var raw = window.atob(base64);
    var rawLength = raw.length;
    var array = new Uint8Array(new ArrayBuffer(rawLength));

    for(i = 0; i < rawLength; i++) {
        array[i] = raw.charCodeAt(i);
    }

    // Create and return a new blob object using binary data
    return new Blob([array], {type: "image/jpeg"});
}

然后,当您收到base64格式的新框架/图片base64Image(例如data:image/jpeg;base64, LzlqLzRBQ...)并且您想要更新html <img />对象imageElement时,请使用此代码:

// Destroy old image
if(temporaryImage) objectURL.revokeObjectURL(temporaryImage);

// Create a new image from binary data
var imageDataBlob = convertDataURIToBlob(base64Image);

// Create a new object URL object
temporaryImage = objectURL.createObjectURL(imageDataBlob);

// Set the new image
imageElement.src = temporaryImage;

根据需要重复此最后一个代码,不会出现内存泄漏。此解决方案不需要使用canvas元素,但您可以调整代码以使其工作。

答案 1 :(得分:3)

我认为没有任何关于数据URL的内存使用的保证。如果你能想出一种方法让他们在一个浏览器中表现出来,那么它对其他浏览器或版本几乎没有任何保证。

如果将图像数据放入blob然后创建blob URL,则可以取消分配该数据。

这是一个将数据URI转换为blob URL的示例;您可能需要更改/删除webkit-&amp;浏览器以外的浏览器上的WebKit-前缀以及可能的未来Chrome版本。

var parts = dataURL.match(/data:([^;]*)(;base64)?,([0-9A-Za-z+/]+)/);

//assume base64 encoding
var binStr = atob(parts[3]);

//might be able to replace the following lines with just
// var view = new Uint8Array(binStr);
//haven't tested.

//convert to binary in ArrayBuffer
var buf = new ArrayBuffer(binStr.length);
var view = new Uint8Array(buf);
for(var i = 0; i < view.length; i++)
  view[i] = binStr.charCodeAt(i);
//end of the possibly unnecessary lines

var builder = new WebKitBlobBuilder();
builder.append(buf);

//create blob with mime type, create URL for it
var URL = webkitURL.createObjectURL(builder.getBlob(parts[1]))
return URL;

解除分配就像以下一样简单:

webkitURL.revokeObjectURL(URL);

您可以将blob网址用作img的{​​{1}}。

不幸的是,在v10之前的IE中似乎不支持blob URL。

API参考:

http://www.w3.org/TR/FileAPI/#dfn-createObjectURL

http://www.w3.org/TR/FileAPI/#dfn-revokeObjectURL

兼容性参考:

http://caniuse.com/#search=blob%20url

答案 2 :(得分:3)

尝试设置image.src =&#34;&#34;画完后。

var canvas = document.getElementById("placeholder_canvas");
var ctx = canvas.getContext("2d");
var img = new Image();
img.onload = function() {
    ctx.drawImage(img, 0, 0); 
    //after drawing set src empty
    img.src = "";
}   
img.src = "data:image/png;base64," + imgString;

这可能会有所帮助

答案 3 :(得分:1)

我有一个非常类似的问题。

Setting img.src to dataUrl Leaks Memory

长话短说,我只是简单地处理了Image元素。我使用javascript解码器解码并在画布上显示图像数据。除非用户试图下载图像,否则他们也不会知道其中的差异。另一个缺点是你将被限制在现代浏览器中。好的一面是,这种方法不会像筛子那样泄漏:)

答案 4 :(得分:1)

修补了ellisbben的答案,因为BlobBuilder已经过时,https://developer.mozilla.org/en-US/Add-ons/Code_snippets/StringView提供了从base64到UInt8Array的快速转换:

in html:

<script src='js/stringview.js'></script>

在js:

window.URL =    window.URL ||
                window.webkitURL;
function blobify_dataurl(dataURL){
    var parts = dataURL.match(/data:([^;]*)(;base64)?,([0-9A-Za-z+/]+)/);

    //assume base64 encoding
    var binStr = atob(parts[3]);

    //convert to binary in StringView
    var view = StringView.base64ToBytes(parts[3]);

    var blob = new Blob([view], {type: parts[1]}); // pass a useful mime type here

    //create blob with mime type, create URL for it
    var outURL = URL.createObjectURL(blob);
    return outURL;
}

我仍然没有看到它实际上在Safari移动设备中更新图像,但是chrome可以通过websocket快速接收数据,并且比不得不手动迭代字符串更好地跟上它们。如果你知道你总是拥有相同类型的dataurl,你甚至可以将正则表达式换成子串(可能更快......?)

运行一些快速的内存配置文件,看起来Chrome甚至可以跟上解除分配(如果你还记得这样做......):

URL.revokeObjectURL(outURL);

答案 5 :(得分:1)

我使用了不同的方法来解决这个问题,但没有一种方法可行。当img.src = base64string并且那些内存永远不会被释放时,内存似乎会泄漏。这是我的解决方案。

fs.writeFile('img0.jpg', img_data, function (err) {
    // console.log("save img!" );
});
document.getElementById("my-img").src =  'img0.jpg?'+img_step;
img_step+=1;

请注意,您应该将base64转换为jpeg缓冲区。

我的电子应用程序每隔50ms更新一次img,内存不会泄漏。 忘记磁盘使用情况。 Chrome的内存管理让我感到沮丧。

答案 6 :(得分:0)

除非Safari或Mobile Safari 泄漏数据网址,否则服务器端可能是在所有浏览器上执行此操作的唯一方法。

可能最简单的方法是为您的图片流制作一个网址,GET它会将302或303的响应重定向到一次性网址,该网址会提供所需的图片。您可能必须销毁并重新创建图像标记以强制重新加载URL。

您也将受到浏览器的img缓存行为的支配。而且我对HTTP规范的理解(或缺乏理解)是仁慈的。但是,除非服务器端操作不符合您的要求,否则首先尝试。它增加了服务器的复杂性,但这种方法更自然地使用浏览器。

但是使用浏览器 un - 自然呢?根据浏览器如何实现iframe并处理其相关内容,可能能够在不泄漏内存的情况下使数据网址正常工作。这有点像弗兰肯斯坦的狗屎,正是那种无人应该做的废话。好处:它可以工作。缺点:有许多方法可以尝试,不均衡的,无证件的行为正是我所期待的。

一个想法:嵌入一个包含页面的iframe;此页面及其嵌入使用的页面cross document messaging(请注意兼容性矩阵中的GREEN!); embeddee获取PNG字符串并将其传递给嵌入页面,然后嵌入页面生成适当的img标记。当嵌入器需要显示新消息时,它会销毁嵌入的iframe(希望释放数据URL的内存),然后创建一个新的消息,并将新的PNG字符串传递给它。

如果你想要稍微聪明一些,你实际上可以将embeddee页面中的嵌入式框架的源代码作为数据URL嵌入;然而,这可能会泄漏那个数据网址,我想这对于尝试这样的反应是一种诗意的正义。

“在Safari中运行的东西会更好。”浏览器技术不断向前发展。当他们没有将功能交给你时,你必须变得狡猾。

答案 7 :(得分:0)

var inc = 1;
                        var Bulk = 540;
                        var tot = 540;
                        var audtot = 35.90;
                        var canvas = document.getElementById("myCanvas");
                        //var imggg = document.getElementById("myimg");
                        canvas.width = 550;
                        canvas.height = 400;
                        var context = canvas.getContext("2d");
                        var variation = 0.2;
                        var interval = 65;
                        function JLoop() {
                            if (inc < tot) {

                                if (vid.currentTime < ((audtot * inc) / tot) - variation || (vid.currentTime > ((audtot * inc) / tot) + variation)) {
                                    contflag = 1;
                                    vid.currentTime = ((audtot * inc) / tot);
                                }
                                // Draw the animation
                                try {
                                    context.clearRect(0, 0, canvas.width, canvas.height);
                                    if (arr[inc - 1] != undefined) {
                                      context.drawImage(arr[inc - 1], 0, 0, canvas.width, canvas.height);
  

arr [inc - 1] .src =“”;

                                    //document.getElementById("myimg" + inc).style.display = "block";;
                                    //    document.getElementById("myimg" + (inc-1)).style.display = "none";
                                    //imggg.src = arr[inc - 1].src;
                                    }
                                    $("#audiofile").val(inc);

                                   // clearInterval(ref);
                                } catch (e) {
                                }

                                inc++;
                                // interval = 60;
                                //setTimeout(JLoop, interval);
                            }
                            else {

                            }
                        }
                        var ref = setInterval(JLoop, interval);
                    });

感谢老兄的内存泄漏。