记两次优化导致的Bug
人云,过早的优化不如不优化。个人的理解,还是要具体情况具体分析。一般这里认为的是,开发过程的变动会导致之前做出的优化失灵。 如果没有,那说明你赌对了,不,说明你眼光真好。
废话到此结束。 本文记录了两次巧合。优化本身一般不会导致Bug,但是可能会有其它没预料到的问题。Bug本天成,菜鸡偶遇之。
和requestFrameAnimation有关。
和requestFrameAnimation
就是在下一帧的时候调用, 调用这个方法后会返回一个id,凭借此id可以取消下一帧的调用cancelAnimationFrame(id)
。
在图形渲染中,经常会使用连续渲染的方式来不断更新视图,使用此api可以保证每一帧只绘制一次,一帧绘制一次就够了,两次没有意义。 当然,这里说的一次,是指一整个绘制,包含了后处理(比如three的postprocess
)的一系列操作。
大致情况是这样的, 现在已经封装好了一个3d渲染器,然后在更新场景数据的,会有比较多的循环,这个时候,3d渲染就可以停掉了,这里就是做了这个优化。
来看主要代码,渲染器部分。只要调用animate方法就会循环渲染, pause停止,resume恢复渲染。 看上去好像没啥问题。
// 渲染
animate = () => {
if (this.paused ) return ;
this.animateId = requestAnimationFrame(this.animate);
};
pause() {
this.paused = true;
}
resume() {
if (this.paused) {
this.paused = false;
this.animate()
}
}
再看,更新数据的部分。 问题就出在这里,更新数据这个操作,是没有任何异步的,也就是说在当前帧的宏任务里,先暂停后恢复, 结果就是,下一帧执行animate的时候, paused仍为true, 这个优化毫无意义。
view.current.pause() ;
//更新数据完成
view.current.resume()
无意义倒也没啥,但是 resume方法执行了之后,会新开一个requestAnimationFrame, 上一个requestAnimationFrame的循环又没有取消,
所以现在, 一帧里会执行两次render方法。 这个卡顿啊,就十分明显了,也就是当时的项目模型还不是特别大,只有在某些场景模型较大的时候才会卡,所以没急着处理。
过了几个月,临近更新了,不得不解决。排查的时候,是靠git记录二分才找出来的。
其次,要说明的是,使用了requestAnimationFrame连续渲染,这种在一帧里先暂停再继续的操作肯定是无意义的,因为下一帧才执行,只看执行前的结果。
当然,前面的暂停和继续的逻辑,也是一个隐患。 于是,就改成了我惯用的那种方式。 那就是暂停的时候,只是不渲染,循环继续空跑,如此而已。
// 渲染
animate = () => {
!this.paused && this.renderer.render(this.scene, this.camera);
this.animateId = requestAnimationFrame(this.animate);
};
pause() {
this.paused = true;
}
resume() {
this.paused = false;
}
和 URL.create 有关
上面的那个代码,还可以说是写的人不熟悉requestAnimationFrame
的特性造成的。 这一次的这个,真的是因为,引用关系比较多。
这次是一个纯2d项目,这里有一个将(后端)处理后的图片在canvas2d编辑器上显示出来的操作。这个图,叫产品图, 产品图是用户上传的, 也可以不经过后端处理,就直接进入到2d编辑器里。 后端处理之后,返回的是一个url。 所以有了下面的函数。
function afterImgMatter(img:File|string) {
setShowMatter(false);
if (img instanceof File) {
tempImg.src = URL.createObjectURL(img);
} else {
tempImg.src = img
}
console.log(tempImg.src);
if (productImg) {
let url = (productImg.image as HTMLImageElement)?.src
url.startsWith('blob') && URL.revokeObjectURL(url)
}
if (refCanvas.current) {
tempImg.onload = () => {
// 产品图层不可手动删除
productImg = refCanvas.current!.addImg(tempImg, false, null, 'product');
if (img instanceof File) {
productImg.id = globalData.productId;
} else {
productImg.id = globalData.matteredId;
}
setCanvasEditState(true);
!curScene && changeSene(scenes[0]);
}
}
}
如果是没经过后端处理的,那个图片就是文件转URL,就用到了这个方法URL.createObjectURL
, 这个方法为二进制文件生成一个临时路径,mdn强调了一定要手动释放它。 浏览器在 document 卸载的时候,会自动释放它们,也就是说在这之前GC不会自动释放他们,即便找不到引用了。
那这个优化,我必然不能不做。 所以,我就判断了一下,如果之前的src是这个方法生成的,我就去释放它。 于是,在重新上传图片,也就是二次触发这个方法的时候出问题了, 画布直接白屏了,原来是报错了。
Uncaught DOMException: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The HTMLImageElement provided is in the 'broken' state.
说我提供的图像源是破碎的,所以这个drawImage方法不能执行,其实上面还有一个报错,我之前一直没在意, 说得是一个url路径失效了,图片加载失败了,因为我释放了路径,所以我觉得出现这个,应该是正常的。
但是,现在结合drawImage的执行失败,这里还是有问题的。 我发现,确实就是因为我释放了要用做图像源的那个路径。 因为这里的productImg和 tempImg其实是通一个引用,只不过语义不同。
解决办法也很简单,那就是把释放的这一段代码,放到onload的回调里执行即可,图片加载完成之后,释放这个url也能正常工作
tempImg.onload = () => {
if (productImg) {
let url = (productImg.image as HTMLImageElement)?.src
url.startsWith('blob') && URL.revokeObjectURL(url)
}
}
这里之所以会有 productImg和 tempImg通一个引用,语义不同, 也是因为我想优化一下。之前是每次加载图片的时候,都new Image
,实际上这个可以用同一个对象,所以就有了tempImg
结束
本文记录了两个bug,顺带说了一下requestAnimationFrame
和 URL.createObjectURL
的部分用法。
没有直接阐述他们的用法,有兴趣了解的可以直接看文档。
ectURL