最近在使用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()
})
}