抖音/微信小程序canvas内存溢出问题的解决办法(ios)

最近在使用2d canvas渲染的时候发现抖音小程序和微信小程序(ios)都存在内存溢出的问题,将解决办法记录如下。

1. 分拆渲染流程 + 虚拟canvas

在业务中,我需要每秒25帧渲染画面,当我准备了200M的Images交给canvas绘制的时候,canvas的内存也会增加,最终增加的内存可能是200M+300M。而这个新增的300M,即使我把Images全部删除,有时候也不会降低到0。(如果我在代码中一边drawImage一边释放Image,就会导致画面闪动...可能跟框架本身有关)

基于上述问题,方法一可以简单概括如下:

  • 创建两个canvas,一个负责UI层绘制,另一个责负责复合各种素材,然后交给前一个canvas绘制。这里需要注意下,部分小程序平台是支持offscreenCanvas的,这就很nice.
  • 对canvas绘制进行流程分拆。举个例子,我要绘制一个开幕动画,涉及到很多舞台元素,那么我就需要把这个流程拆成很多步,然后给每一步依次创建offscreenCanvas。由于内存开销的原因,建议在流程状态切换的时候再清除上一个offscreenCanvas + 创建新的offscreenCanvas。

NOTE: 只有将offscreenCanvas手动清空才能彻底释放内存。(这个过程可能不是实时的,但几秒内就会释放掉,还是可以接受的)

伪代码如下:

js 复制代码
let main_canvas = '...'
let main_canvas_ctx = '....'

let off_canvas_map = new Map;
let game_state = ref(-1);  //枚举就不写了...

onLoad(()=> {
    initMainCanvas();
    setupOffScreenCanvas(0)
})

const initMainCanvas = ()=> {...}

const setupOffScreenCanvas = (newState:number)=> {
    if(!off_canvas_map.has(newState)) {
        let off_canvas = uni.creteOffscreenCanvas()
        let ctx = off_canvas.getContext('2d')
        off_canvas_map.set(newState, {
            off_canvas, ctx
        })
    }
}

const destoryOffScreenCanvas = (oldState:number)=> {
    if(off_canvas_map.has(oldState)) {
        let {
            off_canvas, ctx
        } = off_canvas_map.get(oldState)
        off_canvas = null
        ctx = null
        off_canvas_map.delete(oldState)
    }
}

watch(game_state, (nV, oV)=> {
    setupOffScreenCanvas(nV)
    destoryOffScreenCanvas(oV)
})


onShow(()=> {
    main_canvas.requestAnimationFrame(()=> {
        _render()
    })
})

const _render = ()=> {
    if(game_state.value == 0) {
        renderForStage0()  //在这里面渲染offscreenCanvas
    }
    if(game_state.value == 1) {
        renderForStage1()
    }
    ....
    
    
    let {off_canvas} = off_canvas_map.get(game_state)
    off_canvas && main_canvas_ctx.drawImage(off_canvas, 0, 0)
    main_canvas.requestAnimationFrame(()=> {
        _render()
    })
 
}

2. webgl +纹理贴图绘制

小程序内使用webgl绘制可以避免canvas占用大量内存的问题。关键在于找到一个相对简单的方法把创建好的Image转变成问题贴图。

在知乎找到了一个相对可以用的轮子,手动改了下代码。如果需要修改图像的显示位置,关键在于verticesTexCoords

js 复制代码
const vertexShaderSrc = `
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
varying vec2 v_TexCoord;
void main() {
 gl_Position = a_Position;
 v_TexCoord = a_TexCoord;
}
`;

const fragmentShaderSrc = `
precision highp float;
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main() {
  gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}
`;

/**** 渲染器生成处理 ****/
// 创建顶点渲染器
let vertexShader: any
// 创建片元渲染器
let fragmentShader: any
// 程序对象
let program: any

// 顶点坐标,纹理坐标
const verticesTexCoords = new Float32Array([
	-1.0, -1.0,  0.0, 1.0,
  1.0, -1.0, 1.0, 1.0,
  -1.0, 1.0, 0.0, 0.0,
  1.0, 1.0, 1.0, 0.0
]);
const FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;
let texture: any, u_Sampler: any


const initWebglCanvas = () => {
    vertexShader = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(vertexShader, vertexShaderSrc);
    gl.compileShader(vertexShader);

    fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(fragmentShader, fragmentShaderSrc);
    gl.compileShader(fragmentShader);

    program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);
    gl.useProgram(program);
    gl.program = program;

    // 创建缓存对象
    const verticesTexBuffer = gl.createBuffer();
    // 绑定缓存对象到上下文
    gl.bindBuffer(gl.ARRAY_BUFFER, verticesTexBuffer);
    // 向缓存区写入数据
    gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);

    // 获取 a_Position 变量地址
    const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    // 将缓冲区对象分配给 a_Position 变量
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
    // 允许访问缓存区
    gl.enableVertexAttribArray(a_Position);

    // 传入纹理坐标位置信息
    const a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
    gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
    gl.enableVertexAttribArray(a_TexCoord);

    /***** 纹理对象 *****/
    texture = gl.createTexture(); // 创建纹理对象
    u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler'); // 获取 u_Sampler 地址

    // gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); // 翻转纹理图像的 y 轴
    gl.activeTexture(gl.TEXTURE0); // 开启 0 号纹理单元
    gl.bindTexture(gl.TEXTURE_2D, texture); // 将我们的材质对象绑定上去

    // 配置纹理参数
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

}

onShow(()=> {
    _render()
})

const _render = async () => {
    //获取image  -> 需要提前创建好并存入map, 不要在render中createImage, 会来不及渲染
    let key = Date.now() / 1000 //...假装获取image_key
    let img = image_map.get(key)
    if(img) {
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
        gl.uniform1i(u_Sampler, 0);
        gl.clearColor(0, 0, 0, 1);
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    }
    main_canvas.requestAnimationFrame(()=> {
        _render()
    })
}

参考文档

相关推荐
漠河愁21 天前
pdf文件渲染到canvas
canvas·pdf.js·fabirc.js
xachary1 个月前
前端使用 Konva 实现可视化设计器(21)- 绘制图形(椭圆)
canvas·konva
x007xyz1 个月前
前端纯手工绘制音频波形图
前端·音视频开发·canvas
甄齐才2 个月前
canvas绘制文本时,该如何处理首行缩进、自动换行、多内容以省略号结束、竖排的呢?
canvas·html2canvas·海报·html转图片·文章分享·dom-to-image·html转image
万水千山走遍TML2 个月前
canvas绘制表格
前端·javascript·vue.js·canvas·canvas绘图·在vue中使用canvas·canvas绘制表格
xachary2 个月前
前端使用 Konva 实现可视化设计器(19)- 连接线 - 直线、折线
javascript·vue·canvas·konva
梦想身高1米82 个月前
canvas.toDataURL后图片背景变成黑色
前端·canvas
x007xyz2 个月前
Fabric.js实时播放视频并扣除绿幕
前端·javascript·canvas
xachary2 个月前
前端使用 Konva 实现可视化设计器(18)- 素材嵌套 - 加载阶段
算法·canvas·konva
LeaferJS2 个月前
LeaferJS 1.0 重磅发布:强悍的前端 Canvas 渲染引擎
前端·canvas