紧接上文,终端开发使用的WindVane、wax、ReactNative等已经是一种跨平台的技术,我们称之为上层跨平台,Cocos2d-x这种直接使用C/C++,我们成为底层跨平台。上层跨平台,提升开发效率;下层跨平台,提升程序性能。
1. 为什么Cocos2d-x性能比Native开发要好?
因为Cocos2d-X是游戏引擎呗,人家是专业做游戏特效的好不好,直接调用GPU的OpenGL绘图的好不好。打开Cocos2d-X代码,感触最深的不是CCNode这些游戏节点,cocos2d-x已经开始为App开发做准备了!
大家看到了什么?UIImageView?UIScrollView?OMG!当我们还在纠结要不要学习一下游戏开发的时候,人家都已经开始做UI系统了,有木有!!
很长一段时间,我们以为Game和App之间有一种天然的鸿沟,长久以来,这两种技术之间没有直接的联系,App可以做各种UI控件,游戏可以做各种UI特效,当游戏开发者想NativeApp大步的走过来的时候,我们还在学习如何用UITableView去优化图片的显示,岂不知在游戏世界里,早已实现了图片的异步加载、解码、缓存了!
别忘了,从iOS7.0开始,苹果已经默认集成了GameKit.framework,颤抖吧!游戏和App即将面临傻傻分不清楚的时候了。
2. 一张图片在OpenGL的世界里,是如何从SDCard显示到屏幕
这一次,我们不是讲UIImage如何加载一个图片,而是自己读取图片文件,解码图像,上传纹理,推送顶点缓冲,刷新帧缓冲,一步步显示图片到屏幕上。
上图之前,我们先了解几个概念:
2.1 帧缓冲:
我们要显示的视图绘制到屏幕上,本质是一个int32 buffer[width x height x 4]的一个数组,不管2D还是3D,我们都要吧GPU的对象投影到这个buffer中,一个像素4字节表示[R/G/B/A]。
2.2 顶点缓冲:
终端设备,只支持精简版的OpenGL,称之为OpenGL ES,这里我们只用2.0版本。在这个版本中,我们只能画三角形,一个正方形的图片纹理,需要两个三角形才可以描述:
我们要画图片之前,就要告诉OpenGLES图片顶点的位置,先忽略三角形的顺序问题,就这样告诉GPU:【ABDBDC】,同时设置当前的激活的纹理,就可以绘制图片了。
2.3 上传纹理:
现在绝大多数手机,都有独立的显卡芯片,每个显卡芯片都有独立的显存,这么就清楚了:我们常说的内存是CPU直接操控的内存条容量,算上显存,就有两个缓存空间了,程序员可以发挥的空间又多了,是不是有点小激动?
2.4 图片解码:
常见的jpg、png等图片文件,都是压缩后的文件,如果不压缩,一张1024x1024的图片,至少需要4M空间。一张图片文件解码到内存,需要解码器来解码,不幸的是,Android很多机型没有集成硬件解码,庆幸的是iOS设备内部支持硬件解码。如果一张图片文件可以瞬间在底层硬件解码完成,要比上层代码优化,更加直接!这就是我们为什么一直追求的是底层跨平台的原因。
2.5 讲了很多概念了,我们上图吧
3. 原来图片可以不占用内存!
我们用C的API来读取一张图片,用libpng来解码一张png图片,调用OpenGL ES2.0API上传图片到GPU显存,指定绘图坐标,显示一张图片,我们上代码吧:
一)读取文件:
AEFileData AEFileDataWithPathfile(std::string& pathfile) {
AEFileData data;
FILE* file = fopen(pathfile.c_str(), "rb");
if (file == NULL) {
return data;
}
fseek(file, 0, SEEK_END);
data.size = (GLuint)ftell(file);
fseek(file, 0, SEEK_SET);
data.bytes = (GLchar*)malloc(sizeof(GLchar) * data.size);
fread(data.bytes, sizeof(GLchar), data.size, file);
fclose(file);
return data;
}
二)解码图片:
为了跨平台通用,我们用libpng来解码图片,获取png的format和rgba数组,【代码太多,就不贴了】
三)上传GPU:
glGenTextures(1, &_textureId);
glBindTexture(type, _textureId);
glTexImage2D(type, 0, _format, _size.width, _size.height, 0, _format, GL_UNSIGNED_BYTE, pixels);
四)释放内存
AEFileData file = AEFileDataWithPathfile(pathfile);
this->initPNG((GLuchar*)file.bytes, file.size);
__FREE(file.bytes);
五)指定顶点索引,并绘制
vb[0] = {d11, t11, color};
vb[1] = {d21, t21, color};
vb[2] = {d12, t12, color};
vb[3] = {d21, t21, color};
vb[4] = {d12, t12, color};
vb[5] = {d22, t22, color};
glDrawArrays(GL_TRIANGLES, 0, _vertexIndex);
4. 对比iOS与Android,图片的渲染和展示做了什么?
Android的技术框架更偏于上层,看不到底层C/C++代码实现,优化的空间有限;Objective-C更靠近C语言,优化的空间明显。下面我们以iOS为例:
5. 主流框架的图片优化思路
要读懂图片的展示与优化,就要看看当前主流的图片优化框架,他们是如何做优化的?主流的ImageCache框架:SDWebImage/FastImageCache/TBImageCache
左边的是SDWebImage,中间是FastImageCache,右边是TBImageCache
SDWebImage:
将解码后的图片分别保存到内存和SDCard,将复杂的操作放到了子线程中;
FastImageCache:
直接将文件读取到mmap映射的文件中,减少了一次从系统内核到用户空间的一次拷贝【然并卵】
TBImageCache:
在首次加载图片时,根据用户需要,对图片增加圆角阴影处理,增加了异步线程的处理的工作量,同时保存到sdcard中,消耗了额外的资源,换取的是更好的图片性能【解决了圆角阴影在终端的痛点】
图片缓存框架,只是让CPU负载曲线更均衡
,有没有可能再进一步优化?
6. 游戏世界与App世界对比
CPU与GPU是终端的双驾马车,缺一不可,CPU load过高,导致终端卡顿,同理GPU也是如此。iOS的UIKit框架封装的太好了,也是优化做的最好的,让我们看不到底层的原理,看不到iOS是如何平衡CPU、GPU、RAM、GPU RAM的4者之间的关系,让我们大胆大猜测一下吧:
一)UIImageView管理者UIImage对象
UIImage就是内存中得bitmap数组,当我们需要绘制一张图片的时候,CPU发出指令,上传BitMap到GPU的显存中,GPU返回生成的纹理ID给CPU。
二)为了提高iOS的性能,苹果做出了一个优化策略,不释放CPU的bitmap
当GPU显存空间不足的时候,iOS会释放一部分图片纹理,以节省显存空间,但不会释放内存的bitmap。当一张图片重新需要绘制的时候,iO快速从内存bitmap快速上传到显存,完成绘制过程。
三)当内存的bitmap释放的时候,同步释放显存的纹理对象
本质意义上说,bitmap在内存中得存储,不是真正显示的那个缓存,当bitmap上传到GPU的时候,GPU会在显存中申请同样大小的空间,来存放bitmap,并返回纹理ID,每当CPU需要显示某张图片的时候,只需要高速GPU要绘制那个纹理ID就可以了。
四)上传GPU后,我们完全可以释放内存bitmap
释放内存的bitmap很简单: free(bitmap);
这样内存里只剩下了对应的GPU纹理的ID,当GPU内存紧张的时候,程序要要自己释放不需要的纹理ID,如果这个ID在某个时候有需要使用了,怎么办?那要把之前的读取文件到上传GPU重走一遍,太耗性能了,我想苹果也是不想看到这个原因,才不会轻易释放bitmap的
总结:游戏的架构模式,需要开发者自己优化所有场景
游戏开发者不仅需要优化CPU内存,也要优化GPU内存,通过将所有图片合成一张大的纹理图片,来统一管理GPU内存,需要把大量浮点数运算根据不同的场景,或者交给CPU或者交给GPU,让CPU和GPU的负载达到均衡,各司其职。
这一些优化,对于我们做系统上层应用的无线开发,是很难想象有多少坑等着我们,终端App的开发,还是使用上层框架好,图形图像的世界水很深,有想法的同学,可以继续关注下一章分享,想了解我们的同学,可以用Android下载安装我们的Demo: