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

點擊上方藍字關(guān)注我,知識會給你力量

Flutter的圖片系統(tǒng)基于Image的一套架構(gòu),但是這東西的性能,實在不敢恭維,感覺還停留在Native開發(fā)至少5年前的水平,雖然使用上非常簡單,一個Image.network走天下,但是不管是解碼性能還是加載速度,抑或是內(nèi)存占用和緩存邏輯,都遠遠不如Native的圖片庫,特別是Glide。雖然Google一直在有計劃優(yōu)化Flutter Image的性能,但現(xiàn)階段,體驗最佳的圖片加載方式,還是通過插件,使用Glide來進行加載。
所以,在混編的大環(huán)境下,將Flutter的圖片加載功能托管給原生,是最合理且性能最佳的方案。
那么對于橋接到原生的方案來說,主要有兩個方向,一個是通過Channel來傳遞加載的圖像的二進制數(shù)據(jù)流,然后在Flutter內(nèi)解析二進制流后來解析圖像,另一個則是通過外接紋理的方式,來共享圖像內(nèi)存,顯然,第二種方案是更好的解決方案,不管從內(nèi)存消耗還是傳輸性能上來說,外接紋理的方案,都是Flutter橋接Native圖片架構(gòu)的最佳選擇。
雖然說外接紋理方案比較好,但是網(wǎng)絡(luò)上對于這個方案的研究卻不是很多,比較典型的是Flutter官方Plugins中的視頻渲染的方案,地址如下所示。
https://github.com/flutter/plugins/tree/main/packages/video_player
這是我們研究外接紋理的第一手方案,除此之外,閑魚開源的PowerImage,也是基于外接紋理的方案來實現(xiàn)的,同時他們也給出了基于外接紋理的一系列方案的預研和技術(shù)基礎(chǔ)研究,這些也算是我們了解外接紋理的最佳途徑,但是,基于阿里的一貫風格,我們不太敢直接大范圍使用PowerImage,研究研究外接紋理,來實現(xiàn)一套自己的方案,其實是最好的。
https://www.infoq.cn/article/MLMK2bx8uaNb5xJm13SW
https://juejin.cn/post/6844903662548942855
外接紋理的基本概念
其實上面兩篇閑魚的文章,已經(jīng)把外接紋理的概念講解的比較清楚了,下面我們就簡單的總結(jié)一下。
首先,F(xiàn)lutter的渲染機制與Native渲染完全隔離,這樣的好處是Flutter可以完全控制Flutter頁面的繪制和渲染,但壞處是,F(xiàn)lutter在獲取一些Native的高內(nèi)存數(shù)據(jù)時,通過Channel來進行傳遞就會導致浪費和性能壓力,所以Flutter提供了外接紋理,來處理這種場景。
在Flutter中,系統(tǒng)提供了一個特殊的Widget——Texture Widget。Texture在Flutter的Widget Tree中是一個特殊的Layer,它不參與其它Layer的繪制,它的數(shù)據(jù)全部由Native提供,Native會將動態(tài)渲染數(shù)據(jù),例如圖片、視頻等數(shù)據(jù),寫入到PixelBuffer,而Flutter Engine會從GPU中拿到相應的渲染數(shù)據(jù),并渲染到對應的Texture中。
Texture實戰(zhàn)
Texture方案來加載圖片的過程實際上是比較長的,涉及到Flutter和Native的雙端合作,所以,我們需要創(chuàng)建一個Flutter Plugin來完成這個功能的調(diào)用。
我們創(chuàng)建一個Flutter Plugin,Android Studio會自動幫我們生成對應的插件代碼和Example代碼。
整體流程
Flutter和Native之間,通過外接紋理的方式來共享內(nèi)存數(shù)據(jù),它們之間相互關(guān)聯(lián)的紐帶,就是一個TextureID,通過這個ID,我們可以分別關(guān)聯(lián)到Native側(cè)的內(nèi)存數(shù)據(jù),也可以關(guān)聯(lián)到Flutter側(cè)的Texture Widget,所以,一切的故事,都是從TextureID開始的。
Flutter加載圖片的起點,從Texture Widget開始,Widget初始化的時候,會通過Channel請求Native,創(chuàng)建一個新的TextureID,并將這個TextureID返回給Flutter,將當前Texture Widget與這個ID進行綁定。
接下來,F(xiàn)lutter側(cè)將要加載的圖片Url通過Channel請求Native,Native側(cè)通過TextureID找到對應的Texture,并在Native側(cè)通過Glide,用傳遞的Url進行圖片加載,將圖片資源寫入Texture,這個時候,F(xiàn)lutter側(cè)的Texture Widget就可以實時獲取到渲染信息了。
最后,在Flutter側(cè)的Texture Widget回收時,需要對當前的Texture進行回收,從而將這部分內(nèi)存釋放。
以上就是整個外接紋理方案的實現(xiàn)過程。
Flutter側(cè)
首先,我們需要創(chuàng)建一個Channel來注冊上面提到的幾個方法調(diào)用。
class MethodChannelTextureImage extends TextureImagePlatform {
@visibleForTesting
final methodChannel = const MethodChannel('texture_image');
@override
Future<int?> initTextureID() async {
final result = await methodChannel.invokeMethod('initTextureID');
return result['textureID'];
}
@override
Future<Size> 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<int?> disposeTextureID(int textureID) async {
var params = {};
params["textureID"] = textureID;
final result = await methodChannel.invokeMethod('disposeTextureID', params);
return result['textureID'];
}
}
接下來,回到Flutter Widget中,封裝一個Widget用來管理Texture。
在這個封裝的Widget里面,你可以對尺寸作調(diào)整,或者是對生命周期進行管理,但核心只有一個,那就是創(chuàng)建一個Texture。
Texture(textureId: _textureID),
使用前面創(chuàng)建的Channel,來完成流程的加載。
@override
void initState() {
initTextureID().then((value) {
_textureID = value;
_textureImagePlugin.loadByTextureID(widget.url, _textureID).then((value) {
if (mounted) {
setState(() => bitmapSize = value);
}
});
});
super.initState();
}
Future<int> 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側(cè)的流程就完成了——創(chuàng)建TextureID——>綁定TextureID和Url——>回收TextureID。
Native側(cè)
Native側(cè)的處理都集中在Plugin的注冊類中,在注冊時,我們需要創(chuàng)建TextureRegistry,這是系統(tǒng)提供給我們使用外接紋理的入口。
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "texture_image")
channel.setMethodCallHandler(this)
context = flutterPluginBinding.applicationContext
textureRegistry = flutterPluginBinding.textureRegistry
}
接下來,我們需要對Channel進行處理,分別實現(xiàn)前面提到的三個方法。
"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中創(chuàng)建一個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<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
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<String, Int> = HashMap()
reply["width"] = bitmap.width
reply["height"] = bitmap.height
result.success(reply)
}
}
override fun onLoadCleared(placeholder: Drawable?) {
}
})
}
}
load方法,就是我們熟悉的Glide了,通過Glide來獲取對應Url的圖片數(shù)據(jù),再通過SurfaceTextureEntry,來創(chuàng)建Surface對象,并將Glide返回的數(shù)據(jù),寫入到Surface中,最后,將圖像的寬高回傳給Flutter,做后續(xù)的一些處理。
"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一直在申請新的內(nèi)存,就會導致Native內(nèi)存一直上漲而不會被回收,所以,在Flutter側(cè)調(diào)用dispose后,我們需要對相應TextureID對應的資源進行回收。
以上,我們就完成了Native的處理,通過和Flutter側(cè)配合,借助Glide的高效加載能力,我們就完成就一次完美的圖片加載過程。
總結(jié)
通過外接紋理來加載圖片,我們可以有下面這些優(yōu)點。
復用Native的高效、穩(wěn)定的圖片加載機制,包括緩存、編解碼、性能等 降低多套方案的內(nèi)存消耗,降低App的運行內(nèi)存 打通Native和Flutter,圖片資源可以進行內(nèi)存共享
但是,當前這個方案也并不是「完美的」,只能說,上面的方案是一個「可用」的方案,但還遠遠沒有達到「好用」的級別,為了更好的實現(xiàn)外接紋理的方案,我們還需要處理一些細節(jié)。
復用、復用,還是TMD復用,對于同Url的圖片、加載過的圖片,在Native端和Flutter端,都應該再做一套緩存機制 對于Gif和Webp的支持,目前為止,我們都是處理的靜態(tài)圖片,還未添加動態(tài)內(nèi)容的處理,當然這一定是可以的,只不過我們還沒支持 Channel的Batch調(diào)用,對于一個列表來說,可能一幀中會同時產(chǎn)生大量的圖片請求,雖然現(xiàn)在Channel的性能有了很大的提升,但是如果能對Channel的調(diào)用做一個緩沖區(qū),那么對于特別頻繁的調(diào)用來說,會優(yōu)化一部分Channel的性能
所以這只是第一篇,后面我們會繼續(xù)針對上面的問題進行優(yōu)化,請各位拭目以待。
向大家推薦下我的網(wǎng)站 https://xuyisheng.top/ 點擊原文一鍵直達
專注 Android-Kotlin-Flutter 歡迎大家訪問
往期推薦
更文不易,點個“三連”支持一下??
