探究Safari对于canvas的限制

本博客 hjy-xh,转载请申明出处

探究起因

之前使用canvas做了一个绘制水印的功能,可以简单理解成进入A页面后,从接口获取绘制水印的配置,然后在A页面的某个区域使用canvas加盖一层水印

开发和测试都在chrome浏览器上进行,期间均表现正常,直到交付给客户几个月后接到反馈,其APP内嵌我们开发的H5页面,先后出现了以下问题:

  • Android机型正常进入页面,偶尔水印不加载
  • 在IOS下打开页面后无法正常加载,控制台有报错
  • 在华为鸿蒙系统下APP直接闪退

嚯,问题还挺多,一个比一个严重- -!那接下来看看,是什么问题导致以上现象

下文使用我另外写的一个demo做演示

排查过程

第一个问题相对来说比较简单,这里简单说说

使用Android机型进行测试,确实是偶现不加载的情况,经过反复调试,发现在弱网情况下,没有请求接口,这里调整了请求的顺序就解决了

第二个问题

一开始在chrome和Firefox下均未复现,后来发现仅在iPhone真机Safari下才能复现,报错如下:

难道ctx是null吗,结合部分代码调试后查看确实没有获取到渲染上下文

1
2
3
4
5
const canvas = document.getElementById("canvas");
canvas.width = 10000;
canvas.height = 10000;
const ctx = canvas.getContext("2d");
ctx.font = "20px Microsoft YaHei";

这时想起来Safari对于canvas的使用有一些限制(或者说是优化),印象中该浏览器对于canvas的数量有一定限制,emmm,我的代码中只有一个canvas,应该和这个限制没有关系

然后我开始搜索Safari对于使用canvas有什么限制/webkit内核对于canvas的限制,好的,没找到什么蛛丝马迹。于是开始面向google编程,翻一翻有没有人遇到过相似的问题

在一篇文章中有人提到 Safari对于画布大小有限制,于是我修改宽高,均缩小10倍,也就是渲染的区域缩小了100倍,刷新页面后正常加载了!

1
2
canvas.width = 1000;
canvas.height = 1000;

问题解决了是针不戳啊,那具体的尺寸是多少等解决完所有问题再研究下

第三个问题

APP闪退,刚开始初步判断可能是客户环境webview做了某些处理,和我们H5页面没有什么关系,于是请教APP开发大佬,大佬一顿排查后得出结论:在华为鸿蒙系统下,H5页面渲染太久了,导致闪退

好家伙,那这个情况还是因为受到了画布大小影响?!

于是让测试同学在响应系统上通过APP打开H5页面,确实是没有再发生闪退…

那至此bug全部解决

探究Safari对于canvas的限制

已知:Safari采用webkit内核

解:去github上clone一份源码瞅瞅

1
git clone https://github.com/WebKit/WebKit.git

(下面贴的源码均在Source/WebCore/html/HTMLCanvasElement.cpp文件中,可以直接翻阅,clone下来太久啦)

Safari 在实现canvas时对内存进行了限制,在不同设备上允许使用的内存不同,具体根据设备 RAM 的大小计算,一旦超出限制,使用 getContext(‘2d’) 将会返回 null

C++源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CanvasRenderingContext2D* HTMLCanvasElement::createContext2d(const String& type, CanvasRenderingContext2DSettings&& settings)
{
ASSERT_UNUSED(HTMLCanvasElement::is2dType(type), type);
ASSERT(!m_context);

// Make sure we don't use more pixel memory than the system can support.
size_t requestedPixelMemory = 4 * width() * height();
if (activePixelMemory() + requestedPixelMemory > maxActivePixelMemory()) {
auto message = makeString("Total canvas memory use exceeds the maximum limit (", maxActivePixelMemory() / 1024 / 1024, " MB).");
document().addConsoleMessage(MessageSource::JS, MessageLevel::Warning, message);
return nullptr;
}

m_context = CanvasRenderingContext2D::create(*this, WTFMove(settings), document().inQuirksMode());

#if USE(IOSURFACE_CANVAS_BACKING_STORE)
// Need to make sure a RenderLayer and compositing layer get created for the Canvas.
invalidateStyleAndLayerComposition();
#endif

return static_cast<CanvasRenderingContext2D*>(m_context.get());
}

可以看到 canvas 的内存占用空间为 4 * width * height,这里的4指每个像素的RGBA

允许使用的最大内存空间由 maxActivePixelMemory 这个函数计算,这个函数计算规则如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
size_t HTMLCanvasElement::maxActivePixelMemory()
{
if (maxActivePixelMemoryForTesting)
return *maxActivePixelMemoryForTesting;

static size_t maxPixelMemory;
static std::once_flag onceFlag;
std::call_once(onceFlag, [] {
#if PLATFORM(IOS_FAMILY)
maxPixelMemory = ramSize() / 4;
#else
maxPixelMemory = std::max(ramSize() / 4, 2151 * MB);
#endif
});

return maxPixelMemory;
}

再查找源码对于画布大小的限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static inline size_t maxCanvasArea()
{
if (maxCanvasAreaForTesting)
return *maxCanvasAreaForTesting;

// Firefox limits width/height to 32767 pixels, but slows down dramatically before it
// reaches that limit. We limit by area instead, giving us larger maximum dimensions,
// in exchange for a smaller maximum canvas size. The maximum canvas size is in device pixels.
#if PLATFORM(IOS_FAMILY)
return 4096 * 4096;
#else
return 16384 * 16384;
#endif
}

经过实际测试,在iPhone的Safari中,canvas的最大可用宽高确实是4096

具体可以看参考文章中的Safari难道是下一个IE?兼容性这么“差”

其它

另外在chrome、Firefox中,过大的宽高(100000),也无法正常展示,如下所示:

没有探讨释放canvas占用的内存,大家有兴趣可以尝试一下

参考

MDN

stackoverflow

Safari难道是下一个IE?兼容性这么“差”