Flutter混編工程之打通紋理之路

語言: CN / TW / HK

Flutter的圖片系統基於Image的一套架構,但是這東西的效能,實在不敢恭維,感覺還停留在Native開發至少5年前的水平,雖然使用上非常簡單,一個Image.network走天下,但是不管是解碼效能還是載入速度,抑或是記憶體佔用和快取邏輯,都遠遠不如Native的圖片庫,特別是Glide。雖然Google一直在有計劃優化Flutter Image的效能,但現階段,體驗最佳的圖片載入方式,還是通過外掛,使用Glide來進行載入。

所以,在混編的大環境下,將Flutter的圖片載入功能託管給原生,是最合理且效能最佳的方案。

那麼對於橋接到原生的方案來說,主要有兩個方向,一個是通過Channel來傳遞載入的影象的二進位制資料流,然後在Flutter內解析二進位制流後來解析影象,另一個則是通過外接紋理的方式,來共享影象記憶體,顯然,第二種方案是更好的解決方案,不管從記憶體消耗還是傳輸效能上來說,外接紋理的方案,都是Flutter橋接Native圖片架構的最佳選擇。

雖然說外接紋理方案比較好,但是網路上對於這個方案的研究卻不是很多,比較典型的是Flutter官方Plugins中的影片渲染的方案,地址如下所示。

http://github.com/flutter/plugins/tree/main/packages/video_player

這是我們研究外接紋理的第一手方案,除此之外,閒魚開源的PowerImage,也是基於外接紋理的方案來實現的,同時他們也給出了基於外接紋理的一系列方案的預研和技術基礎研究,這些也算是我們瞭解外接紋理的最佳途徑,但是,基於阿里的一貫風格,我們不太敢直接大範圍使用PowerImage,研究研究外接紋理,來實現一套自己的方案,其實是最好的。

http://www.infoq.cn/article/MLMK2bx8uaNb5xJm13SW

http://juejin.cn/post/6844903662548942855

外接紋理的基本概念

其實上面兩篇閒魚的文章,已經把外接紋理的概念講解的比較清楚了,下面我們就簡單的總結一下。

首先,Flutter的渲染機制與Native渲染完全隔離,這樣的好處是Flutter可以完全控制Flutter頁面的繪製和渲染,但壞處是,Flutter在獲取一些Native的高記憶體資料時,通過Channel來進行傳遞就會導致浪費和效能壓力,所以Flutter提供了外接紋理,來處理這種場景。

在Flutter中,系統提供了一個特殊的Widget——Texture Widget。Texture在Flutter的Widget Tree中是一個特殊的Layer,它不參與其它Layer的繪製,它的資料全部由Native提供,Native會將動態渲染資料,例如圖片、影片等資料,寫入到PixelBuffer,而Flutter Engine會從GPU中拿到相應的渲染資料,並渲染到對應的Texture中。

Texture實戰

Texture方案來載入圖片的過程實際上是比較長的,涉及到Flutter和Native的雙端合作,所以,我們需要建立一個Flutter Plugin來完成這個功能的呼叫。

我們建立一個Flutter Plugin,Android Studio會自動幫我們生成對應的外掛程式碼和Example程式碼。

整體流程

Flutter和Native之間,通過外接紋理的方式來共享記憶體資料,它們之間相互關聯的紐帶,就是一個TextureID,通過這個ID,我們可以分別關聯到Native側的記憶體資料,也可以關聯到Flutter側的Texture Widget,所以,一切的故事,都是從TextureID開始的。

Flutter載入圖片的起點,從Texture Widget開始,Widget初始化的時候,會通過Channel請求Native,建立一個新的TextureID,並將這個TextureID返回給Flutter,將當前Texture Widget與這個ID進行繫結。

接下來,Flutter側將要載入的圖片Url通過Channel請求Native,Native側通過TextureID找到對應的Texture,並在Native側通過Glide,用傳遞的Url進行圖片載入,將圖片資源寫入Texture,這個時候,Flutter側的Texture Widget就可以實時獲取到渲染資訊了。

最後,在Flutter側的Texture Widget回收時,需要對當前的Texture進行回收,從而將這部分記憶體釋放。

以上就是整個外接紋理方案的實現過程。

Flutter側

首先,我們需要建立一個Channel來註冊上面提到的幾個方法呼叫。

``` class MethodChannelTextureImage extends TextureImagePlatform { @visibleForTesting final methodChannel = const MethodChannel('texture_image');

@override Future initTextureID() async { final result = await methodChannel.invokeMethod('initTextureID'); return result['textureID']; }

@override Future loadByTextureID(String url, int textureID) async { var params = {}; params["textureID"] = textureID; params["url"] = url; final size = await methodChannel.invokeMethod('load', params); return Size(size['width']?.toDouble() ?? 0, size['height']?.toDouble() ?? 0); }

@override Future disposeTextureID(int textureID) async { var params = {}; params["textureID"] = textureID; final result = await methodChannel.invokeMethod('disposeTextureID', params); return result['textureID']; } } ```

接下來,回到Flutter Widget中,封裝一個Widget用來管理Texture。

在這個封裝的Widget裡面,你可以對尺寸作調整,或者是對生命週期進行管理,但核心只有一個,那就是建立一個Texture。

Texture(textureId: _textureID),

使用前面建立的Channel,來完成流程的載入。

``` @override void initState() { initTextureID().then((value) { _textureID = value; _textureImagePlugin.loadByTextureID(widget.url, _textureID).then((value) { if (mounted) { setState(() => bitmapSize = value); } }); }); super.initState(); }

Future initTextureID() async { int textureID; try { textureID = await _textureImagePlugin.initTextureID() ?? -1; } on PlatformException { textureID = -1; } return textureID; }

@override void dispose() { if (_textureID != -1) { _textureImagePlugin.disposeTextureID(_textureID); } super.dispose(); } ```

這樣整個Flutter側的流程就完成了——建立TextureID——>繫結TextureID和Url——>回收TextureID。

Native側

Native側的處理都集中在Plugin的註冊類中,在註冊時,我們需要建立TextureRegistry,這是系統提供給我們使用外接紋理的入口。

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "texture_image") channel.setMethodCallHandler(this) context = flutterPluginBinding.applicationContext textureRegistry = flutterPluginBinding.textureRegistry }

接下來,我們需要對Channel進行處理,分別實現前面提到的三個方法。

"initTextureID" -> { val surfaceTextureEntry = textureRegistry?.createSurfaceTexture() val textureId = surfaceTextureEntry?.id() ?: -1 val reply: MutableMap<String, Long> = HashMap() reply["textureID"] = textureId textureSurfaces[textureId] = surfaceTextureEntry result.success(reply) }

initTextureID方法,核心功能就是從TextureRegistry中建立一個surfaceTextureEntry,textureId就是它的id屬性。

``` "load" -> { val textureId: Int = call.argument("textureID") ?: -1 val url: String = call.argument("url") ?: "" if (textureId >= 0 && url.isNotBlank()) { Glide.with(context).load(url).skipMemoryCache(true).into(object : CustomTarget() { override fun onResourceReady(resource: Drawable, transition: Transition?) { if (resource is BitmapDrawable) { val bitmap = resource.bitmap val imageWidth: Int = bitmap.width val imageHeight: Int = bitmap.height val surfaceTextureEntry: SurfaceTextureEntry = textureSurfaces[textureId.toLong()]!! surfaceTextureEntry.surfaceTexture().setDefaultBufferSize(imageWidth, imageHeight) val surface = if (surfaceMap.containsKey(textureId.toLong())) { surfaceMap[textureId.toLong()] } else { val surface = Surface(surfaceTextureEntry.surfaceTexture()) surfaceMap[textureId.toLong()] = surface surface } val canvas: Canvas = surface!!.lockCanvas(null) canvas.drawBitmap(bitmap, 0F, 0F, null) surface.unlockCanvasAndPost(canvas) val reply: MutableMap = HashMap() reply["width"] = bitmap.width reply["height"] = bitmap.height result.success(reply) } }

        override fun onLoadCleared(placeholder: Drawable?) {
        }
    })
}

} ```

load方法,就是我們熟悉的Glide了,通過Glide來獲取對應Url的圖片資料,再通過SurfaceTextureEntry,來建立Surface物件,並將Glide返回的資料,寫入到Surface中,最後,將影象的寬高回傳給Flutter,做後續的一些處理。

"disposeTextureID" -> { val textureId: Int = call.argument("textureID") ?: -1 val textureIdLong = textureId.toLong() if (surfaceMap.containsKey(textureIdLong) && textureSurfaces.containsKey(textureIdLong)) { val surfaceTextureEntry: SurfaceTextureEntry? = textureSurfaces[textureIdLong] val surface = surfaceMap[textureIdLong] surfaceTextureEntry?.release() surface?.release() textureSurfaces.remove(textureIdLong) surfaceMap.remove(textureIdLong) } }

disposeTextureID方法,就是對dispose的Texture進行回收,否則的話,Texture一直在申請新的記憶體,就會導致Native記憶體一直上漲而不會被回收,所以,在Flutter側呼叫dispose後,我們需要對相應TextureID對應的資源進行回收。

以上,我們就完成了Native的處理,通過和Flutter側配合,藉助Glide的高效載入能力,我們就完成就一次完美的圖片載入過程。

總結

通過外接紋理來載入圖片,我們可以有下面這些優點。

  • 複用Native的高效、穩定的圖片載入機制,包括快取、編解碼、效能等
  • 降低多套方案的記憶體消耗,降低App的執行記憶體
  • 打通Native和Flutter,圖片資源可以進行記憶體共享

但是,當前這個方案也並不是「完美的」,只能說,上面的方案是一個「可用」的方案,但還遠遠沒有達到「好用」的級別,為了更好的實現外接紋理的方案,我們還需要處理一些細節。

  • 複用、複用,還是TMD複用,對於同Url的圖片、載入過的圖片,在Native端和Flutter端,都應該再做一套快取機制
  • 對於Gif和Webp的支援,目前為止,我們都是處理的靜態圖片,還未新增動態內容的處理,當然這一定是可以的,只不過我們還沒支援
  • Channel的Batch呼叫,對於一個列表來說,可能一幀中會同時產生大量的圖片請求,雖然現在Channel的效能有了很大的提升,但是如果能對Channel的呼叫做一個緩衝區,那麼對於特別頻繁的呼叫來說,會優化一部分Channel的效能

所以這只是第一篇,後面我們會繼續針對上面的問題進行優化,請各位拭目以待。