Flutter加載圖片與Glide

相對於Android而言。在Flutter中,加載網絡圖片,是很方便的一件事。經過Flutter提供的API就能夠來實現。以下。web

Image.network("https://xxxxx");
複製代碼

但使用後,很快就會發現一些問題,主要有如下幾點。json

  1. Flutter加載網絡圖片的API僅會將圖片緩存在內存中,沒法緩存本地。當內存中圖片不存在時,又須要從新進行網絡請求,這樣一來就比較耗費資源。
  2. 若是在已有項目中添加Flutter模塊,那麼經過上面API就沒法複用Android已有且成熟的網絡圖片處理模塊。
  3. 若是是混合開發項目,那麼針對同一張圖片,沒法作到Flutter模塊與Android的內存間共享。

針對上述問題,目前已經存在一些解決方案。如經過cached_network_image來解決圖片緩存本地問題;經過外接texture來實現同一張圖片在Flutter模塊與Android的內存間共享(可參考閒魚Flutter圖片框架架構演進(超詳細)一文)。緩存

而本文主要就是介紹經過Android已有的網絡圖片加載模塊來實現Flutter中的網絡圖片加載。該方案能夠複用Android中現有的圖片處理模塊及將圖片緩存在本地,而且圖片在本地僅保存一次。但要注意的是,該方案沒法實現同一張圖片在Flutter模塊與Android的內存間共享。網絡

因爲在Android開發中,經過Glide來加載網絡圖片比較廣泛。因此本文也就以Glide爲例。架構

一、網絡圖片的加載

總體實現方案很簡單,就是經過Glide來下載圖片,待下載成功後經過Platform Channel將圖片路徑傳遞給Flutter,最後再經過圖片路徑來加載。這樣圖片在本地僅會保存一次。框架

先來看Flutter端代碼的實現。less

class ImageWidget extends StatefulWidget {
  final String url;
  final double width;
  final double height;

  const ImageWidget({Key key, @required this.url, this.width, this.height})
      : super(key: key);

  @override
  State<StatefulWidget> createState() => ImageWidgetState(url, width, height);
}

class ImageWidgetState extends State<ImageWidget> {
  final String url;//圖片網絡路徑
  final double width;//widget的寬
  final double height;//widget的高

  String _imagePath;//圖片的本地路徑

  bool _visible = false;

  int _cacheWidth;//緩存中圖片的寬
  int _cacheHeight;//緩存中圖片的高

  ImageWidgetState(this.url, this.width, this.height);

  @override
  void initState() {
    super.initState();
    _getImage();
  }

  //從Native獲取圖片的本地路徑
  void _getImage() {
    //從Native獲取圖片路徑
    ChannelManager.instance.getImage(
        url, width * window.devicePixelRatio, height * window.devicePixelRatio,
        (data) {
      if (data == null || data == "") return;
      Map<String, dynamic> imageData = json.decode(data);
      _updateImageInfo(imageData);
      // 將圖片路徑存入內存中
      ImageInfoManager.instance.addImageInfo(url, imageData);
    });
  }

  _updateImageInfo(Map<String, dynamic> imageData) {
    setState(() {
      _visible = true;
      _cacheWidth = imageData['cacheWidth'];
      _cacheHeight = imageData['cacheHeight'];
      _imagePath = imageData['url'];
      print("_imagePath:$_imagePath");
    });
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedOpacity(//淡入淡出動畫
      opacity: _visible ? 1.0 : 0.0,
      duration: Duration(milliseconds: 500),
      child: _imagePath == null
          // 網絡圖片加載前的默認圖片
          ? Container(
              width: width,
              height: height,
              color: Colors.transparent,
            )
          : Image.file(//根據圖片路徑來加載圖片
              File(_imagePath),
              width: width,
              height: height,
              cacheHeight: _cacheHeight,
              cacheWidth: _cacheWidth,
            ),
    );
  }
}

複製代碼

再來看Android端代碼的實現。異步

public class DDMethodChannel implements MethodChannel.MethodCallHandler{
    private static final String TAG = "DDMethodChannel";

    private final Handler mainHandler = new Handler(Looper.getMainLooper());
    private MethodChannel channel;

    public static DDMethodChannel registerWith(BinaryMessenger messenger) {
        MethodChannel channel = new MethodChannel(messenger, "native_http");
        DDMethodChannel ddMethodChannel = new DDMethodChannel(channel);
        channel.setMethodCallHandler(ddMethodChannel);
        return ddMethodChannel;
    }

    private DDMethodChannel(MethodChannel channel) {
        this.channel = channel;

    }

    @Override
    public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
        Map<String, String> args = (Map<String, String>) call.arguments;
        String url = args.get("url");
        switch (call.method) {
            case "getImage":
                double width = TextUtils.isEmpty(args.get("width")) ? 0.0 : Double.parseDouble(args.get("width"));
                double height = TextUtils.isEmpty(args.get("height")) ? 0.0 : Double.parseDouble(args.get("height"));
                Log.i(TAG, "url:" + url + ",width:" + width + ",height:" + height);
                //Glide下載圖片
                Glide.with(Constants.getAppContext())
                        .downloadOnly()//僅下載
                        .load(url)
                        .override((int) width, (int) height)
                        .skipMemoryCache(true)//因爲僅下載圖片,因此能夠跳過內存緩存
                        .dontAnimate()//因爲僅下載圖片,因此能夠取消動畫
                        .listener(new RequestListener<File>() {//監聽圖片是否下載完畢
                            @Override
                            public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
                                Log.i(TAG, "image下載失敗,error:" + e.getMessage());
                                //必須切換回主線程,不然報錯
                                mainHandler.post(new Runnable() {
                                    @Override
                                    public void run() {
                                        if (result != null) {
                                            result.error(-1 + "", e.getMessage(), "");
                                        }
                                    }
                                });

                                return false;
                            }

                            @Override
                            public boolean onResourceReady(File resource, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
                                String data = "";
                                //圖片下載成功,經過一個json將路徑傳遞給Flutter
                                JSONObject object = new JSONObject();
                                try {
                                    object.put("url", resource.getAbsolutePath());
                                    object.put("cacheWidth", outWidth);
                                    object.put("cacheHeight", outHeight);
                                    data = object.toString();
                                } catch (JSONException e) {
// e.printStackTrace();
                                    Log.i(TAG, "error:" + e.getMessage());
                                }

                                String finalData = data;
                                //必須切換回主線程,不然報錯
                                mainHandler.post(new Runnable() {
                                    @Override
                                    public void run() {
                                        MethodChannel.Result result = resultMap.remove(url);
                                        if (result != null) {
                                            result.success(finalData);
                                        }
                                    }
                                });
                                return false;
                            }
                        }).submit();
                break;
        }
    }
}
複製代碼

通過上面代碼,就實現了Flutter經過Glide來加載網絡圖片。ide

上面代碼中省略了Platform Channel使用的代碼,但若是對於Platform Channel的使用不熟悉,能夠參考Flutter與Android間通訊一文。oop

二、圖片內存佔用優化

再來看上面代碼中使用的cacheWidthcacheHeight字段,它們在文檔中的說明以下。

If [cacheWidth] or [cacheHeight] are provided, it indicates to the engine that the image must be decoded at the specified size. The image will be rendered to the constraints of the layout or [width] and [height] regardless of these parameters. These parameters are primarily intended to reduce the memory usage of [ImageCache].

簡單翻譯下,cacheWidthcacheHeight是圖片在內存緩存中的寬與高,設置該值能夠減少圖片在內存中的佔用。所以咱們能夠根據widget的寬高與圖片的實際寬高來進行縮放,從而減少圖片在內存中的佔用。

所以,咱們就能夠根據cacheWidthcacheHeight來優化上面代碼。

@Override
    public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
        Map<String, String> args = (Map<String, String>) call.arguments;
        String url = args.get("url");
        switch (call.method) {
            case "getImage":
                double width = TextUtils.isEmpty(args.get("width")) ? 0.0 : Double.parseDouble(args.get("width"));
                double height = TextUtils.isEmpty(args.get("height")) ? 0.0 : Double.parseDouble(args.get("height"));
                Log.i(TAG, "url:" + url + ",width:" + width + ",height:" + height);
                Glide.with(Constants.getAppContext())
                        .downloadOnly()
                        .load(url)
                        .override((int) width, (int) height)
                        .skipMemoryCache(true)
                        .dontAnimate()
                        .listener(new RequestListener<File>() {
                            @Override
                            public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {...}

                            @Override
                            public boolean onResourceReady(File resource, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
                                Log.i(TAG, "image下載成功,path:" + resource.getAbsolutePath());
                                BitmapFactory.Options options = new BitmapFactory.Options();
                                options.inJustDecodeBounds = true;//這個參數設置爲true纔有效,
                                Bitmap bmp = BitmapFactory.decodeFile(resource.getAbsolutePath(), options);//這裏的bitmap是個空
                                if (bmp == null) {
                                    Log.e(TAG, "經過options獲取到的bitmap爲空 ===");
                                }
                                //獲取圖片的真實高度
                                int outHeight = options.outHeight;
                                //獲取圖片的真實寬度
                                int outWidth = options.outWidth;
                                //計算寬高的縮放比例
                                int inSampleSize = calculateInSampleSize(outWidth, outHeight, (int) width, (int) height);
                                Log.i(TAG, "outWidth:" + outWidth + ",outHeight:" + outHeight + ",inSampleSize:" + inSampleSize);
                                String data = "";
                                JSONObject object = new JSONObject();
                                try {
                                    object.put("url", resource.getAbsolutePath());
                                    //縮放後的cacheWidth
                                    object.put("cacheWidth", outWidth / inSampleSize);
                                    //縮放後的cacheHeight
                                    object.put("cacheHeight", outHeight / inSampleSize);
                                    data = object.toString();
                                } catch (JSONException e) {
// e.printStackTrace();
                                    Log.i(TAG, "error:" + e.getMessage());
                                }

                                String finalData = data;
                                mainHandler.post(new Runnable() {
                                    @Override
                                    public void run() {
                                        if (result != null) {
                                            result.success(finalData);
                                        }
                                    }
                                });
                                return false;
                            }
                        }).submit();
                break;
        }
    }
    
    //獲取圖片的縮放比
    private int calculateInSampleSize(int outWidth, int outHeight, int reqWidth, int reqHeight) {
        int inSampleSize = 1;
        if (outWidth > reqWidth || outHeight > reqHeight) {
            int halfWidth = outWidth / 2;
            int halfHeight = outHeight / 2;
            while ((halfWidth / inSampleSize) >= reqWidth && (halfHeight / inSampleSize) >= reqHeight) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
複製代碼

通過上面代碼的優化,Flutter經過Glide來加載網絡圖片基本上就沒啥大問題了。

三、列表加載圖片優化

再來看一個很是常見的應用場景,列表中加載網絡圖片。在Android中,Glide針對列表有專門的優化,在快速滑動時,不會進行圖片的加載。那麼這在Flutter中該怎麼實現尼?

其實在Flutter中已經幫咱們作了關於快速滑動時的處理,下面來看Image組件的實現代碼。

class _ImageState extends State<Image> with WidgetsBindingObserver {
  ...

  void _resolveImage() {
    //快速滑動時的處理
    final ScrollAwareImageProvider provider = ScrollAwareImageProvider<dynamic>(
      context: _scrollAwareContext,
      imageProvider: widget.image,
    );
    final ImageStream newStream =
      provider.resolve(createLocalImageConfiguration(
        context,
        size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null,
      ));
    assert(newStream != null);
    _updateSourceStream(newStream);
  }

  ...

  @override
  Widget build(BuildContext context) {...}

  ...
}

複製代碼

上面代碼中的ScrollAwareImageProvider就是Image在快速滑時的處理,再來看該類的實現。

@optionalTypeArgs
class ScrollAwareImageProvider<T> extends ImageProvider<T> {
  const ScrollAwareImageProvider({
    @required this.context,
    @required this.imageProvider,
  });

  @override
  void resolveStreamForKey(
    ImageConfiguration configuration,
    ImageStream stream,
    T key,
    ImageErrorListener handleError,
  ) {
    
    ...
    //檢測當前是否在快速滑動
    if (Scrollable.recommendDeferredLoadingForContext(context.context)) {
        SchedulerBinding.instance.scheduleFrameCallback((_) {
          //添加到微任務
          scheduleMicrotask(() => resolveStreamForKey(configuration, stream, key, handleError));
        });
        return;
    }
    //正常加載圖片
    imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
  }

  ...
}
複製代碼

上面代碼很簡單,重點就是判斷當前是否在快速滑動。若是在快速滑動就等待下一幀,不然就將圖片展現在界面上。

因爲Flutter在快速滑動時作了處理,因此基本上不須要再次進行優化,就能夠把上面的圖片加載方案使用在列表中。但在使用時,仍是發現了存在的一個小問題,就是當快速滑動時,每幀的繪製時間會超過16ms。通過仔細排查,主要是因爲快速滑動時,每一個item的淡入淡出動畫還須要執行,從而致使了每幀繪製時間的延長。因此須要列表快速滑動時取消item的淡入淡出動畫。具體實現代碼以下。

void _getImage() {
    //因爲Platform Channel是異步的,因此經過Platform Channel來獲取路徑會產生淡入淡出動畫。這裏從內存中獲取圖片路徑,能夠取消在快速滑動時的淡入淡出動畫,也能夠減小Flutter與Native間的交互。
    Map<String, dynamic> imageInfo =
        ImageInfoManager.instance.getImageInfo(url);
    if (imageInfo != null) {
      print("直接從Map中獲取路徑");
      _visible = true;
      _updateImageInfo(imageInfo);
      return;
    }

    //判斷列表是否在快速滑動
    if (Scrollable.recommendDeferredLoadingForContext(context)) {
      SchedulerBinding.instance.scheduleFrameCallback((_) {
        scheduleMicrotask(() => _getImage());
      });
      return;
    }

    //從Native獲取圖片路徑(目前僅支持Android平臺)
    ChannelManager.instance.getImage(
        url, width * window.devicePixelRatio, height * window.devicePixelRatio,
        (data) {
      if (data == null || data == "") return;
      Map<String, dynamic> imageData = json.decode(data);
      _updateImageInfo(imageData);
      // 將圖片路徑存入內存中
      ImageInfoManager.instance.addImageInfo(url, imageData);
    });
  }
複製代碼

四、總結

前面兩小結中優化事後的代碼就是本文方案的最終實現,作到了混合項目中複用已有的圖片加載模塊及圖片僅在本地保存一次。但仍是沒法作到圖片在Flutter與Native間的內存共享,也沒法作到圖片在多Engine的內存間共享,而關於閒魚經過外接texture方案來實現圖片的內存間共享有必定實現複雜度,因此這種實現方案待後面再來分享。

此外,FlutterImage組件能夠很方便的加載gif與webp,因此上述方案的實現也是可以加載gif與webp。

相關文章
相關標籤/搜索