QQ小游戏显存管理优化
今天碰到开发商的小游戏出现因为内存警告的情况而被杀掉的情况,但是调试了该小游戏后发现并没有泄露,小游戏退出时释放了所有内存。
在Xcode里发现内存的使用一直稳定增长,进一个场景会增加10M左右,长时间玩的话内存会爆,应该是没有及时释放导致的,但这个游戏的场景很简单,按理来说不该占用这么多内存。
分析问题原因
用Allocation跑了一下,发现内存明显小于Xcode里显示的占用量,并且分配大头在VM这一块,而应用本身在CPU的内存占用并不大。
由于Allocation里是看不到VRAM(Video Memory)的使用的,加上VM提供的信息,可以确认内存爆的原因是显存占用过多。
一般来说应用所用的大部分对象都在堆上,应用无法直接操作VM,所以不用过分关心VM的使用情况。但当使用了图形借口的话,系统底层创建一些数据结构保存渲染数据,如IOSurface
。
调试JS代码发现开发商在JS层对WebGL的对象管理相当随意,基本不考虑复用,而且不会主动删除WebGL对象,而OpenGL会通过对象的句柄做更精细的管理。
这种做法在宿主是浏览器里时问题不大,但在小游戏平台会导致内存回收不及时,随着游戏进程而逐渐累积大量内存。
最后在JS层用JS对象包装了Native创建的texture, buffer等对象,当GC时,被回收的对象会删除对应的Native OpenGL对象。改造原有的删除、缓存逻辑,配合GC回收,小游戏的运行时内存占用小了50M以上。
移动端内存分布
和电脑上显卡自带显存不同,移动端的芯片是一块SOC,整个的VRAM和RAM是在同一块连续的内存区域上,所以访问VRAM与一般的内存访问方式相同。虽然苹果从来没开放自己的芯片设计方案,但是诸多资料显示VRAM和RAM之间其实存在一个“共享内存”,这块内存作为中介可以高速读取,访问带宽是一般RAM的2-8倍,并且GPU和CPU都可以访问。可以推测IOSurface
其实就是对这种内存结构封装。
iOS显存
iOS上不能直接操作VRAM,不能像使用RAM一样去寻址,但可以通过OpenGL/Metal、CG等这样的图形接口去间接管理。VRAM拥有更高的带宽,这在数据读写上非常占有优势。比如UIKit的-[UIImage imageNamed:]
就会把图像缓存到VRAM里,为后续的显示提供更高的性能。
应用的VM里有很大一部分都和显存相关,如VM:IOSurface, VM:CoreAnimation等等。
IOSurface
IOSurface
是MAC和iOS上用来存储FBO、RBO等渲染数据的底层数据结构。IOSurface长久以来只有MAC才可以使用,用它可以实现跨进程的渲染,在iOS上的使用非常受限,只开放了很基础的功能,可以用来在不同渲染框架如CoreGraphics, OpenGL, Metal之间传递纹理数据等。
之前的Allcation截图里有3个IOSurface对象就占用了7.86M,可见这个类是比较占内存的。
IOSurface
最强的是提供了CPU里访问VRAM的方式。比如创建IOSurface
对象之后,在CPU往对象里塞纹理数据,GPU就可以直接使用纹理,所以之前推测IOSurface
就是封装了之前提到的那种中介内存。
IOSurface
这个数据结构是和硬件强相关的,之前盘古越狱就是利用了这个接口来破解,获取了最高权限。
小游戏显存管理优化
小游戏的架构特点是JS层驱动Native实现渲染逻辑。而我们要做的就是保证JS层逻辑运行正确的情况下尽量占用最少的内存, 以下是目前主要使用的方法。
懒加载
当JS层创建一个纹理对象时,底层不会立即创建对应的对象, 而是记录下它对应的纹理单元、绑定信息、纹理数据,当它最终有效使用时才在GPU上分配内存创建。
var texture = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);//1 激活
gl.bindTexture(gl.TEXTURE_2D, texture);//2 绑定
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);//3 加载数据
//...
gl.drawArrays(mode, first, count);
当纹理对象在Native层最终没有绑定或者加载纹理数据的话,那这个纹理对象判断为无效的,最终不会创建。
OpenGL对象释放时机优化
当开发商在JS层创建GL对象时,OpenGL函数虽然是返回一个句柄,但会包装句柄成一个JS对象返回给JS,并记录在当前WebGLContext。当这个JS对象被GC时,开发商主动调用delete操作时,或者当前WebGLContext释放时,会删除其对于的OpenGL对象,释放GPU显存。
纹理缓存
在JS传过来的image、imageData、canvas对象做渲染时,Native在上传他们的数据到GPU后,会根据他们的Key来缓存对应的纹理对象,之后的渲染命令会优先从缓存中取纹理,若有就直接使用,不用二次上传到GPU。
纹理共享
利用shareGroup来让不同Context共享纹理,这样在不同Context的纹理对象、图像数据可以直接复用,
不需要先读取到CPU,再塞到对应的纹理中
纹理复用
当JS需要加载或更新一个纹理,如果输入源image、imageData、canvas且满足复用条件时,Native直接拷贝其对应的纹理对象到目标纹理中,不需要CPU作为中介传纹理数据。
纹理LRU离线机制
Native实现了LRU纹理LRU离线缓存机制,若一个纹理长时间没有用到,那么当他被LRU缓存淘汰后会从GPU删除并保存到沙盒里,后续用到才恢复到GPU中。
状态机的管理
Native层实现了一套状态机来记录GL对象的信息、渲染的状态。
当需要在不同Context之间传递GL对象时可以直接利用状态机操作。还可以利用状态机的信息可以减少GL对象的的冗余操作,如一个纹理不会重复加载同一张图像。
面向未来
上面提到的方法主要是优化小游戏运行时的显存占用,在iOS上只能通过Xcode memory report看到RAM+VRAM使用情况。除此之外,还有Program、Shader对象,字体对象可以复用,离屏GLContext复用等等方法没有应用。在未来的工作里,会继续努力降低小游戏的内存占用。