閒魚"同款"的Flutter圖片下載功能(demo版)

左手木亽發表於2020-04-25

前不久閒魚團隊的公眾號發了一篇文章講了閒魚團隊在Flutter圖片框架的演進過程文章,裡面講到了使用外接紋理的方式來實現圖片下載功能:閒魚Flutter圖片框架架構演進(超詳細),本文的用意就是動手實現閒魚的這個外接紋理圖片下載功能。

在剛學Flutter的時候我們的圖片下載功能一般都是直接使用Flutter官方提供的api來載入網路圖片,如:

Image(
  image: NetworkImage("https://www.xxx.com/xx.jpg")
)
複製程式碼

這樣子已經可以實現了介面需要展示的圖片了,可是為什麼閒魚團隊不這麼寫還要"折騰"什麼圖片下載框架呢?在閒魚的文章中丟擲的三點其實就可以解釋了為什麼還要對Flutter的圖片下載功能進行進一步封裝實現:

在這裡插入圖片描述
特別是在混合Flutter開發的時候,很多時候都在想如何才能更高效的複用原生已載入的圖片顯得特別頭疼。在看了閒魚的文章之後外接紋理的思路確實讓我眼前一亮,似乎找到了點複用的頭緒。

什麼是外接紋理? 文章中一直在講的外接紋理是個什麼概念? 其實外接紋理在程式碼上叫做Texture,這個類在Flutter中的程式碼量非常少一共沒幾行:

class Texture extends LeafRenderObjectWidget {
  /// Creates a widget backed by the texture identified by [textureId].
  const Texture({
    Key key,
    @required this.textureId,
  }) : assert(textureId != null),
       super(key: key);

  /// The identity of the backend texture.
  final int textureId;

  @override
  TextureBox createRenderObject(BuildContext context) => TextureBox(textureId: textureId);

  @override
  void updateRenderObject(BuildContext context, TextureBox renderObject) {
    renderObject.textureId = textureId;
  }
}
複製程式碼

雖然程式碼少,但是功能確實強大,整個Flutter渲染流程中需要的東西都提供了。在我看來Texture跟原生的結合的關鍵就是textureId,大致的理解就是Flutter端的TextTure和原生端的Surface兩者通過textureId完成相互繫結,從而達到原生繪製給Flutter端顯示效果。

在這裡插入圖片描述
如何實現這一過程? 因為在Flutter端的Texture中需要傳入textureId從而達到跟原生Surface繫結,所以第一步就是需要生成textureId,這裡原生主要採用自定義MethodChannel的方式:

TextureRegistry textureRegistry = registrar.textures();
TextureRegistry.SurfaceTextureEntry surfaceTextureEntry = textureRegistry.createSurfaceTexture();
long textureId = surfaceTextureEntry.id();
Map<String, Object> reply = new HashMap<>();
reply.put("textureId", textureId);
textureSurfaces.put(String.valueOf(textureId), surfaceTextureEntry);
result.success(reply);
複製程式碼

這裡的關鍵就是通過Flutter提供的SurfaceTextureEntry來獲取值,並通過Channel的方式傳遞給Flutter端,然後在Flutter端進行呼叫從而得到這個textureId的值:

  init() async {
    var response = await _channel.invokeMethod("load");
    _textureId = response["textureId"];
  }
複製程式碼

當我們得到了需要的textureId之後就可以初始化一個Texture物件了:

  Widget build(BuildContext context) {
    return Texture(textureId: _textureId);
  }
複製程式碼

上面完成了第一步,接下來就是如何複用原生的圖片下載功能了從而將得到圖片傳給Flutter端顯示。 在Android原生開發中基本上大家都在使用Fresco或者Glide來載入圖片(當然有自家的圖片庫),不過最終的目的都是得到圖片,然後通過textureId傳給Flutter端。我這裡直接展示圖片下載成功後的回撥,程式碼實現:

int textureId = call.argument("textureId");
final String url = call.argument("url");
int imageWidth = bitmap.getWidth();
int imageHeight = bitmap.getHeight();
TextureRegistry.SurfaceTextureEntry surfaceTextureEntry = textureSurfaces.get(String.valueOf(textureId));
Rect rect = new Rect(0, 0, 200, 200);
surfaceTextureEntry.surfaceTexture().setDefaultBufferSize(imageWidth, imageHeight);
Surface surface = new Surface(surfaceTextureEntry.surfaceTexture());
Canvas canvas = surface.lockCanvas(rect);
canvas.drawBitmap(bitmap, null, rect, null);
bitmap.recycle();
surface.unlockCanvasAndPost(canvas);
result.success(0);
複製程式碼

這裡的原生程式碼最關鍵的就是獲取Surface,有了它就可以獲取到Canvas自然就可以畫出想要的效果,Flutter端就可以顯示了。 而Flutter端呼叫的時候傳入相關的textureId以及圖片地址給原生,如:

    var params = Map();
    params["textureId"] = _textureId;
    params["url"] = url;
    result = await _channel.invokeMethod("start", params);
    value = result;
複製程式碼

整個demo差不多就這樣子結束了,執行起來看到的效果如下:

在這裡插入圖片描述
這樣子實現有什麼好處? 在完成了圖片下載功能後,自然要跟Flutter自帶的Image方式進行比較。首先在滑動的過程中兩者實現方式都差不多,閒魚的文章中對比了下記憶體優化了不少,我這裡也對比下記憶體: 自帶的Image的記憶體表現:
在這裡插入圖片描述
採用Texture的記憶體表現:
在這裡插入圖片描述
Flutter自帶的Image載入圖片的時候在AS中檢視Graphics的記憶體表現會飆升,我這裡一共載入20張圖片滑動到底部後相比確實差了不少。由於兩種實現方式的圖片存在於記憶體的位置不同,如果從總得記憶體佔有量來講Texture肯定表現得更加優秀點。

但是,當你在混合開發中一張圖片已經載入完成原生會直接複用,可能是從記憶體讀取也有可能是從sdcard讀取,不僅載入速度快而且也能為使用者省不少流量,閒魚文章也提到採用外接紋理的方式確實能有效的複用原生圖片,總得來講可以有效的解決了閒魚文章開頭的三個問題。

相關文章