萬萬沒想到——flutter這樣外接紋理

做者:閒魚技術-爐軍java

前言

記得在13年作羣視頻通話的時候,多路視頻渲染成爲了端上一個很是大的性能瓶頸。緣由是每一路畫面的高速上屏(PresentRenderBuffer or  SwapBuffer 就是講渲染緩衝區的渲染結果呈現到屏幕上)操做,消耗了很是多的CPU和GPU資源。
       那時候的解法是將繪製和上屏進行分離,將多路畫面抽象到一個繪製樹中,對其進行遍歷繪製,繪製完成之後統一作上屏操做,而且每一路畫面再也不單獨觸發上屏,而是統一由Vsync信號觸發,這樣極大的節約了性能開銷。
       那時候甚至想過將整個UI界面都由OpenGL進行渲染,這樣還能夠進一步減小界面內諸如:聲音頻譜,呼吸效果等動畫的性能開銷。但因爲各類條件限制,最終沒有去踐行這個想法。 
 
萬萬沒想到的是這種全界面OpenGL渲染思路還能夠拿來作跨平臺。

Flutter渲染框架

下圖爲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的線程結構:
如圖所示,Flutter一般狀況下會建立4個Runner,這裏的TaskRunner相似於IOS的GCD,是以隊列的方式執行任務的一種機制,一般狀況下(一個Runner會對應一個線程,而Platform Runner會在跑在主線程),這裏和本文相關的有三個Runner:GPU Runner、IORunner、Platform Runner。
GPU Runner:負責GPU的渲染相關操做。
IO 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相關操做務必至少聽從如下一點:
1:儘可能不要在主線程作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
相關文章
相關標籤/搜索