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

        共 12881字,需瀏覽 26分鐘

         ·

        2022-08-08 22:52

        點擊上方藍字關(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 歡迎大家訪問



        往期推薦


        本文原創(chuàng)公眾號:群英傳,授權(quán)轉(zhuǎn)載請聯(lián)系微信(Tomcat_xu),授權(quán)后,請在原創(chuàng)發(fā)表24小時后轉(zhuǎn)載。
        < END >
        作者:徐宜生

        更文不易,點個“三連”支持一下??


        瀏覽 46
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 宝贝做一次嘛忍不住了视频 | 噢美一级片| 借种迷乱嗯啊h | 大香蕉视频精品 | 国产精品探花在线 |