iOS Canvas 画布上限保护:解决白屏、导出空白、页面崩溃

12次阅读
没有评论

适用场景:H5、Safari、WKWebView、小程序 WebKit、海报长图导出、网页截图、图片合成

关键词:Canvas 静默失效、WebKit 硬限制、Jetsam OOM、分片瓦片绘制、内存泄漏

一、现象与根因

很多前端在做海报导出、长截图、大图合成时,会遇到诡异 Bug:

  • 代码无报错,toDataURL / toBlob 返回空白图片
  • 绘制完成画布整体变白,控制台没有异常抛出
  • 导出大图后页面直接刷新、白屏,用户体验极差

根本原因:iOS WebKit 存在 Canvas 硬上限

经典限制:单张画布最大像素 4096 × 4096 = 16,777,216 px,RGBA 每像素占 4 字节,满画布占用约 64MB 显存 / 内存。

新高端机型理论可放宽至 8192×8192,但内存紧张时依然会静默失败,没有任何 JS 异常提示。

重要提醒:

  1. canvas.width / height(物理像素)≠ CSS 样式宽高,超限判断必须使用物理像素;
  2. OffscreenCanvas、WebGL 纹理同样受该限制,无法绕过 WebKit 限制
  3. 多画布并发创建会触发 WebContent 进程 OOM,系统通过 Jetsam 机制直接杀掉进程,页面刷新白屏。

二、完整防护策略

整套防护分为 5 个核心环节:前置尺寸校验、DPR 动态适配、分片瓦片绘制、手动内存释放、超限降级兜底。

1. 前置安全校验(基础拦截)

不建议直接写死 4096,生产环境推荐保守阈值 3840px,预留安全余量,避免部分内存较小的老旧机型翻车。

js

运行

// 安全阈值
const SAFE_MAX = 3840;
const SAFE_AREA = SAFE_MAX * SAFE_MAX;

/**
 * 校验画布尺寸是否安全
 * @param {number} width 画布物理宽
 * @param {number} height 画布物理高
 * @returns boolean
 */
function isCanvasSafe(width, height) {
  const area = width * height;
  return width <= SAFE_MAX && height <= SAFE_MAX && area <= SAFE_AREA;
}

进阶方案:运行时探测设备真实上限,不要依赖 UA 判断机型,不同内存版本上限差异很大。

生产推荐开源库:canvas-size,专门探测当前浏览器支持的最大画布尺寸,支持 OffscreenCanvasGitHub。

2. 动态适配 DPR,避免 Retina 屏意外超限

高 DPR 手机(devicePixelRatio=2/3),视觉尺寸不大,但物理画布会被成倍放大,极易触发上限。

解决方案:保持 CSS 视觉尺寸不变,超限后动态降低 DPR,缩小物理画布分辨率。

js

运行

/**
 * 根据CSS尺寸,返回安全的物理画布尺寸
 * @param {number} cssW 样式宽度
 * @param {number} cssH 样式高度
 * @returns { physW, physH } 画布物理宽高
 */
function getSafeCanvasSize(cssW, cssH) {
  const dpr = window.devicePixelRatio || 1;
  let physW = cssW * dpr;
  let physH = cssH * dpr;

  if (!isCanvasSafe(physW, physH)) {
    // 计算缩放比例,预留5%余量
    const scale = Math.sqrt(SAFE_AREA / (physW * physH)) * 0.95;
    const newDpr = Math.max(1, dpr * scale);
    physW = Math.round(cssW * newDpr);
    physH = Math.round(cssH * newDpr);
  }
  return { physW, physH };
}

3. 超大长图:分片瓦片绘制(核心方案)

当整体图片物理尺寸远超安全上限(超长海报、长页面截图),禁止创建一张超大画布

方案思路:

  1. 将整张图纵向 / 横向切分为瓦片,单块瓦片尺寸控制在安全范围内;
  2. 循环逐块绘制,每块绘制完成导出 Blob /base64;
  3. 绘制完成立即销毁临时画布,释放内存;
  4. 所有瓦片导出完成后,使用一张安全画布拼接完整大图,或交由后端服务合并图片。

开发规范:禁止全局长期保存大量临时 canvas、ImageData、base64 字符串,极易造成内存堆积。

4. iOS 必做:手动释放画布内存

iOS JavaScript GC 回收时机不可控,画布像素缓冲区不会自动及时释放,大量临时画布叠加会快速 OOM。

绘制完成后强制销毁画布

js

运行

/**
 * 销毁画布,释放像素缓冲区内存
 * @param {HTMLCanvasElement | OffscreenCanvas} canvas
 */
function destroyCanvas(canvas) {
  if (!canvas) return;
  const ctx = canvas.getContext("2d");
  ctx?.clearRect(0, 0, canvas.width, canvas.height);
  canvas.width = 0;
  canvas.height = 0;
  canvas.remove?.();
}

5. 超限兜底降级方案

当尺寸无论如何压缩都无法放入单张安全画布时,提供多级降级:

  1. 降低整体导出分辨率,适当牺牲画质;
  2. 前端分片输出多张切片,后端服务合并完整大图;
  3. 超长文档优先使用 SVG / PDF 导出,替代全量 Canvas 绘制;
  4. UI 交互提示用户:图片尺寸过大,建议缩小素材后重新生成。

三、原生 iOS(WKWebView / Swift CoreGraphics)补充防护

如果是 App 内嵌 H5(WKWebView),除前端 JS 防护,原生层也需要兜底:

  1. 监听 webViewWebContentProcessDidTerminate Web 进程崩溃回调,做页面恢复;
  2. 监听内存警告 didReceiveMemoryWarning,通知 JS 销毁全部临时画布、释放图片资源;
  3. 控制页面并发画布数量,串行绘图,禁止同时创建多张大画布。

原生 CoreGraphics / UIGraphics 绘制同样存在内存上限:

  1. 预估内存占用:字节 = width * height * 4,创建前预判可用内存;
  2. 巨图绘制使用 mmap 内存映射,像素写入临时磁盘文件,不占用进程 RAM;
  3. 使用完毕及时释放 CGContext、CGImage 对象,避免 C 层内存泄漏。

四、高频踩坑清单

  1. ❌ 混淆 CSS 样式尺寸与 canvas 物理宽高,只用样式判断是否超限;
  2. ❌ 认为 OffscreenCanvas / WebGL 纹理可以绕过 4096 尺寸限制;
  3. ❌ 大量临时画布不手动销毁,依赖 JS GC 自动回收;
  4. ❌ 直接将相机原图(几 k~ 上万像素原图)丢入 canvas,未提前下采样;
  5. ❌ 使用 UA 判断设备上限,没有运行时探测真实画布最大支持尺寸;
  6. ❌ 多张大画布并发绘制,短时间耗尽 WebKit 进程内存,触发 Jetsam 杀进程。

五、总结

iOS Canvas 超限问题最大难点是静默失败,无报错,线上很难复现定位。

完整防护链路:运行时探测上限 → 前置尺寸校验 → DPR 动态降采样 → 超大图分片瓦片绘制 → 绘制后手动释放内存 → 超限降级兜底

海报导出、长截图等重 Canvas 场景,建议上线前在老旧小内存 iPhone 真机充分测试,避免线上大面积白屏崩溃。

如果你需要,我可以补充一份完整可上线的【分片瓦片绘制 + 图片合并】生产 JS Demo,包含 blob 拼接、进度回调、异常捕获。


扩展阅读

正文完
可以使用微信扫码关注公众号(ID:xzluomor)
post-qrcode
 0
评论(没有评论)
验证码