我们正在研究visualization web application,它使用d3力在画布上绘制网络。 由于每个节点都包含大量信息,并且为每帧重绘所有内容都需要占用大量CPU,因此我们实现了一种缓存,其中每个节点在其画布上绘制(未链接到DOM),每个缩放级别一次。然后,将这些画布绘制在节点位置的主画布(链接到DOM)上。
我们对速度提高感到满意,即使现在的结果是占用大量内存(尤其是在hidpi显示器(视网膜)上,像素密度可以是2或3)。
但是现在我们在iOS浏览器上遇到了问题,该过程在与界面进行少量交互后便崩溃了。 我记得,这不是旧版本(iOS12之前的版本)的问题,但我没有任何未更新的设备来确认这一点。
我认为这段代码总结了问题:
const { range } = require('d3-array')
// create a 1MB image
const createImage = () => {
const size = 512
const canvas = document.createElement('canvas')
canvas.height = size
canvas.width = size
const ctx = canvas.getContext('2d')
ctx.strokeRect(0, 0, size, size)
return canvas
}
const createImages = i => {
// create i * 1MB images
let ctxs = range(i).map(() => {
return createImage()
})
console.log(`done for ${ctxs.length} MB`)
ctxs = null
}
window.cis = createImages
然后在iPad上和检查器中:
> cis(256)
[Log] done for 256 MB (main-a9168dc888c2e24bbaf3.bundle.js, line 11317)
< undefined
> cis(1)
[Warning] Total canvas memory use exceeds the maximum limit (256 MB). (main-a9168dc888c2e24bbaf3.bundle.js, line 11307)
< TypeError: null is not an object (evaluating 'ctx.strokeRect')
是的,我创建了256 x 1MB的画布,一切顺利,但是我又创建了一个画布,canvas.getContext返回一个空指针。 这样就不可能创建另一个画布。
该限制似乎与设备有关,因为iPad上为256MB,iPhone X上为288MB。
> cis(288)
[Log] done for 288 MB (main-a9168dc888c2e24bbaf3.bundle.js, line 11317)
< undefined
> cis(1)
[Warning] Total canvas memory use exceeds the maximum limit (288 MB). (main-a9168dc888c2e24bbaf3.bundle.js, line 11307)
< TypeError: null is not an object (evaluating 'ctx.strokeRect')
由于它是缓存,因此我应该能够删除某些元素,但我不能删除(因为将ctxs或ctx设置为null会触发GC,但不能解决问题)。
我发现的唯一与此相关的页面是webkit源代码页面:HTMLCanvasElement.cpp。
我怀疑问题可能来自webkit本身,但是我想确定一下,然后再发布到webkit问题跟踪器中。
还有另一种销毁画布上下文的方法吗?
在此先感谢您的想法,指导,...
编辑:
要添加一些信息,我尝试了其他浏览器。 Safari 12在macOS上也存在相同的问题,即使限制更高(如Webkit来源所述,为计算机内存的1/4)。我也尝试了最新的Webkit版本(236590),但没有更多的运气。 但是代码可以在Firefox 62和Chrome 69上运行。
我优化了测试代码,因此可以直接在调试器控制台中执行它。如果有人可以在较旧的Safari(例如11)中测试代码,那将非常有帮助。
let counter = 0
// create a 1MB image
const createImage = () => {
const size = 512
const canvas = document.createElement('canvas')
canvas.height = size
canvas.width = size
const ctx = canvas.getContext('2d')
ctx.strokeRect(0, 0, size, size)
return canvas
}
const createImages = n => {
// create n * 1MB images
const ctxs = []
for( let i=0 ; i<n ; i++ ){
ctxs.push(createImage())
}
console.log(`done for ${ctxs.length} MB`)
}
const process = (frequency,size) => {
setInterval(()=>{
createImages(size)
counter+=size
console.log(`total ${counter}`)
},frequency)
}
process(2000,1000)
答案 0 :(得分:6)
我度过了一个周末,制作了一个可以快速显示问题的简单网页。我已经向Google和Apple提交了错误报告。该页面将显示一个地图。您可以平移和缩放所有所需的内容,而Safari检查器(在iPad上运行网络,使用MacBook Pro查看画布)看不到画布。
然后您可以点击一个按钮并绘制一条折线。当您这样做时,您会看到41幅画布。平移或缩放,您将看到更多。每个画布为1MB,因此在您拥有256个孤立的画布之后,由于iPad上的画布内存已满,错误开始出现。
重新加载页面,点击一个按钮以放置一个多边形,然后发生同样的事情。
同样有趣的是,我添加了按钮来为白天和黑夜设置地图样式。您可以仅在地图上来回移动(或仅带有标记的地图,有一个按钮可以在地图上显示一些标记)。没有孤立的画布。但是画一条线,然后在更改样式时,会看到更多的孤立画布。
在Active Monitor的MacBook上查看Safari的大小,在绘制多边形后平移和缩放地图时,大小会保持不变*
我希望苹果和谷歌能解决这个问题,而不是声称这是另一家公司的问题。 IOS12运行的网页已经稳定多年,并且仍然可以在IOS 9和10 iPad上运行,因此,这一切都发生了变化,我一直进行测试以确保较旧的设备可以显示当前网页。希望这项测试/实验对您有所帮助。
答案 1 :(得分:4)
WebKit的最近更改可能是导致这些问题的原因 https://github.com/WebKit/webkit/commit/5d5b478917c685e50d1032ccf761ca53fc8f1b74#diff-b411cd4839e4bbc17b00570536abfa8f
答案 2 :(得分:3)
有人发布了答案,显示了解决方法。想法是在删除画布之前将高度和宽度设置为0。这不是一个真正合适的解决方案,但它可以在我的缓存系统中使用。
我添加一个小示例,该示例创建画布直到引发异常,然后清空缓存并继续。
感谢现在匿名发布此答案的人。
let counter = 0
// create a 1MB image
const createImage = () => {
const size = 512
const canvas = document.createElement('canvas')
canvas.height = size
canvas.width = size
const ctx = canvas.getContext('2d')
ctx.strokeRect(0, 0, size, size)
return canvas
}
const createImages = nbImage => {
// create i * 1MB images
const canvases = []
for (let i = 0; i < nbImage; i++) {
canvases.push(createImage())
}
console.log(`done for ${canvases.length} MB`)
return canvases
}
const deleteCanvases = canvases => {
canvases.forEach((canvas, i, a) => {
canvas.height = 0
canvas.width = 0
})
}
let canvases = []
const process = (frequency, size) => {
setInterval(() => {
try {
canvases.push(...createImages(size))
counter += size
console.log(`total ${counter}`)
}
catch (e) {
deleteCanvases(canvases)
canvases = []
}
}, frequency)
}
process(2000, 1000)
答案 3 :(得分:1)
我可以确认这个问题。可以更改已有多年使用的现有代码。但是,就我而言,加载页面时画布仅绘制一次。然后,用户可以在不同的画布之间浏览,浏览器会重新加载页面。
到目前为止,我的调试尝试表明Safari 12 显然在两次页面重新加载之间泄漏了内存。通过Web Inspector分析内存消耗情况表明,每次重新加载页面时,内存都在不断增长。另一方面,Chrome和Firefox似乎将内存消耗保持在同一水平。
从用户的角度来看,等待20-30秒并重新加载页面会有所帮助。 Safari会同时清除内存。
编辑:这是一个最小的概念证明,它显示了Safari 12如何在页面加载之间泄漏内存。
01.html
<a href="02.html">02</a>
<canvas id="test" width="10000" height="1000"></canvas>
<script>
var canvas = document.getElementById("test");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "#0000ff";
ctx.fillRect(0,0,10000,1000);
</script>
02.html
<a href="01.html">01</a>
<canvas id="test" width="10000" height="1000"></canvas>
<script>
var canvas = document.getElementById("test");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "#00FF00";
ctx.fillRect(0,0,10000,1000);
</script>
复制步骤:
我向Apple提交了错误报告。将会看到效果如何。
编辑:我将Canvas的尺寸更新为10000x1000,作为更好的概念证明。如果现在将两个文件都上传到服务器上并在iOS设备上运行,则如果您在页面之间快速切换,则在重新加载多个页面后将不会绘制Canvas。如果然后等待30-60秒,则似乎已清除了一些缓存,重新加载后将再次显示Canvas。
答案 4 :(得分:1)
只是想说我们有一个使用Three.js的Web应用程序在iOS 12的iPad Pro(第一代)上崩溃。 升级到iOS 13 Public Beta 7 已修复该问题。该应用程序不再崩溃。
答案 5 :(得分:1)
我已经有很长时间了,但是看来我今天能够解决它。我使用了画布,并在其上多次绘制,没有问题。但是,有时在调整大小后,我遇到了一个异常“总的画布内存使用量超过了最大限制”,并且我的画布似乎已经消失了……
我的解决方案是将画布的大小减小到0,然后删除整个画布。调整大小后,随后初始化一个新画布。
DoResize();
if (typeof canvas === "object" && canvas !== null) {
canvas.width = 0;
canvas.height = 0;
canvas.remove();
delete canvas;
canvas = null;
}
canvas = document.createElement("canvas");
container.appendChild(canvas);
// Just in case, wait for the Browser
window.requestAnimationFrame(() => {
let context = canvas.getContext("2d");
context.moveTo(10, 10);
context.lineTo(30, 30);
context.stroke();
});
不一定需要requestAnimationFrame,但我只想等待设备更新画布。我用iPhone XS Max进行了测试。
答案 6 :(得分:0)
我已经向Apple提交了新的错误报告,但尚未回复。在Google地图中使用折线绘制一条线之后,我添加了执行以下所示代码的功能:(堆栈溢出:为什么这么难包含代码?)
function makeItSo(){
var foo = document.getElementsByTagName(“ canvas”);
console.log(foo);
for(var i = 0; i
foo [i] .height = 32;
}
}
查看控制台输出,仅找到4个canvas元素。但是,在Safari调试器中查看“画布”面板时,显示了33个画布(其数量取决于您打开的网页的大小)。
上面的代码运行之后,画布显示将显示4个较小的画布,这可能是人们期望的。所有其他“孤立”画布仍显示在调试器中。
我怀疑这证实了“内存泄漏”理论-存在但未包含在文档中的画布。如果超出了画布的内存容量,则无法使用画布渲染任何内容。
再次,所有这些工作到IOS12为止。我的较旧的运行IOS 10的iPad仍然可以使用。
答案 7 :(得分:0)
另一个数据点:我发现Safari Web Inspector(12.1-14607.1.40.1.4)保留了打开时创建的每个Canvas对象,即使这些对象可能会被垃圾回收。关闭网络检查器,然后重新打开,大多数旧画布将消失。
这不能解决最初的问题-在不运行Web检查器时超出了画布的内存,但是在不知道这个小提示的情况下,我浪费了很多时间走错了路,以为我没有释放任何我的临时画布。