抖音/微信小程序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()
    })
}

参考文档

相关推荐
一粒马豆1 天前
three.js用粒子使用canvas生成的中文字符位图材质
3d·three.js·canvas·中文字符·粒子动画
猫猫村晨总4 天前
基于 Vue3 + Canvas + Web Worker 实现高性能图像黑白转换工具的设计与实现
前端·vue3·canvas
好_快5 天前
Echarts vs G2
echarts·数据可视化·canvas
MervynZ14 天前
动效实现的进化之路:详解前端各类动效技术与选型指南
前端·canvas·动效
猫猫村晨总19 天前
前端图像处理实战: 基于Web Worker和SIMD优化实现图像转灰度功能
前端·图像处理·vue3·canvas·web worker
万少20 天前
鸿蒙元服务实战-笑笑五子棋(5)
前端·harmonyos·canvas
左耳咚22 天前
【Fabric.js 系列】Fabric.js 是如何实现元素的平移、旋转、缩放的
前端·javascript·canvas
放逐者-保持本心,方可放逐23 天前
js 之图片流式转换及图片处理+createObjectURL+canvas+webgl+buffer
开发语言·javascript·webgl·canvas·createobjecturl·buffer
webmote1 个月前
Fabric.js 入门教程:扩展自定义对象的完整实践(V6)
运维·javascript·canvas·fabric·绘图