[置顶] High Performance Canvas Game for Android

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的渲染性能。css

 

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

 

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

 

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

 

 Rule #4 使用合适大小的Canvas

 

考虑移动设备的性能限制,Canvas不适宜太大,不然须要消耗更多的GPU资源和内存带宽,480p或者600p是一个比较合适的选择(横屏游戏能够选择800p或者960p),通常不该该超过720p。而且游戏图片资源的分辨率应该跟Canvas的分辨率保持一致,也就是正常状况下图片绘制到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. 拼接的图片应该尽可能避免留下大量空白区域,形成无谓的浪费
相关文章
相关标签/搜索