背景:去年,闲鱼技术团队的新一代图片库PowerImage经过一系列的灰度、问题修复、代码优化,已经稳定应用于闲鱼。相比上一代IFImage,PowerImage进行了进一步的进化,适应了更多的业务场景和最新的颤振特性,解决了一系列痛点:比如因为完全抛弃了原生ImageCache,在与原生图像混合的场景中,一些低频图像反而会占用缓存;比如我们不能在模拟器上显示图片;比如我们在相册的时候,需要在图片库之外建立一个图片通道。
简介PowerImage是一个充分利用原生图像库,具有高扩展性的flutter图像库。我们巧妙地将外部纹理与ffi方案相结合,更加贴近原生设计,解决了一系列业务痛点。
特性支持加载ui.Image的能力,在基于外部纹理的方案中,用户无法获得真实的ui。图片来使用,这让图片库在这种特殊的使用场景下很无奈。
支持图像预加载能力。就像原生的前化学图像一样。这在一些画面显示速度较高的场景中非常有用。
添加纹理缓存,与原生图像库缓存打通!统一的图像缓存,以避免因混合原生图像而导致的内存问题。
支持模拟器。在flutter-1.23.0-18.1.pre之前,模拟器无法显示纹理小部件。
改进自定义图像类型通道。解决业务自定义图片获取请求。
完美的异常捕获和收集。
支持动画。(公关自淘特)
颤振原生方案在介绍新方案之前,简单回忆一下颤振原生图片方案。
原生图像小部件首先通过ImageProvider获取ImageStream,通过监控其状态来显示各种状态。例如,frameBuilder和loadingBuilder最终会在图像加载成功后重新构建RawImage,而RawImage将由RenderImage绘制。整个图的核心是ui。ImageInfo中的图像。
Image:负责显示图像加载的各种状态,如加载、失败、加载成功等。
ImageProvider:负责获取ImageStream,比如系统内置的NetworkImage和AssetImage。
ImageStream:图像资源加载的对象。
梳理了一下flutter的原生图片方案,发现在某个时候有机会以原生方式获取flutter图片和原生通过?
在新一代中,我们巧妙地将FFi方案与外部纹理方案相结合,解决了一系列业务痛点。
FFI如开头所说,有些事情是纹理方案做不到的,需要其他方案来补充,而其中的核心就是ui。Image我们将原生内存地址、长度等信息传递给flutter,用于生成ui。图像
首先原生端先获取必要的参数(以iOS为例):
_ row bytes=CGImageGetBytesPerRow(CG image);
CGDataProviderRef data provider=cgimagegetdata provider(CG image);
CFDataRef rawDataRef=CGDataProviderCopyData(data provider);
_ handle=(long)CFDataGetBytePtr(rawDataRef);
ns data * data=CFBridgingRelease(rawDataRef);
self.data=数据;
_长度=data.lengthdart得到它后,
@覆盖
FutureOrImageInfo createImageInfo(地图地图){
CompleterImageInfo completer=CompleterImageInfo();
int handle=map 'handle
int length=map 'length
int width=map 'width
int height=map 'height
int row bytes=map“row bytes”;
ui。像素格式像素格式=
ui。像素格式.值?0;
指针8指针=指针8.fromAdd
ress(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. 解码前的图片数据给 flutter,由 flutter 提供的解码器解码,从而削减内存拷贝峰值。
2. 与 flutter 官方讨论,尝试从内部减少这次内存拷贝。
FFI 这种方式适合轻度使用、特殊场景使用,支持这种方式可以解决无法获取 ui.Image 的问题,也可以在模拟器上展示图片(flutter <= 1.23.0-18.1.pre),并且图片缓存将完全交给 ImageCache 管理。
TextureTexture 方案与原生结合有一些难度,这里涉及到没有 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';
class TextureImage implements ui.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
int get height => _height;
@override
Future<ByteData> toByteData(
{ImageByteFormat format = ImageByteFormat.rawRgba}) {
// TODO: implement toByteData
throw UnimplementedError();
}
@override
int get width => _width;
}这样的话,TextureImage 实际上就是个壳,仅仅用来计算 cache 大小。实际上,ImageCache 计算大小,完全没必要直接接触到 ui.Image,可以直接找 ImageInfo 取,这样的话就没有这个问题了。
问题三:关于 native 侧感知 flutter image 释放时机的问题。
修改的 ImageCache 释放如下(部分代码):
typedef void HasRemovedCallback(dynamic key, dynamic value);
class RemoveAwareMap<K, V> implements Map<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;
});
}整体架构我们将两种解决方案非常优雅地结合在了一起:
我们抽象出了 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机型: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 在流畅度方面会有一定优势。
滚动流畅性分析设备: 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了,其他方式加载的是原图。
更精简的代码dart 侧代码有较大幅度的减少,这归功于技术方案贴合 flutter 原生设计,我们与原生图片共用较多代码。
FFI 方案补全了外接纹理的不足,遵循原生 Image 的设计规范,不仅让我们享受到 ImageCache 带来的统一管理,也带来了更精简的代码。
单测为了保证核心代码的稳定性,我们有着较为完善的单测,行覆盖率接近95%。
关于开源我们期待通过社区的力量让 PowerImage 更加完善与强大,也希望 PowerImage 能为大家在工程研发中带来收益。
Issues关于 issue,我们希望大家在使用 PowerImage 遇到问题与诉求时,积极交流,提出 issue 时尽可能提供详细的信息,以减少沟通成本。在提出 issue 前,请确保已阅读 readme。
对于 bug 的 issue,我们自定义了模板(Bug report),可以方便地填一些必要的信息。其他类型则可以选择 Open a blank issue。
我们每周会花部分时间统一处理 issues,也期待大家的讨论与 PR。
PR为了保持 PowerImage 核心功能的稳定性,我们有着完善的单测,行覆盖率达到了 95%(power_image库)。
在提交PR时,请确保所提交的代码被单测覆盖到,并且涉及到的单测代码请同时提交。
得益于 Github 的 Actions 能力,我们在主分支 push 代码、对主分支进行 PR 操作时,都会触发 flutter test任务,只有单测通过才可合入。
未来开源是 PowerImage 的开始,而不是结束,PowerImage 可做的事情还有很多,有趣而丰富。比如第一个 issue 中描述的 loadingBuilder 如何实现?比如 ffi 方案如何支持动图?再比如Kotlin和Swift
PowerImage 未来将持续演进,在当前 texture 方案与 ffi 方案共存的情况下,伴随着 flutter 本身的迭代,我们将更倾向于向 ffi 发展,正如在上文的对比中, ffi 方案可以天然享用 raster cache 所带来的流畅度的优势。
PowerImage 也会持续追随 flutter 的脚步,以始终贴合原生的设计理念,不断进步,我们希望更多的同学加入进来,共同成长。
其他四个Flutter开源项目: 闲鱼技术公众号-闲鱼开源
PowerImageGitHub:(star)
https://github.com/alibaba/power_image
Flutter pub:(like)
https://pub.dev/packages/power_image
<