文 / 游族网络Mob云平台iOS开发专家 李永超
众所周知,由于iOS系统的封闭性,也出于保护用户隐私的角度,苹果并没有公开的API供开发者调用,来录制屏幕内容。导致许多游戏或者应用没有办法直接通过调用系统API的方式提供录制功能,用户也无法将自己一些玩游戏的过程录制下来分享到其他玩家。基于此,ShareREC应运而生。下面我们从说一下ShareREC的录屏的实现原理。
由于苹果UI是基于不同的引擎渲染,所以目前针对不同的引擎,主要是采用以下几种不同的方式实现:
- 原生UI。主要是指UIKit框架下面的UI,即苹果原生UI。其实现方式主要是通过获取当前显示的layer,然后通过Core Graphics将这个layer绘制成UIImage,然后将UIImage拼接成视频。这种做法有个问题,就是每一帧都需要使用Core Graphics来重绘,会造成CPU占用率暴涨,效率非常低。
- OpenGL 。由于 Unity 3D 或 Cocos2d两种引擎,在iOS设备上都是采用OpenGL ES这个底层库实现渲染,所以后面会将两者放在OpenGL中一起讨论。
- Metal。Metal是苹果推出的专门针对iPhone和iPad中GPU编程高度优化的框架。目前Unity 5已经支持64位iOS Metal技术,导出Xcode项目时,可以进行选择。Metal这个名称的来源是想说明这个图形框架的的确确是非常底层的- -底层到已经非常接近金属板了(metal)。
如果是基于越狱系统,开发者还可以通过调用系统的私有API方式,其中比较重要一个方法是UIGetScreenImage来实现录制功能,这种方式的优点是录制效率高且是无损画质,但同时也有一个致命的弱点,就是应用没办法上架。 ReplayKit。ReplayKit是苹果在iOS9上苹果公开的一个API,通过这个API,可以录制除AVPlayer播放视频以外的应用界面。但是由于对于系统版本要求比较高,同时由于没办法获取到录制的视频的路径,所以可定制化比较低。但iOS11的ReplayKit,已经可以拿到每一帧的回调(这个没有做详细验证,只是看到新的方法里面已经含有samplebuffer的回调,有兴趣的同学可以试验一下),这样就可以实现更高的定制化功能。所以不考虑低版本兼容性的话,也可以通过调用这个系统库实现。
目前ShareREC支持OpenGL和Metal两种渲染引擎的录制,上面提到过Unity3d与Cocos2d底层其实也是通过OpenGL来渲染的,所以在其上面开发的游戏,ShareREC均是完美支持的。ShareREC是通过HOOK(钩子)的方式,捕捉屏幕画面,进行录制的;其中心原理是首先捕获到当前绘制的内容,此时拿到绘制的纹理后,可以自行进行处理;然后重新将内容绘制到屏幕上【这一步很重要,否则由于已经渲染的内容被钩取,屏幕上将不会显示任何内容】。
下面我们将分别介绍ShareREC捕获两种引擎OpenGL和Metal的实现原理。
OpenGL
首先iOS系统默认支持OpenGL ES 1.0、ES2.0以及ES3.0 (OpenGL ES是OpenGL在移动端的简化版本)三个版本,三者之间并不是简单的版本升级,设计理念甚至完全不同。所以我们后续的一些操作还会对于版本的不同分别做处理。
废话不多说,首先我们是要先通过钩子,获取到当前绘制的上下文对象Context(Context是一个非常抽象的概念,我们姑且把它理解成一个包含了所有OpenGL状态的对象,如果我们把一个Context销毁了,那么OpenGL也不复存在)。画了一个图,仅供理解。
然后根据当前的context,创建捕获屏幕纹理CVOOpenGLESTextureRef,随后创建中间渲染纹理;最后绑定纹理到FBO上面,此时,原本绘制到屏幕上的内容,将转为绘制到我们创建的中间渲染纹理上面。此时,当OpenGL再次渲染屏幕内容时,将会首先被我们创建的屏幕纹理捕获,从而拿到渲染内容;最后再重新将渲染画面输出到屏幕。
其实现流程如图所示:
其中绑定纹理到FBO的代码如下:
//绑定纹理到FBO上 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _renderTexture, 0); GLint rbo; glGetIntegerv(GL_RENDERBUFFER_BINDING, &rbo); GLint fbo; glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fbo); //创建输出屏幕FBO glGenFramebuffers(1, &_outputFbo); glBindFramebuffer(GL_FRAMEBUFFER, _outputFbo); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo); //创建截图FBO glGenFramebuffers(1, &_captureFbo); glBindFramebuffer(GL_FRAMEBUFFER, _captureFbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, CVOpenGLESTextureGetName(_captureTexture), 0);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
上面主要阐述创建自己的renderTexture后,然后通过绑定纹理到FBO上面,执行这样的操作以后,原本输出到屏幕上的内容,将转为绘制到renderTexture中,然后再创建输出屏幕FBO,以及截图的FBO;最后再通过_captureFbo画入捕捉纹理,通过_outFbo输出到屏幕。
Metal
iOS8.0起,Apple为了更充分地发挥GPU的潜力,引入了Metal框架。Metal和OpenGL ES是并列的,他们都是应用对GPU访问的底层接口。而Metal则提供了更底层,更面向硬件的接口,这也是为何Apple给这个框架起名为“Metal”的原因。OpenGL ES3.1之前,GPU只能做图形渲染流水线,而不能直接做通用计算流水线。现在iOS的Metal把这道门打开了。通过Metal,我们可以直接使用通用计算流水线,也就是GPU的Compute Shader。因此,在目前的Metal框架中可以使用三种着色器——Vertex Shader、Fragment Shader以及Compute Shader。当然,正因为Metal要求GPU得具有通用计算能力,因此一些老旧的GPU就不能支持了。目前支持Metal的GPU必须是Apple A7开始的,也就是至少为Power VR 6系列。
首先我们先了解下Metal引擎的渲染流程,它的渲染流水线如下图所示:
目前很多API都通过具体的“类”来实现平台支持,不过Metal使用的方法是基于“协议”的。因为Metal中具体的类型是由运行的设备所决定的。这很好的鼓励了程序员选择面向接口编程而非面向实现,以降低程序的耦合。当然也意味着需要冒着风险大量的在Objective-C 运行时来对Metal的类型添加继承和扩展类型。
其整个流程如下图所示:
但协议的这种方式,又无形中增加了我们钩子的复杂程度。因为我们没有办法直接拿到相关的类;同时又考虑到兼容低版本Xcode环境的问题,我们也无法直接导入Metal框架,只能通过动态加载Metal.framework的方式。只能通过动态(NSClassFromString和NSSelectorFromString)获取相关类和方法的方式来钩取。同时基于“协议”的类,就只能通过dlsym/dlopen【最近苹果对热更新的审核比较严格,这种动态方法尽量还是少用】的方式获取。
再来说一下具体实现。其根本也是通过钩子进行的。其中一个最重要的一个钩子是presentDrawable:,这个主要是用于展示最终的渲染内容到屏幕上面的函数,其中有一个最重要的参数MTLDrawableRef,这个参数就是一个可绘制对象,也包含了最终要展示到屏幕的纹理。当我们取得最后展示到屏幕的drawable后,最后调用copyTextureAction方法将勾取到的内容画回屏幕:
id blitCommandEncoder = blitCommandEncoderAction(commandBuffer, _blitCommandEncoderSEL); id tmptexture = textFromDrawableAction(drawable, _textureFromDrawableSEL); copyTextureAction(blitCommandEncoder, _copyTextureSEL, tmptexture, 0, 0, OriginMake_SRE(0, 0, 0), SizeMake_SRE(textureSizeAction(captureTexture, _widthFromTextureSEL), textureSizeAction(captureTexture, _heightFromTextureSEL), textureSizeAction(captureTexture, _depthFromTextureSEL)), captureTexture, 0, 0, OriginMake_SRE(0, 0, 0)); endEncodingAction(blitCommandEncoder, _endEncodingSEL);
至此,整个Metal的录制过程就结束了。最后,将获取到的CVPixelBufferRef按照指定格式写入文件。
最后,关于音频与视频多线程同步的问题,是使用两个信号量dispatch_semaphore_t分别进行控制,以防引起线程崩溃。
上面就是ShareREC iOS分别对于OpenGL ES和Metal两种引擎的渲染的录制过程。其核心的方式就是通过HOOK的方式钩取最后要渲染的内容,然后再将原来的内容重新渲染到屏幕上。