做者:闲鱼技术-炉军java
前言
记得在13年作群视频通话的时候,多路视频渲染成为了端上一个很是大的性能瓶颈。缘由是每一路画面的高速上屏(PresentRenderBuffer or SwapBuffer 就是讲渲染缓冲区的渲染结果呈现到屏幕上)操做,消耗了很是多的CPU和GPU资源。
那时候的解法是将绘制和上屏进行分离,将多路画面抽象到一个绘制树中,对其进行遍历绘制,绘制完成之后统一作上屏操做,而且每一路画面再也不单独触发上屏,而是统一由Vsync信号触发,这样极大的节约了性能开销。
那时候甚至想过将整个UI界面都由OpenGL进行渲染,这样还能够进一步减小界面内诸如:声音频谱,呼吸效果等动画的性能开销。但因为各类条件限制,最终没有去践行这个想法。
万万没想到的是这种全界面OpenGL渲染思路还能够拿来作跨平台。
Flutter渲染框架
Layer Tree:这个是dart runtime输出的一个树状数据结构,树上的每个叶子节点,表明了一个界面元素(Button,Image等等)。
Skia:这个是谷歌的一个跨平台渲染框架,从目前IOS和anrdroid来看,SKIA底层最终都是调用OpenGL绘制。Vulkan支持还不太好,Metal还不支持。
Shell:这里的Shell特指平台特性(Platform)的那一部分,包含IOS和Android平台相关的实现,包括EAGLContext管理、上屏的操做以及后面将会重点介绍的外接纹理实现等等。
从图中能够看出,当Runtime完成Layout输出一个Layertree之后,在管线中会遍历Layertree的每个叶子节点,每个叶子节点最终会调用Skia引擎完成界面元素的绘制,在遍历完成后,在调用glPresentRenderBuffer(IOS)或者glSwapBuffer(Android)按完成上屏操做。
基于这个基本原理,Flutter在Native和Flutter Engine上实现了UI的隔离,书写UI代码时不用再关心平台实现从而实现了跨平台。
问题
正所谓凡事有利必有弊,Flutter在与Native隔离的同时,也在Flutter Engine和Native之间竖立了一座大山,Flutter想要获取一些Native侧的高内存占用图像(摄像头帧、视频帧、相册图片等等)会变得困难重重。传统的如RN,Weex等经过桥接NativeAPI能够直接获取这些数据,可是Flutter从基本原理上就决定了没法直接获取到这些数据,而Flutter定义的channel机制,从本质上说是提供了一个消息传送机制,用于图像等数据的传输必然引发内存和CPU的巨大消耗。
解法
为此,Flutter提供了一种特殊的机制:外接纹理(ps:纹理Texture能够理解为GPU内表明图像数据的一个对象)
上图是前文提到的LayerTree的一个简单架构图,每个叶子节点表明了dart代码排版的一个控件,能够看到最后有一个TextureLayer节点,这个节点对应的是Flutter里的Texture控件(ps.这里的Texture和GPU的Texture不同,这个是Flutter的控件)。
当在Flutter里建立出一个Texture控件时,表明的是在这个控件上显示的数据,须要由Native提供。
如下是IOS端的TextureLayer节点的最终绘制代码(android相似,可是纹理获取方式略有不一样),总体过程能够分为三步
1:调用external_texture copyPixelBuffer,获取CVPixelBuffer
2:CVOpenGLESTextureCacheCreateTextureFromImage建立OpenGL的Texture(这个是真的Texture)
3:将OpenGL Texture封装成SKImage,调用Skia的DrawImage完成绘制。
void IOSExternalTextureGL::Paint(SkCanvas& canvas, const SkRect& bounds) {
if (!cache_ref_) {
CVOpenGLESTextureCacheRef cache;
CVReturn err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL,
[EAGLContext currentContext], NULL, &cache);
if (err == noErr) {
cache_ref_.Reset(cache);
} else {
FXL_LOG(WARNING) << "Failed to create GLES texture cache: " << err;
return;
}
}
fml::CFRef<CVPixelBufferRef> bufferRef;
bufferRef.Reset([external_texture_ copyPixelBuffer]);
if (bufferRef != nullptr) {
CVOpenGLESTextureRef texture;
CVReturn err = CVOpenGLESTextureCacheCreateTextureFromImage(
kCFAllocatorDefault, cache_ref_, bufferRef, nullptr, GL_TEXTURE_2D, GL_RGBA,
static_cast<int>(CVPixelBufferGetWidth(bufferRef)),
static_cast<int>(CVPixelBufferGetHeight(bufferRef)), GL_BGRA, GL_UNSIGNED_BYTE, 0,
&texture);
texture_ref_.Reset(texture);
if (err != noErr) {
FXL_LOG(WARNING) << "Could not create texture from pixel buffer: " << err;
return;
}
}
if (!texture_ref_) {
return;
}
GrGLTextureInfo textureInfo = {CVOpenGLESTextureGetTarget(texture_ref_),
CVOpenGLESTextureGetName(texture_ref_), GL_RGBA8_OES};
GrBackendTexture backendTexture(bounds.width(), bounds.height(), GrMipMapped::kNo, textureInfo);
sk_sp<SkImage> image =
SkImage::MakeFromTexture(canvas.getGrContext(), backendTexture, kTopLeft_GrSurfaceOrigin,
kRGBA_8888_SkColorType, kPremul_SkAlphaType, nullptr);
if (image) {
canvas.drawImage(image, bounds.x(), bounds.y());
}
}
复制代码
最核心的在于这个external_texture_对象,它是哪里来的呢?
void PlatformViewIOS::RegisterExternalTexture(int64_t texture_id,NSObject<FlutterTexture>*texture) {
RegisterTexture(std::make_shared<IOSExternalTextureGL>(texture_id,texture));
}
复制代码
能够看到,当Native侧调用RegisterExternalTexture前,须要建立一个实现了FlutterTexture这个protocol的对象,而这个对象最终就是赋值给这个external_texture_。这个external_texture_就是Flutter和Native之间的一座桥梁,在渲染时能够经过他源源不断的获取到当前所要展现的图像数据。
如图,经过外接纹理的方式,实际上Flutter和Native传输的数据载体就是PixelBuffer,Native端的数据源(摄像头、播放器等)将数据写入PixelBuffer,Flutter拿到PixelBuffer之后转成OpenGLES Texture,交由Skia绘制。
至此,Flutter就能够容易的绘制出一切Native端想要绘制的数据,除了摄像头播放器等动态图像数据,诸如图片的展现也提供了Image控件以外的另外一种可能(尤为对于Native端已经有大型图片加载库诸如SDWebImage等,若是要在Flutter端用dart写一份也是很是耗时耗力的)。
优化
上述的整套流程,看似完美解决了Flutter展现Native端大数据的问题,可是许多现实状况是这样:
如图工程实践中视频图像数据的处理,为了性能考虑,一般都会在Native端使用GPU处理,而Flutter端定义的接口为copyPixelBuffer,因此整个数据流程就要通过:
GPU->CPU->GPU的流程。而熟悉GPU处理的同窗应该都知道,CPU和GPU的内存交换是全部操做里面最耗时的操做,一来一回,一般消耗的时间,比整个管道处理的时间都要长。
既然Skia渲染的引擎须要的是GPU Texture,而Native数据处理输出的就是GPU Texture,那能不能直接就用这个Texture呢?答案是确定的,可是有个条件:EAGLContext的资源共享(这里的Context,也就是上下文,用来管理当前GL环境,能够保证不一样环境下的资源的隔离)。
如图所示,Flutter一般状况下会建立4个Runner,这里的TaskRunner相似于IOS的GCD,是以队列的方式执行任务的一种机制,一般状况下(一个Runner会对应一个线程,而Platform Runner会在跑在主线程),这里和本文相关的有三个Runner:GPU Runner、IORunner、Platform Runner。
Platform Runner:运行在main thread上,负责全部Native与Flutter Engine的交互。
一般状况下一个使用OpenGL的APP线程设计都会有一个线程负责加载资源(图片到纹理),一个线程负责渲染的方式。可是常常会发现为了可以让加载线程建立出来的纹理,可以在渲染线程使用,两个线程会共用一个EAGLContext。可是从规范上来讲这样使用是不安全的,多线程访问同一对象加锁的的话不可避免会影响性能,代码处理很差甚至会引发死锁。所以Flutter在EAGLContext的使用上使用了另外一种机制:
两个线程各自使用本身的EAGLContext,彼此经过ShareGroup(android为shareContext)来共享纹理数据。(这里须要提一下的是:虽然两个Context的使用者分别是GPU 和IO Runner,可是现有Flutter的逻辑下两个Context都是在Platform Runner下建立的,这里不知道是Flutter是出于什么考虑,可是由于这个设计给咱们带来很大的困扰,后面会说到。)
对于Native侧使用OpenGL的模块,也会在本身的线程下面建立出本身线程对应的Context,为了可以让这个Context下建立出来的Texture,可以输送给Flutter 端,并交由Skia完成绘制,咱们在Flutter建立内部的两个Context时,将他们的ShareGroup透出,而后在Native侧保存好这个ShareGroup,
当Native建立Context时,都会使用这个ShareGroup进行建立。这样就实现了Native和Flutter之间的纹理共享。
经过这种方式来作external_texture有两个好处:
第一:节省CPU时间,从咱们测试上看,android机型上一帧720P的RGBA格式的视频,从GPU读取到CPU大概须要5ms左右,从CPU在送到GPU又须要5ms左右,哪怕引入了PBO,也仍是有5ms左右的耗时,这对于高帧率场景显然是不能接受的。
第二:节省CPU内存,显而易见数据都在GPU中传递,对于图片场景尤为适用(由于可能同一时间会有不少图片须要展现)。
后语
至此,咱们介绍完了Flutter外接纹理的基本原理,以及优化策略。可是可能你们会有疑惑,既然直接用Texture做为外接纹理这么好,为何谷歌要用Pixelbuffer?这里又回到了那个命题,凡事有利必有弊,使用Texture,必然须要将ShareGroup透出,也就是至关于将Flutter的GL环境开放了,若是外部的OpenGL操做不当(OpenGL的对象对于CPU而言就是一个数字,一个Texture或者FrameBuffer咱们断点看到的就是一个GLUint,若是环境隔离,咱们随便操做deleteTexture,deleteFrameBuffer不会影响别的环境下的对象,可是若是环境打通,这些操做极可能会影响Flutter本身的Context下的对象),因此
做为一个框架的设计者,保证框架的封闭完整性才是首要。
咱们在开发过程当中,碰到一个诡异的问题,定位了好久发现就是由于咱们在主线程没有setCurrentContext的状况下,调用了glDeleteFrameBuffer,从而误删了Flutter的FrameBuffer,致使flutter 渲染时crash。因此建议若是采用这种方案的同窗,Native端的GL相关操做务必至少听从如下一点:
2:在有GL操做的函数调用前,要加上setCurrentContext。
还有一点就是本文大多数逻辑都是以IOS端为范例进行陈述,Android总体原理是一致的,可是具体实现上稍有不一样,Android端Flutter自带的外接纹理是用SurfaceTexture实现,其机理其实也是CPU内存到GPU内存的拷贝,Android OpenGL没有ShareGroup这个概念,用的是shareContext,也就是直接把Context传出去。而且Shell层Android的GL实现是基于C++的,因此Context是一个C++对象,
要将这个C++对象和AndroidNative端的java Context对象进行共享,须要在jni层这样调用:
static jobject GetContext(JNIEnv* env,
jobject jcaller,
jlong shell_holder) {
jclass eglcontextClassLocal = env->FindClass("android/opengl/EGLContext");
jmethodID eglcontextConstructor = env->GetMethodID(eglcontextClassLocal, "<init>", "(J)V");
void * cxt = ANDROID_SHELL_HOLDER->GetPlatformView()->GetContext();
if((EGLContext)cxt == EGL_NO_CONTEXT)
{
return env->NewObject(eglcontextClassLocal, eglcontextConstructor, reinterpret_cast<jlong>(EGL_NO_CONTEXT));
}
return env->NewObject(eglcontextClassLocal, eglcontextConstructor, reinterpret_cast<jlong>(cxt));
}
复制代码
联系咱们
若是对文本的内容有疑问或指正,欢迎告知咱们。
闲鱼技术团队是一只短小精悍的工程技术团队。咱们不只关注于业务问题的有效解决,同时咱们在推进打破技术栈分工限制(android/iOS/Html5/Server 编程模型和语言的统一)、计算机视觉技术在移动终端上的前沿实践工做。做为闲鱼技术团队的软件工程师,您有机会去展现您全部的才能和勇气,在整个产品的演进和用户问题解决中证实技术发展是改变生活方式的动力。
简历投递:guicai.gxy@alibaba-inc.com