1. 创业也首页
  2. 网赚项目

节日献礼:Flutter图片库重磅开源!

背景

去年,闲鱼技术团队新一代图片库 PowerImage 在经过一系列灰度、问题修复、代码调优后,已全量稳定应用于闲鱼。相对于上一代 IFImage,PowerImage 经过进一步的演进,适应了更多的业务场景与最新的 flutter 特性,解决了一系列痛点:比如,因为完全抛弃了原生的 ImageCache,在与原生图片混用的场景下,会让一些低频的图片反而占用了缓存;比如,我们在模拟器上无法展示图片;比如我们在相册中,需要在图片库之外再搭建图片通道。

简介

PowerImage 是一个充分利用 native 原生图片库能力、高扩展性的flutter图片库。我们巧妙地将外接纹理与 ffi 方案组合,以更贴近原生的设计,解决了一系列业务痛点。

能力特点

  • 支持加载 ui.Image 能力。在基于外接纹理的方案中,使用方无法拿到真正的 ui.Image 去使用,这导致图片库在这种特殊的使用场景下无能为力。
  • 支持图片预加载能力。正如原生precacheImage一样。这在某些对图片展示速度要求较高的场景下非常有用。
  • 新增纹理缓存,与原生图片库缓存打通!统一图片缓存,避免原生图片混用带来的内存问题。
  • 支持模拟器。在 flutter-1.23.0-18.1.pre之前的版本,模拟器无法展示 Texture Widget。
  • 完善自定义图片类型通道。解决业务自定义图片获取诉求。
  • 完善的异常捕获与收集。
  • 支持动图。(来自淘特的PR)

Flutter 原生方案

在介绍新方案开始之前,先简单回忆一下 flutter 原生图片方案。

节日献礼:Flutter图片库重磅开源!

原生 Image Widget 先通过 ImageProvider 得到 ImageStream,通过监听它的状态,进行各种状态的展示。比如frameBuilder、loadingBuilder,最终在图片加载成功后,会 rebuild出 RawImage,RawImage会通过 RenderImage来绘制,整个绘制的核心是 ImageInfo中的 ui.Image。

  • • Image:负责图片加载的各个状态的展示,如加载中、失败、加载成功展示图片等。
  • • ImageProvider:负责 ImageStream 的获取,比如系统内置的 NetworkImage、AssetImage 等。
  • • ImageStream:图片资源加载的对象。

在梳理 flutter 原生图片方案之后,我们发现是不是有机会在某个环节将 flutter 图片和 native 以原生的方式打通?

新一代方案

我们巧妙地将 FFi 方案与外接纹理方案组合,解决了一系列业务痛点。

FFI

正如开头说的那些问题,Texture 方案有些做不到的事情,这需要其他方案来互补,这其中核心需要的就是 ui.Image。我们把 native 内存地址、长度等信息传递给 flutter 侧,用于生成 ui.Image。

首先 native 侧先获取必要的参数(以 iOS 为例):

_rowBytes = CGImageGetBytesPerRow(cgImage);

CGDataProviderRef dataProvider = CGImageGetDataProvider(cgImage);

CFDataRef rawDataRef = CGDataProviderCopyData(dataProvider);

_handle = (long)CFDataGetBytePtr(rawDataRef);

NSData *data = CFBridgingRelease(rawDataRef);

self.data = data;

_length = data.length;

dart 侧拿到后

@override

FutureOrcreateImageInfo(Map map) {

Completercompleter = Completer();

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 ?? 0];

Pointerpointer = Pointer.fromAddress(handle);

Uint8List pixels = pointer.asTypedList(length);

ui.decodeImageFromPixels(pixels, width, height, pixelFormat,

(ui.Image image) {

ImageInfo imageInfo = ImageInfo(image: image);

completer.complete(imageInfo);

//释放 native 内存

PowerImageLoader.instance.releaseImageRequest(options);

}, rowBytes: rowBytes);

return completer.future;

}

我们可以通过 ffi 拿到 native 内存,从而生成 ui.Image。这里有个问题,虽然通过 ffi 能直接获取 native 内存,但是由于 decodeImageFromPixels会有内存拷贝,在拷贝解码后的图片数据时,内存峰值会更加严重。

这里有两个优化方向:

  1. 1. 解码前的图片数据给 flutter,由 flutter 提供的解码器解码,从而削减内存拷贝峰值。
  2. 2. 与 flutter 官方讨论,尝试从内部减少这次内存拷贝。

FFI 这种方式适合轻度使用、特殊场景使用,支持这种方式可以解决无法获取 ui.Image 的问题,也可以在模拟器上展示图片(flutter <= 1.23.0-18.1.pre),并且图片缓存将完全交给 ImageCache 管理。

Texture

Texture 方案与原生结合有一些难度,这里涉及到没有 ui.Image只有 textureId。这里有几个问题需要解决:

问题一:Image Widget 需要 ui.Image去 build RawImage从而绘制,这在本文前面的Flutter 原生方案介绍中也提到了;问题二:ImageCache 依赖 ImageInfo 中 ui.Image的宽高进行 cache 大小计算以及缓存前的校验;问题三:native 侧 texture 生命周期管理。分别都有解决方案:

问题一:通过自定义 Image 解决,透出 imageBuilder 来让外部自定义图片 widget

问题二:为 Texture 自定义 ui.image,如下:

import'dart:typed_data';

import'dart:ui'as ui show Image;

import'dart:ui';

classTextureImageimplementsui.Image{

int_width;

int_height;

int textureId;

TextureImage(this.textureId, int width, int height)

: _width = width,

_height = height;

@override

void dispose() {

// TODO: implement dispose

}

@override

intget height =>_height;

@override

FuturetoByteData(

{ImageByteFormat format = ImageByteFormat.rawRgba}) {

// TODO: implement toByteData

throw UnimplementedError();

}

@override

intget width =>_width;

}

这样的话,TextureImage 实际上就是个壳,仅仅用来计算 cache 大小。实际上,ImageCache 计算大小,完全没必要直接接触到 ui.Image,可以直接找 ImageInfo 取,这样的话就没有这个问题了。

问题三:关于 native 侧感知 flutter image 释放时机的问题。

修改的 ImageCache 释放如下(部分代码):

typedefvoid HasRemovedCallback(dynamic key, dynamic value);

classRemoveAwareMap<K, V>implementsMap<K, V>{

HasRemovedCallback hasRemovedCallback;

...

}

//------

final RemoveAwareMap<Object, _PendingImage>_pendingImages = RemoveAwareMap<Object, _PendingImage>();

//------

void hasImageRemovedCallback(dynamic key, dynamic value) {

if(key is ImageProviderExt) {

waitingToBeCheckedKeys.add(key);

}

if(isScheduledImageStatusCheck) return;

isScheduledImageStatusCheck = true;

//We should do check in MicroTask to avoid if image is remove and add right away

scheduleMicrotask(() {

waitingToBeCheckedKeys.forEach((key) {

if(!_pendingImages.containsKey(key) &&

!_cache.containsKey(key) &&

!_liveImages.containsKey(key)) {

if(key is ImageProviderExt) {

key.dispose();

}

}

});

waitingToBeCheckedKeys.clear();

isScheduledImageStatusCheck = false;

});

}整体架构

我们将两种解决方案非常优雅地结合在了一起:

节日献礼:Flutter图片库重磅开源!

我们抽象出了 PowerImageProvider ,对于 external(ffi)、texture,分别生产自己的 ImageInfo 即可。它将通过对 PowerImageLoader 的调用,提供统一的加载与释放能力。

蓝色实线的 ImageExt 即为自定义的 Image Widget,为 texture 方式透出了 imageBuilder。

蓝色虚线 ImageCacheExt 即为 ImageCache 的扩展,仅在 flutter < 2.2.0 版本才需要,它将提供 ImageCache 释放时机的回调。

这次,我们也设计了超强的扩展能力。除了支持网络图、本地图、flutter 资源、native 资源外,我们提供了自定义图片类型的通道,flutter 可以传递任何自定义的参数组合给 native,只要 native 注册对应类型 loader,比如「相册」这种场景,使用方可以自定义 imageType 为 album ,native 使用自己的逻辑进行加载图片。有了这个自定义通道,甚至图片滤镜都可以使用 PowerImage 进行展示刷新。

除了图片类型的扩展,渲染类型也可进行自定义。比如在上面 ffi 中说的,为了降低内存拷贝带来的峰值问题,使用方可以在 flutter 侧进行解码,当然这需要 native 图片库提供解码前的数据。

数据

FFI vs Texture

节日献礼:Flutter图片库重磅开源!

机型:iPhone 11 Pro;图片:300 张网络图;行为:在listView中手动滚动到底部再滚动到顶部;

native Cache:20 maxMemoryCount; flutter Cache:30MB

flutter version 2.5.3; release 模式下

这里有两个现象:

FFI: 186MB波动

Texture:194MB波动

在 2.5.3 版本中,Texture 方案与 FFI,在内存水位上差异不大,内存波动上面与 flutter 1.22 结论相反。

图中棋格图,为打开 checkerboardRasterCacheImages后所展示,可以看出,ffi方案会缓存整个cell,而texture方案,只有cell中的文字被缓存,RasterCache 会使得 ffi 在流畅度方面会有一定优势。

滚动流畅性分析

节日献礼:Flutter图片库重磅开源!

设备: Android OnePlus 8t,CPU和GPU进行了锁频。

case: GridView每行4张图片,300张图片,从上往下,再从下往上,滑动幅度从500,1000,1500,2000,2500,5轮滑动。重复20次。

方式: for i in {1..20}; do flutter drive --target=test_driver/app.dart --profile; done 跑数据,获取TimeLine数据并分析。

结论:

  • • UI thread 耗时 texture 方式最好,PowerImage 略好于 IFImage,FFI方式波动比较大。
  • • Raster thread 耗时 PowerImage 好于 IFImage。Origin 原生方式好是因为对图片 resize了,其他方式加载的是原图。

更精简的代码

节日献礼:Flutter图片库重磅开源!

dart 侧代码有较大幅度的减少,这归功于技术方案贴合 flutter 原生设计,我们与原生图片共用较多代码。

FFI 方案补全了外接纹理的不足,遵循原生 Image 的设计规范,不仅让我们享受到 ImageCache 带来的统一管理,也带来了更精简的代码。

单测

节日献礼:Flutter图片库重磅开源!

为了保证核心代码的稳定性,我们有着较为完善的单测,行覆盖率接近95%。

关于开源

我们期待通过社区的力量让 PowerImage 更加完善与强大,也希望 PowerImage 能为大家在工程研发中带来收益。

Issues

关于 issue,我们希望大家在使用 PowerImage 遇到问题与诉求时,积极交流,提出 issue 时尽可能提供详细的信息,以减少沟通成本。在提出 issue 前,请确保已阅读 readme。

节日献礼:Flutter图片库重磅开源!

对于 bug 的 issue,我们自定义了模板(Bug report),可以方便地填一些必要的信息。其他类型则可以选择 Open a blank issue。

我们每周会花部分时间统一处理 issues,也期待大家的讨论与 PR。

PR

为了保持 PowerImage 核心功能的稳定性,我们有着完善的单测,行覆盖率达到了 95%(power_image库)。

在提交PR时,请确保所提交的代码被单测覆盖到,并且涉及到的单测代码请同时提交。

节日献礼:Flutter图片库重磅开源!

得益于 Github 的 Actions 能力,我们在主分支 push 代码、对主分支进行 PR 操作时,都会触发 flutter test任务,只有单测通过才可合入。

未来

开源是 PowerImage 的开始,而不是结束,PowerImage 可做的事情还有很多,有趣而丰富。比如第一个 issue 中描述的 loadingBuilder如何实现?比如 ffi 方案如何支持动图?再比如Kotlin和Swift···

PowerImage 未来将持续演进,在当前 texture 方案与 ffi 方案共存的情况下,伴随着 flutter 本身的迭代,我们将更倾向于向 ffi 发展,正如在上文的对比中, ffi 方案可以天然享用 raster cache 所带来的流畅度的优势。

PowerImage 也会持续追随 flutter 的脚步,以始终贴合原生的设计理念,不断进步,我们希望更多的同学加入进来,共同成长。

本站(www.chuangyeye.com)部分图文转自网络,刊登本文仅为传播信息之用,绝不代表赞同其观点或担保其真实性。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与本网联系,我们将及时更正、删除,谢谢