关闭

[置顶] High Performance Canvas Game for Android(高性能Android Canvas游戏开发)

标签: graphicswebkitandroidbrowsercanvas
4927人阅读 评论(0) 收藏 举报
分类:


Rule #0 为移动平台进行优化


为移动平台进行优化是十分重要的,因为移动平台的性能大概只有桌面平台的1/10左右(*1),它通常意味着:
  1. 更慢的CPU速度,这意味着不经过优化的JavaScript代码,性能会十分糟糕;
  2. 更小的内存,没有虚拟内存的支持,这意味着加载太多的资源容易导致内存不足,JSVM更容易引发GC,并且GC造成的停顿时间也越长;
  3. 更慢的GPU速度,没有独立的显存,内存带宽相比PC要慢的多,这意味着即使使用GPU对Canvas进行加速,您仍然需要小心网页DOM树的复杂度,游戏所使用的分辨率(Canvas的大小),图片资源的分辨率,游戏场景的复杂度和尽量避免Overdraw等等;

注释:
  1. 如果您需要对移动平台浏览器的性能有一个全面的了解,建议阅读文章“Why mobile web apps are slow”(原文译文)和”5 Myths About Mobile Web Performance“(原文译文)。

Rule #1 为Android而不是iOS而优化


牢记这一点非常重要,Mobile Safari的Canvas渲染机制跟Android平台有很大的不同,特别地针对Mobile Safari进行优化的Canvas游戏在Android平台的渲染性能会十分的糟糕。

Mobile Safari使用了iOS/MacOS平台特有的IOSurface作为Canvas的Buffer,当通过Canvas API往IOSurface绘制内容的时候是没有GPU加速的,iOS仍然使用CPU进行绘制,但是将一个IOSurface绘制到另外一个IOSurface上的时候,iOS会使用GPU的2D位拷贝加速单元进行加速(*1)。这种机制其实也是iOS UI界面Layer Rendering渲染架构的基础。所以为iOS优化的Canvas游戏会倾向于使用大量的Off-Screen Canvas(*2),不管是静态的图片也好,还是需要动态产生的内容也好,统统都缓存到一个Off-Screen Canvas上,最终游戏场景的绘制就是一个把一堆Off-Screen Canvas绘制到一个On-Screen Canvas的过程,这样就可以充分利用iOS绘制IOSurface到IOSurface使用了GPU加速的特性来提升渲染性能。

但是这种大量使用Off-Screen Canvas的做法在Android平台的浏览器上会非常糟糕。Android平台并没有IOSurface的同等物(一块同时支持CPU读写和GPU读写的缓冲区),所以它只能使用GL API对Canvas进行加速,一个加速的Canvas,它的Buffer是一个GL Texture(被attach到一个FBO上),这意味着:
  1. 无论是绘制到Canvas本身,还是Canvas绘制到Canvas都是GPU加速的,普通的位图要绘制到Canvas上,需要先被加载到一个Texture中;
  2. Texture Buffer只能通过GPU进行读写,如果要使用CPU访问,必须先通过glReadPixels把内容从显存拷贝到一块普通内存,而这个操作会非常慢并且会造成GL渲染流水线的阻塞;
  3. 如果游戏频繁创建和销毁一些比较小的Canvas,会很容易造成显存的碎片化,让显存的耗尽速度加快,并且创建太多的Canvas也容易把GPU资源都消耗光,导致后续的分配失败和渲染错误;
  4. 当每个Game Loop都对多个Canvas进行同时更新时,会导致GL Context不断地切换不同的Render Target(FBO),而这对GL的渲染性能有很大的影响;

后续的内容会进一步说明如何针对使用GL加速的Canvas渲染架构进行优化。

注释:
  1. 一般GPU都会带有多个独立的加速单元,包括3D加速单元,支持GL和D3D这样的3D API;2D位拷贝加速单元,对将一块缓冲区绘制到另外一块缓冲区进行加速;2D矢量绘制加速单元,支持像OpenVG这样的2D API,但是Android平台只支持通过GL API使用GPU加速,并没有公开的2D位拷贝加速API,虽然2.x的时候厂商可以提供一个copybit模块对位拷贝进行加速,供SurfaceFlinger使用,但这个模块不是通用的,并且不对外公开,另外在4.x的时候也已经移除了。
  2. Off-Screen Canvas在文中是指display:none,没有attach到DOM树上的Canvas,相对于On-Screen Canvas而言。

Rule #2 优化网页的DOM树结构

 

Canvas只是网页的一部分,它最终要显示出来还需要浏览器对网页本身的绘制,如果网页的DOM树结构越复杂,浏览器花在网页绘制上的时间也就越长,网页绘制占用的CPU/GPU资源也就越多,而留给Canvas绘制的CPU/GPU资源也就越少,这意味着Canvas绘制本身需要的时间也越长。并且网页绘制的耗时越长,Canvas最终更新到屏幕上的延迟也就越长,总之,这是一个此消彼涨的过程。所以,优化网页的DOM树结构,让其尽可能简单,浏览器就可以把更多的系统资源花费在Canvas的绘制上,从而提升Canvas的渲染性能。

 

最理想的DOM结构就是只包含一个<body>,加上一个<div>作为容器和加上一个<canvas>本身。如果Canvas上面需要显示其它的网页内容,最好只是用于一些临时使用的对话框之类的东西,而不是一直固定显示。

 

Rule #3 优化网页元素的css背景设置

 

跟#2的道理一样,背景设置越简单或者根本不设置背景,浏览器花费在网页本身绘制的开销也就越小,一般来说<canvas>元素本身都不应该设置css背景,它的背景应该通过Canvas API来绘制,避免浏览器在绘制<canvas>元素时还要先绘制背景,然后再绘制Canvas的内容。另外<body>和其它元素都应该首先考虑使用background-color而不是background-image,因为background-image的绘制耗时比一个纯色填充要大的多,而且背景图片本身还需要消耗额外的显存资源(生成Texture)。

 

 Rule #4 使用合适大小的Canvas

 

考虑移动设备的性能限制,Canvas不适宜太大,否则需要消耗更多的GPU资源和内存带宽,480p或者600p是一个比较合适的选择(横屏游戏可以选择800p或者960p),一般不应该超过720p。并且游戏图片资源的分辨率应该跟Canvas的分辨率保持一致,也就是正常情况下图片绘制到Canvas上应该不需要缩放。

 

我们需要避免创建了一个较大的Canvas,但是仍然使用较低分辨率的图片资源,图片绘制到Canvas上还需要经过缩放的情况。这样做毫无意义,因为游戏本身的分辨率是由图片资源的分辨率来决定的,上述的情形既不能提升游戏的精美程度,也白白浪费了系统资源。

 

如果自己预先指定了Canvas的大小,又希望Canvas在网页中全屏显示,可以通过<meta viewport>标签设置viewport的大小(*1,*2),直接告诉浏览器网页虚拟viewport的宽度应该是多少,并且让viewport的宽度等于Canvas的宽度,而浏览器会自动按照viewport宽度和屏幕宽屏的比值对网页进行整体放大。

 

注释:
  1. <meta viewport>的设置可以参考这个例子:http://www.craftymind.com/factory/guimark3/bitmap/GM3_JS_Bitmap.html
  2. Android系统浏览器在网页不指定viewport宽度时,它会认为这是一个WWW页面,并且使用980的默认viewport宽度,UC浏览器也遵循了同样的做法。这意味着您不设置viewport宽度,并且直接使用window.clientWidth作为Canvas的宽度时,就会创建出一个980p的Canvas,通常这是毫无意义的;

 Rule #5 避免使用多个On-Screen Canvas


如#1所述,多个Canvas同时更新会降低GL渲染的效率,并且如#2所述,多个On-Screen Canvas会导致网页本身的绘制时间增加,所以应该避免使用。

Rule #6 合理地使用Off-Screen Canvas


在GL加速的Canvas渲染架构下,合理地使用Off-Screen Canvas可以在某些特定的场景提升渲染性能,但是不合理的使用反而会导致性能下降。

  1. 将图片绘制到一个Off-Screen Canvas上,然后把这个Canvas当作原图片使用,这种用法,如#1所述,在iOS上是有用的,但是在Android上不必要的,甚至会导致额外的资源浪费(虽然渲染性能还是一样)。浏览器会自动将需要绘制到Canvas的图片加载成Texture并缓存起来,避免反复加载,只要这个缓存池大小没有超过限制,图片的绘制就只需要付出一次贴图的开销,这对GPU来说是很小的;
  2. 避免使用过大或者过小的Off-Screen Canvas —— 首先过大的Canvas会超过系统的Max Texture Size而无法进行加速(*1,*2),而太小的Canvas(*3,*4),因为对它加速不但不会加快渲染速度,反而会导致如#1所述的一些问题 —— 加快GPU资源的耗尽,频繁切换Render Target的额外开销等,所以也是不加速的;
  3. 避免频繁动态创建和销毁Canvas对象,这样很容易引发GC,而且浏览器为了避免大量的Canvas Buffer把GPU资源耗尽,还会在接近临界值时进行强制GC(*5),而强制GC造成的停顿比一般GC还要长,通常会达到500ms~1000ms。所以一般来说应该事先生成所有需要的Canvas然后一直使用,或者建立一个缓存池来回收和重用;
  4. Canvas初始大小设置后就不应该再改变,否则浏览器需要为它分配新的Buffer;
  5. 需要动态生成的内容,可以在一个Off-Screen Canvas上预先生成,然后直接将这个Canvas绘制到On-Screen Canvas上,但是这个生成应该是一次性的(或者偶尔),而不是每个Game Loop都需要更新,否则就会造成#1所述的问题 —— 频繁切换Render Target的额外开销
  6. 如果场景中的部分内容很少发生变化,但是位置,缩放比例,旋转角度,透明度等属性需要频繁变化,可以把一个Off-Screen Canvas当作Layer使用,缓存这部分内容,然后在绘制这部分内容时就直接绘制这个Off-Screen Canvas

总结一下,Off-Screen Canvas的使用应该尽量遵循以下原则:
  1. 数量适中(越少越好);
  2. 大小适中(面积128x128以上,长宽2048以内,并且为2的幂次方对GPU来说是最友好的);
  3. 一次创建,大小固定,持续使用;
  4. 读多写少(可以在每个Game Loop都绘制到On-Screen Canvas上,但是自身更新/变化的次数应该很少,避免每个Game Loop都更新);

注释:
  1. 一般手机的Max Texture Size是2048,高端的机器可能会到4096或者8192,Canvas长宽任意一边超过这个大小都无法使用Texture做为自己的Buffer;
  2. 非加速的Canvas仍然使用普通的Bitmap作为自己的Buffer,这意味着它的绘制仍然使用CPU,并且它绘制到另外一个Canvas还需要先加载成Texture,而加速的Canvas本身就是一个Texture,所以它绘制到另外一个Canvas上只需要一次贴图的开销;
  3. WebKit默认的设置是128x128大小以内的Canvas不加速,UC和Chrome都使用了默认的设置;
  4. 把一个比较大,加速的Canvas绘制到一个比较小,不加速的Canvas会非常非常慢,这是因为浏览器需要从显存拷贝内容到普通内存,拷贝的速度很慢并且会造成GL渲染流水线的阻塞
  5. 一个小技巧是,如果一个Canvas不再使用,可以将它的长宽设置为0,这样在JSVM的垃圾收集器还没有回收该Canvas对象时,浏览器就可以先释放它的Buffer,这样可以避免浏览器因为Buffer占用太多而不得不强制GC,不过总的来说最好还是自己建立缓存池;

Rule #7 避免频繁调用getImageData,putImageData和toDataURL


因为它们都会需要从显存拷贝内容到普通内存,或者相反,拷贝的速度很慢并且会造成GL渲染流水线的阻塞。所以不要在每个Game Loop都调用这几个API。

Rule #8 如果需要最大帧率,优先使用requestAnimationFrame而不是Timer


如果您的游戏只需要20或者30帧,那么就只能使用Timer。但是如果希望达到设备本身的最大帧率,则应该使用rAF,因为rAF可以让浏览器把网页绘制,Canvas绘制跟屏幕刷新保持同步,减少Canvas更新的延迟,并且在网页不可见的时候还可以自动停止rAF回调,避免无谓的浪费电池。

Rule #9 图片资源大小应该对GPU友好


  1. 避免使用太多小图片,而是应该把它们拼接成一张大图;
  2. 拼接的图片长宽应该是2的幂次方,并且小于或者等于2048,512x512,1024x1024都是不错的选择;
  3. 拼接的图片应该尽量避免留下大量空白区域,造成无谓的浪费;

1
0

猜你在找
查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:239176次
    • 积分:3147
    • 等级:
    • 排名:第11220名
    • 原创:64篇
    • 转载:0篇
    • 译文:2篇
    • 评论:95条
    文章分类
    最新评论