跳至主要內容

Flutter 图片加载方案分析之 power_image

JI,XIAOYONG...大约 16 分钟

Flutter 默认提供了Image用于从网络、文件等加载图片,并且使用ImageCache统一管理图片缓存,但有时候并不能满足使用需求(比如网络图片没有磁盘缓存,导致每次 ImageCache 清除缓存之后又要从网络下载),所以又出现了flutter_cached_network_imageopen in new windowextended_imageopen in new window等基于 Flutter 原生的解决方案,以及power_imageopen in new window等基于混合开发的解决方案。

本文对 Alibaba 中的 power_image 加载过程、原理做一简单分析。

power_image

power_image是阿里巴巴出品的 Flutter 混合开发图片加载库,通过textureffi技术借助原生图片加载库加载图片、Flutter 端展示图片。

无论是 Flutter Imageopen in new window 组件,还是第三方的extende_imageopen in new windowflutter_cached_nework_image都是在 Flutter 端加载解析图片,这些方案对一般纯 Flutter 开发的 APP 来说基本可以满足要求,但是对于大多数混合开发的 APP 来说,这些方案会在 Flutter 和 Native 同时存在两份图片资源造成内存浪费,此外根据贝壳的分析open in new window,Flutter 端解决方案存在图片内存释放时机(Flutter 引擎持有的 SkImage 释放时机)以及超大图内存峰值等问题。

power_image能够较好的解决上述问题,其整体架构如下:

类结构图:
类结构图

架构图:
架构图

power_image可以大体划分为Flutter 端图片展示Native 图片加载两部分,下面分别分析。

Flutter 端图片展示

PowerImage

PowerImage继承自StatefulWidget,提供多种创建方式:既可以使用预设的PowerImage.networkPowerImage.file等构造函数从网络、文件等获取图片;也可以使用PowerImage.typePowerImage.options等自定义通道获取图片并展示;或者使用PowerImage()完全自定义。

除了PowerImage()构造函数之外,上述其余构造函数都根据传入的String? renderingType指定了 PowerImage 特定的PowerImageProvider image属性(是ffi还是texture)用于获取图片。

PowerImageState

class PowerImageState extends State<PowerImage> {
  
  Widget build(BuildContext context) {

    ImageErrorWidgetBuilder? errorWidgetBuilder = widget.errorBuilder;
    errorWidgetBuilder ??= (...) {
      return SizedBox(...);
    };

    if (widget.image.runtimeType == PowerTextureImageProvider) {
      return PowerTextureImage(
        provider: widget.image as PowerTextureImageProvider,
        ...);
    } else if (widget.image.runtimeType == PowerExternalImageProvider) {
      return PowerExternalImage(
        provider: widget.image as PowerExternalImageProvider,
        ...
      );
    }

    return ImageExt(
      image: widget.image,
      imageBuilder: widget.imageBuilder,
      ...
    );
  }
}

在 PowerImageState 的build()方法中,根据不同的PowerImageProvider image类型会返回不同的 Widget:

  • imagePowerTextureImageProvider 类型:采用 texture 模式展示图片,返回**PowerTextureImage,最终会返回经过封装的Texture**对象。
  • image是**PowerExternalImageProvider类型:采用 ffi 模式展示图片,返回PowerExternalImage,最终返回的是RawImage**对象,和使用 Flutter Image 展示图片的流程一致。
  • 其他类型,按照自定义的规则展示。

让我们来分别看一下**PowerTextureImagePowerExternalImage**的实现:

PowerTextureImage

class PowerTextureImage extends StatefulWidget {
  const PowerTextureImage({...}):super(key: key);

  final PowerTextureImageProvider provider;

  
  PowerTextureState createState() {
    return PowerTextureState();
  }
}

class PowerTextureState extends State<PowerTextureImage> {
  
  Widget build(BuildContext context) {
    return ImageExt(
      // 这里的 provider 实际上创建的是一个虚假的 ui.Image? dummy
      image: widget.provider,
      frameBuilder: widget.frameBuilder,
      errorBuilder: widget.errorBuilder,
      width: widget.width,
      height: widget.height,
      fit: widget.fit,
      alignment: widget.alignment,
      // 注意,这里会创建一个封装的 Texture,真正展示图片内容
      imageBuilder: buildImage,
      semanticLabel: widget.semanticLabel,
      excludeFromSemantics: widget.excludeFromSemantics,
    );
  }

  Widget buildImage(BuildContext context, ImageInfo? imageInfo) {
    if (imageInfo == null || imageInfo is! PowerTextureImageInfo) {
      return SizedBox(
        width: widget.width,
        height: widget.height,
      );
    }

    PowerTextureImageInfo textureImageInfo = imageInfo;
    return ClipRect(
      child: SizedBox(
        child: FittedBox(
          fit: widget.fit ?? BoxFit.contain,
          alignment: widget.alignment,
          child: SizedBox(
            width: textureImageInfo.width?.toDouble() ?? widget.width,
            height: textureImageInfo.height?.toDouble() ?? widget.height,
            child: Texture(
              // 注意,这里的 textureId 是从 provider 创建的 ImageInfo 中获取的
              textureId: textureImageInfo.textureId!,
            ),
          ),
        ),
        width: widget.width,
        height: widget.height,
      ),
    );
  }
}

PowerExternalImage

class PowerExternalImage extends StatefulWidget {
  const PowerExternalImage({...}):super(key: key);

  final PowerExternalImageProvider provider;

  
  PowerExteralState createState() => PowerExteralState();
}

class PowerExteralState extends State<PowerExternalImage> {
  
  Widget build(BuildContext context) {
    return ImageExt(
      frameBuilder: widget.frameBuilder,
      errorBuilder: widget.errorBuilder,
      // provider 会根据 Native 数据创建含有对应的 ui.Image 的
      // ImageInfo,展示对应图片
      image: widget.provider,
      width: widget.width,
      height: widget.height,
      fit: widget.fit,
      alignment: widget.alignment,
      semanticLabel: widget.semanticLabel,
      excludeFromSemantics: widget.excludeFromSemantics,
    );
  }
}

通过对比**PowerTextureImagePowerExternalImage**的源码可以发现,二者最终还是创建了 ImageExt 对象,只不过 PowerTextureImage 中 ImageExt.imageBuilder 返回了 Texture,而 PowerExternalImage 中 ImageExt.imageBuilder 为 null。

再根据下面的_ImageExtState.build源码可以确定,当使用 PowerTextureImage 时 PowerImage 创建的是封装了的 Texture,而 PowerExternalImage 时则会使用 PowerExternalImageProvider 创建的 ImageInfo 创建 RawImage,这实际上与 Flutter 原有的 Image 组件一致。

// _ImageExtState.build
  
  Widget build(BuildContext context) {
    ...

    Widget result;

    if (widget.imageBuilder != null) {
      result = widget.imageBuilder!(context, _imageInfo);
    } else {
      result = RawImage(
        image: _imageInfo?.image,
        ...
      );
    }

    ...

    return result;
  }

通过上面的分析,可以知道,PowerImage使用从PowerImageProvider 获取的 ImageInfo 来展示图片,采用**texture**方案时,使用 ImageInfo 中的 textureId 并返回 Texture 对象展示图片;而使用 ffi 方案时,会使用 ImageInfo 中的ui.Image image对象传入 RawIamge 展示图片(这部分与 Flutter Image 组件逻辑一致)。


PowerImageProvider

PowerImageProvider 继承自ImageProviderExt -> ImageProvider,是power_image的关键类之一,主要实现通过 Flutter/Native 跨端通信从 Native 获取/释放图片资源等,创建供 ImageExt 使用的 ImageInfo。

相对于 Flutter 官方的 ImageProvider,除了修改部分类为 power_image 对应的类之外,PowerImageProvider 主要有以下几点改变:

  • 工厂方法PowerImageProvider.options生产 PowerImageProvider:根据传入的 PowerImageRequestOptions 中PowerImageRequestOptions.renderingType的值,分别创建对应的**PowerExternalImageProvider或者PowerTextureImageProvider**。
  • 重写_loadAsync方法,调用 Native 图片库加载图片,并根据返回值调用子类 createImageInfo 方法创建 PowerImageInfo。

_loadAsync

通过重写_loadAsync方法,PowerImageProvider 实现了不同的子类分别创建 PowerImageInfo 展示图片、统一让 ImageCache 管理图片。

Future<ImageInfo> _loadAsync(
      PowerImageProvider key, DecoderCallback? decode) async {
    try {
      // 跨端通信获取图片资源,后面再详细分析
      PowerImageCompleter powerImageCompleter =
          PowerImageLoader.instance.loadImage(options);
      Map map = await powerImageCompleter.completer!.future;
      bool? success = map['success'];

      // remove multiFrame image cache On Last Listener Removed
      bool? isMultiFrame = map['_multiFrame'];
      if (isMultiFrame == true) {
        _completer!
          .addOnLastListenerRemovedCallback(() {
            scheduleMicrotask(() {
              PaintingBinding.instance!.imageCache!.evict(key);
            });
          });
      }
      _completer = null;

      if (success != true) {
        // The network may be only temporarily unavailable, or the file will be
        // added on the server later. Avoid having future calls to resolve
        // fail to check the network again.
        final PowerImageLoadException exception =
            PowerImageLoadException(nativeResult: map);
        PowerImageMonitor.instance().anErrorOccurred(exception);
        throw exception;
      }
      // 创建 ImageInfo
      return createImageInfo(map);
    } catch (e) {
      // Depending on where the exception was thrown, the image cache may not
      // have had a chance to track the key in the cache at all.
      // Schedule a microtask to give the cache a chance to add the key.
      scheduleMicrotask(() {
        PaintingBinding.instance!.imageCache!.evict(key);
      });
      rethrow;
    } finally {
      // chunkEvents.close();
    }
  }

在上面的分析中,我们得知,textureffi方案分别使用 ImageProvider 提供的 PowerImageInfo 中的int? textureIdui.Image image展示图片,让我们分别看一下他们是如何获取的:

PowerTextureImageProvider

class PowerTextureImageProvider extends PowerImageProvider {
  PowerTextureImageProvider(PowerImageRequestOptions options) : super(options);

  
  FutureOr<ImageInfo> createImageInfo(Map map) {
    int? textureId = map['textureId'];
    int? width = map['width'];
    int? height = map['height'];
    return PowerTextureImageInfo.create(
        textureId: textureId, width: width, height: height);
  }

  
  void dispose() {
    PowerImageLoader.instance.releaseImageRequest(options);
    super.dispose();
  }
}

class PowerTextureImageInfo extends PowerImageInfo {
	static ui.Image? dummy;
	final int? textureId;

	// 此方法使用一个通用的 ui.Image? dummy 创建 PowerTextureImageInfo
	// 以便让 ImageCache 能够管理 texture 创建的图片
	static FutureOr<PowerTextureImageInfo> create(
      {int? textureId, int? width, int? height}) async {
    if (dummy != null) {
      return PowerTextureImageInfo(
          textureId: textureId,
          width: width,
          height: height,
          image: dummy!.clone());
    }

    dummy = await _createImage(1, 1);
    return PowerTextureImageInfo(
        textureId: textureId,
        width: width,
        height: height,
        image: dummy!.clone());
  }
}

Future<ui.Image> _createImage(int width, int height) async {
  final Completer<ui.Image> completer = Completer<ui.Image>();
  ui.decodeImageFromPixels(// 使用指定的 Uint8List 创建 ui.Image
    Uint8List.fromList(
        List<int>.filled(width * height * 4, 0, growable: false)),
    width,
    height,
    ui.PixelFormat.rgba8888,
    (ui.Image image) {
      completer.complete(image);
    },
  );
  return completer.future;
}

PowerExternalImageProvider

class PowerExternalImageProvider extends PowerImageProvider {
  PowerExternalImageProvider(PowerImageRequestOptions options) : super(options);

  
  FutureOr<ImageInfo> createImageInfo(Map map) {
    Completer<ImageInfo> completer = Completer<ImageInfo>();
    int handle = map['handle'];
    int length = map['length'];
    int width = map['width'];
    int height = map['height'];
    int? rowBytes = map['rowBytes'];
    ui.PixelFormat pixelFormat =
        ui.PixelFormat.values[map['flutterPixelFormat'] ?? 0];
    // 获取图片在内存中的指针
    Pointer<Uint8> pointer = Pointer<Uint8>.fromAddress(handle);
    // 获取对应内存中的数据
    Uint8List pixels = pointer.asTypedList(length);
    // 根据内存中的数据创建 ui.Image,这里会发生内存拷贝,大图片会出现内存峰值偏高
    ui.decodeImageFromPixels(pixels, width, height, pixelFormat,
        (ui.Image image) {
      ImageInfo imageInfo = PowerImageInfo(image: image);
      completer.complete(imageInfo);
      //释放 platform_image
      PowerImageLoader.instance.releaseImageRequest(options);
    }, rowBytes: rowBytes);
    return completer.future;
  }
}

从上述代码可以看到:

  • texture 方案采用的 PowerTextureImageProvider 创建的 ImageInfo 对应的 ui.Image image 一个共享的占位符,并不能真正真正绘制内容,实际上图片信息在对应的PowerTextureImageInfo.textureId中;
  • ffi 方案创建的 ImageInfo 则根据 native 内存中的图片数据创建了对应的 ui.Image,与 Flutter 默认的 ImageProvider 提供的 ImageInfo 一样可以被 RawImage 正常使用。

这里需要注意,虽然 textureffi 都采用了 ImageCache 来管理图片缓存,甚至 ffi 的内存也在 Flutter 侧管理,但是 PowerImage 本身不会出现我们之前在Flutter Imageopen in new window中分析的加载大量高清网图会出现的内存爆炸,这是因为虽然在 ImageCache.putIfAbsent 方法中_pendingImages 同样保存了加载中的图片,但是实际这些图片加载过程中的内存由 Native 端图片加载库管理,而非 Flutter,所以只要 Native 端图片加载库比较成熟,就可以避免这个问题。

到目前为止,我们分析了 PowerImage 根据 PowerImageProvider 获取的 ImageInfo 分别采用 ffi 和 texture 两种方案展示图片的过程。


接下来分析一下之前提到的 PowerImageProvider._loadAsync 方法中使用 PowerImageLoader 获取图片的过程。整个过程可以分为 flutter 端发起请求/处理回调native 端接收请求/返回结果两部分,在这过程中 Flutter 和 Native 使用 MethodChannel(发送获取释放图片指令)和 EventChannel(接收图片成功加载的事件)进行通信。

Flutter/Native 通信

在上面分析PowerImageProvider._loadAsync方法时,我们注意到其中使用了**PowerImageLoader**获取图片信息 PowerImageCompleter:

// PowerImageProvider._loadAsync 省略部分代码
Future<ImageInfo> _loadAsync(
    PowerImageProvider key, DecoderCallback? decode) async {
    try {
        PowerImageCompleter powerImageCompleter =
          PowerImageLoader.instance.loadImage(options);
          Map map = await powerImageCompleter.completer!.future;
        }
    return createImageInfo(map);
}

这里是使用 PowerImageLoader 的单例加载图片,看一下具体的实现:

class PowerImageLoader {
    // 保存发起的图片请求
    static Map<String?, PowerImageCompleter> completers =
      <String?, PowerImageCompleter>{};

    PowerImageChannel channel = PowerImageChannel();
    static PowerImageLoader instance = PowerImageLoader._();

    PowerImageLoader._() {
        channel.impl = PowerImagePlatformChannel();
    }

    // 初始化 PowerImageChannel 等,需要在加载图片之前(比如 runApp 之前执行)
    void setup(PowerImageSetupOptions? options) {
        _globalRenderType = options?.globalRenderType ?? defaultGlobalRenderType;
        PowerImageMonitor.instance().errorCallback = options?.errorCallback;
        PowerImageMonitor.instance().errorCallbackSamplingRate = options?.errorCallbackSamplingRate;
        channel.setup();
  }

    PowerImageCompleter loadImage(PowerImageRequestOptions options,) {
        PowerImageRequest request = PowerImageRequest.create(options);// 创建 PowerImageRequest
        // 发起图片请求
        channel.startImageRequests(<PowerImageRequest>[request]);
        // 使用 completers 记录下刚刚发起的请求
        PowerImageCompleter completer = PowerImageCompleter();
        completer.request = request;
        completer.completer = Completer<Map>();
        completers[request.uniqueKey()] = completer;
        return completer;
    }

    // 当上面 loadImage 发起的图片加载完成之后,会调用此方法,从 completers 中取回对应的请求,调用完成
    void onImageComplete(Map<dynamic, dynamic> map) async {
        String? uniqueKey = map['uniqueKey'];
        PowerImageCompleter? completer = completers.remove(uniqueKey);
        //todo null case
        completer?.completer?.complete(map);
  }
}

首先创建了 PowerImageRequest 对象:

class PowerImageRequest {
  PowerImageRequest.create(PowerImageRequestOptions options)
      : imageWidth = options.imageWidth,
        imageHeight = options.imageHeight,
        imageType = options.imageType,
        renderingType = options.renderingType,
        src = options.src;
}

其中:

  • imageType表示获取图片的方式(比如networknativeAssetfileasset等);
  • renderingType表示图片渲染方式,比如external(即 ffi 方案)、texture

然后通过PowerImageChannel发送请求(实际的执行的类是PowerImagePlatformChannel):

class PowerImagePlatformChannel extends PowerImageChannelImpl {

  StreamSubscription? _subscription;

  PowerImagePlatformChannel() {
    eventHandlers['onReceiveImageEvent'] = (Map<dynamic, dynamic> event) {
        // 将 onReceiveImageEvent 放到 eventHandlers 中,
        // 上述 PowerImageLoader 发起的请求完成后会执行下述代码
        PowerImageLoader.instance.onImageComplete(event);
    };
  }

  
  void setup() {
    // 监听回调方法,监听 Native 端发送的图片加载结束事件
    startListening();
  }

  StreamSubscription? startListening() {
    _subscription ??= eventChannel.receiveBroadcastStream().listen(onEvent);
    return _subscription;
  }

  Map<String, EventHandler?> eventHandlers = <String, EventHandler?>{};

  // 处理 Native 端发送的事件
  void onEvent(dynamic val) {
    assert(val is Map<dynamic, dynamic>);
    final Map<dynamic, dynamic> event = val;
    String? eventName = event['eventName'];
    EventHandler? eventHandler = eventHandlers[eventName!];
    if (eventHandler != null) {
      eventHandler(event);
    } else {
      //TODO 发来了不认识的事件,需要处理一下
    }
  }

  void registerEventHandler(String eventName, EventHandler eventHandler) {
    assert(eventName.isNotEmpty);
    eventHandlers[eventName] = eventHandler;
  }

  void unregisterEventHandler(String eventName) {
    eventHandlers[eventName] = null;
  }

  
  final MethodChannel methodChannel = const MethodChannel('power_image/method');

  
  EventChannel eventChannel = const EventChannel('power_image/event');

  // 主动发送请求到 Native 端
  
  void startImageRequests(List<PowerImageRequest> requests) async {
    await methodChannel.invokeListMethod(
        'startImageRequests', encodeRequests(requests));
  }

  
  void releaseImageRequests(List<PowerImageRequest> requests) async {
    await methodChannel.invokeListMethod(
        'releaseImageRequests', encodeRequests(requests));
  }
}

小结一下:

  • 使用PowerImageLoader.setup注册 MethodChannel 和 EventChannel
  • 使用PowerImageLoader.loadImage向 Native 发起请求加载图片,将请求保存到PowerImageLoader.completers中并返回给调用者
  • 当 Native 端处理完请求之后会回调 PowerImagePlatformChannel 中注册的 EventChannel,然后会执行PowerImageLoader.instance.onImageComplete(event)方法,使用返回的图片信息,从PowerImageLoader.completers找出并完成之前的请求

以上分析为 Flutter 端向 Native 端发起请求的过程,下面以 Android 端为例分析一下 Native 端的处理过程:

首先是在 PowerImagePlugin 中向 Flutter 引擎注册对应的方法。

public class PowerImagePlugin implements FlutterPlugin, MethodCallHandler {

    @Override
    public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {
        if(sContext == null){
            sContext = flutterPluginBinding.getApplicationContext();
        }
        // 注册与 Flutter 端对应的方法
        methodChannel = new MethodChannel(
                flutterPluginBinding.getBinaryMessenger(), "power_image/method");
        methodChannel.setMethodCallHandler(this);
        eventChannel = new EventChannel(
                flutterPluginBinding.getBinaryMessenger(), "power_image/event");
        eventChannel.setStreamHandler(PowerImageEventSink.getInstance());
        PowerImageRequestManager.getInstance()
                .configWithTextureRegistry(flutterPluginBinding.getTextureRegistry());
        PowerImageDispatcher.getInstance().prepare();
    }
}

当 Flutter 端向 Native 发送消息时,Flutter 引擎会调用PowerImagePlugin.onMethodCall方法:

    // PowerImagePlugin.onMethodCall
    @Override
    public void onMethodCall(MethodCall call, Result result) {
        if ("startImageRequests".equals(call.method)) {
            if (call.arguments instanceof List) {
                List arguments = (List) call.arguments;
                // 将请求结果返回,只是根据传参创建请求并保存,
                // 将请求信息返回给 Flutter 端
                List results = PowerImageRequestManager.getInstance()
                        .configRequestsWithArguments(arguments);
                result.success(results);
                // 开始真正执行请求,找到上一步创建的请求 PowerImageBaseRequest
                // 并执行 PowerImageBaseRequest.startLoading
                PowerImageRequestManager.getInstance().startLoadingWithArguments(arguments);
            } else {
                throw new IllegalArgumentException("startImageRequests require List arguments");
            }
        } else if ("releaseImageRequests".equals(call.method)) {
            if (call.arguments instanceof List) {
                List arguments = (List) call.arguments;
                // 立即执行释放请求
                List results = PowerImageRequestManager.getInstance().releaseRequestsWithArguments(arguments);
                result.success(results);
            } else {
                throw new IllegalArgumentException("stopImageRequests require List arguments");
            }
        } else {
            result.notImplemented();
        }
    }

对于不同的调用请求:

  • startImageRequests:先根据请求参数创建好请求并返回给 Flutter 调用方;然后通过 PowerImageRequestManager 真正执行请求(最终会通过PowerImagePlugin.PowerImageEventSink.getInstance().sendImageStateEvent向 Flutter 通知结果)。
  • releaseImageRequests:立即从PowerImageRequestManager.requests中去除对应的请求并尝试终止任务,并向 Flutter 返回结果。

下面着重分析一下执行图片请求的逻辑(startImageRequests的情况):

public class PowerImageRequestManager {
  private Map<String, PowerImageBaseRequest> requests;
  private WeakReference<TextureRegistry> textureRegistryWrf;

  public List<Map<String, Object>> configRequestsWithArguments(List<Map<String, Object>> list) {
        List<Map<String, Object>> results = new ArrayList<>();
        if (list == null || list.isEmpty()) {
            return results;
        }
        for (int i = 0; i < list.size(); i++) {
            Map<String, Object> arguments = list.get(i);
            String renderType = (String) arguments.get("renderingType");
            PowerImageBaseRequest request;
            if (RENDER_TYPE_EXTERNAL.equals(renderType)) {// ffi 方案
                request = new PowerImageExternalRequest(arguments);
            } else if (RENDER_TYPE_TEXTURE.equals(renderType)) {// texture 方案
                request = new PowerImageTextureRequest(arguments, textureRegistryWrf.get());
            } else {
                continue;
            }
            // 保存创建的请求
            requests.put(request.requestId, request);
            boolean success = request.configTask();
            Map<String, Object> requestInfo = request.encode();
            requestInfo.put("success", success);
            results.add(requestInfo);
        }
        return results;
    }

    public void startLoadingWithArguments(List arguments) {
        if (arguments == null || arguments.isEmpty()) {
            return;
        }
        for (int i = 0; i < arguments.size(); i++) {
            Map arg = (Map) arguments.get(i);
            String requestId = (String) arg.get("uniqueKey");
            // 找出在 configRequestsWithArguments 方法中创建的请求并执行
            PowerImageBaseRequest request = requests.get(requestId);
            request.startLoading();
        }
    }
}

可见对于ffitexture方案,分别涉及到**PowerImageExternalRequestPowerImageTextureRequest两个类。他们都继承自PowerImageBaseRequest**类,其startLoading方法会调用performLoadImage方法:

public abstract class PowerImageBaseRequest {
    private void performLoadImage() {
        // 获取图片
        PowerImageLoader.getInstance().handleRequest(
                imageRequestConfig,
                new PowerImageLoaderProtocol.PowerImageResponse() {
                    
                    public void onResult(PowerImageResult result) {
                        // 加载到图片之后进行解析
                        PowerImageBaseRequest.this.onLoadResult(result);
                    }
                }
        );
    }
}

PowerImageBaseRequest.performLoadImage方法中:

  • 会先通过PowerImageLoader.getInstance().handleRequest方法获取图片;
  • 然后调用PowerImageBaseRequest.this.onLoadResult方法也就是PowerImageExternalRequestPowerImageTextureRequestonLoadResult()方法。

在他们的onLoadResult(final PowerImageResult result)方法中,入参 PowerImageResult 持有 FlutterImage 对象,后者持有加载的图片的 Drawable,他们根据各自的特点对图片进行处理后(ffi获取 Drawable 的 bitmap 对象,<>如果图片不是ARGB_8888发生一次 Bitmap 拷贝>;texture使用 Bitmap 绘制到 Canvas 上面),通过PowerImageBaseRequest.onLoadSuccess()方法或者PowerImageBaseRequest.onLoadFailed返回结果。

其中:

  • PowerImageExternalRequest 从获取到的图片生成 Bitmap 并返回其指针、宽高、大小等属性返回;
  • PowerImageTextureRequest 则将图片绘制到Surface中并返回textureId等信息。

而对于PowerImageLoader.getInstance().handleRequest(),这里面的各个 PowerImageLoaderProtocol 由 Native 端通过PowerImageLoader.getInstance().registerImageLoader注册具体的实现handleRequest()方法正是调用他们获取图片。

public class PowerImageLoader implements PowerImageLoaderProtocol {

    private final Map<String, PowerImageLoaderProtocol> imageLoaders;

    private PowerImageLoader() {
        imageLoaders = new HashMap<>();
    }

    private static class Holder {
        private final static PowerImageLoader instance = new PowerImageLoader();
    }

    public static PowerImageLoader getInstance() {
        return PowerImageLoader.Holder.instance;
    }

    // 在 Android 中调用此方法,注册获取"network"、"nativeAsset"、"asset"、"file"等图片的实现
    public void registerImageLoader(PowerImageLoaderProtocol loader, String imageType) {
        imageLoaders.put(imageType, loader);
    }

    // 此方法调用上面 registerImageLoader 方法注册的 ImageLoader 获取图片
    @Override
    public void handleRequest(PowerImageRequestConfig request, PowerImageResponse response) {
        PowerImageLoaderProtocol imageLoader = imageLoaders.get(request.imageType);
        if (imageLoader == null) {
            throw new IllegalStateException("PowerImageLoader for "
                    + request.imageType + " has not been registered.");
        }
        imageLoader.handleRequest(request, response);
    }
}

Native 端图片获取

上面提到,power_image默认的**PowerImageLoaderProtocol**有以下几种类:"network"、"nativeAsset"、"asset"、"file",这些都需要使用者在 Native 端注册才能正常使用。

以"network"为例,在MainActivity.onCreate方法中:

class MainActivity: FlutterActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
            ...
            PowerImageLoader.getInstance().registerImageLoader(
            PowerImageNetworkLoader(this.applic ationContext), "network"
        )
    }
}

PowerImageNetworkLoader继承自PowerImageLoaderProtocol,图片的加载逻辑在其handleRequest方法中:

class PowerImageNetworkLoader(private val context: Context) : PowerImageLoaderProtocol {
        override fun handleRequest(request: PowerImageRequestConfig, response: PowerImageResponse) {
        // 使用 Glide 加载图片 Drawable
        Glide.with(context).asDrawable().load(request.srcString())
            .listener(object : RequestListener<Drawable> {
                override fun onLoadFailed(e: GlideException?,model: Any,
                    target: Target<Drawable>,isFirstResource: Boolean): Boolean {
                    response.onResult(PowerImageResult.genFailRet("Native 加载失败:" + if (e != null) e.message else "null"))
                    return true
                }

                override fun onResourceReady(resource: Drawable,model: Any target:Target<Drawable>,dataSource: DataSource,isFirstResource:Boolean
                ): Boolean {
                    if (resource is GifDrawable) {
                        // 动图
                        // 加载成功,调用回调
                        response.onResult(
                            PowerImageResult.genSucRet(
                                GlideMultiFrameImage(
                                    resource as GifDrawable,
                                    false
                                )
                            )
                        )
                    } else {
                        if (resource is BitmapDrawable) {// 普通图片
                            response.onResult(
                                PowerImageResult.genSucRet(
                                    FlutterSingleFrameImage(
                                        resource as BitmapDrawable
                                    )
                                )
                            )
                        } else {
                            response.onResult(PowerImageResult.genFailRet("Native 加载失败:resource : $resource"))
                        }
                    }
                    return true
                }
            }).submit(
                if (request.width <= 0) Target.SIZE_ORIGINAL else request.width,
                if (request.height <= 0) Target.SIZE_ORIGINAL else request.height
            )
    }
}

这样,当 Flutter 端向 Native 发送消息时:

  • Flutter 引擎会调用PowerImagePlugin.onMethodCall方法,先创建对应的请求**PowerImageBaseRequest**;
  • 然后PowerImageRequestManager.getInstance().startLoadingWithArguments执行刚刚上一步创建的请求,此方法内部执行PowerImageBaseRequest.startLoading()方法;
  • 在**PowerImageBaseRequest类内部,其startLoading方法会调用performLoadImage方法,后者又会调用PowerImageLoader.getInstance().handleRequest()方法请求加载图片**,并指定回调方法为PowerImageBaseRequest.onLoadResult(result)
  • PowerImageLoader.handleRequest方法内部通过请求的imageType找到 Native 端(比如 Android 在MainActivity.onCreate中注册的)PowerImageLoaderProtocol imageLoader,并执行其handleRequest方法处理加载图片请求;
  • PowerImageLoaderProtocol.handleRequest()方法中调用原生的图片加载库获取 Drawable 并生成 PowerImageResult 回调PowerImageResponse.onResult方法,此方法会回调PowerImageBaseRequest.this.onLoadResult(result)
  • 在**PowerImageTextureRequest或者PowerImageExternalRequest**的onLoadResult方法中对获取到的PowerImageResult进行处理之后回调PowerImageBaseRequestonLoadSuccess()或者onLoadFailed(final String errMsg)方法返回图片请求结果。

总结

power_image 是一个利用原生库加载/管理图片的比较适用于 Flutter/Native 混合开发的图片加载库,提供了 textureffi 两种加载图片的方式。

其中,texture 方案实际使用 Texture 组件展示图片;而 ffi 方案则只有图片获取在 Native 端,当使用ui.decodeImageFromPixels方法从 Bitmap 内存指针创建ui.Image之后(根据阿里的描述open in new window,这里会发生一次内存拷贝,实际代码可以参考这里open in new window),剩下按照和 Flutter Image 类似的步骤展示图片。

根据官方的说法:

  1. Texture 适用于日常场景,优先选择;
  2. FFI 更适用于
    1. flutter <= 1.23.0-18.1.pre 版本中,在模拟器上显示图片
    2. 获取 ui.Image 图片数据
    3. flutter 侧解码,解码前的数据拷贝影响较小。

此外,根据官方power_image/issues/17open in new window的说法,“在 2.5.3 上 ffi 性能已经跟  texture不相上下了”,而且 textrue 方案在 Android 上较大尺寸可能会 crash(flutter/flutter#92397open in new window),所以更推荐使用 ffi 方案

参考资料

https://github.com/alibaba/power_imageopen in new window

Flutter 图片库高燃新登场open in new window

闲鱼 Flutter 图片框架架构演进(超详细)open in new window

Flutter 图片内存优化实践open in new window

https://github.com/alibaba/power_image/issues/17open in new window

ui.decodeImageFromPixels分析open in new window

文章标题:《Flutter 图片加载方案分析之 power_image》
本文作者: JI,XIAOYONG
发布时间: 2022/08/02 15:54:39 UTC+8
更新时间: 2023/12/30 16:17:02 UTC+8
written by human, not by AI
本文地址: https://jixiaoyong.github.io/blog/posts/727d7800.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。转载请注明出处!
你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.8