支持加载 ui.Image 能力。在去年基于外接纹理的方案中,使用方无法拿到真正的 ui.Image
去使用,这导致图片库在这种特殊的使用场景下无能为力。
支持图片预加载能力。正如原生precacheImage
一样。这在某些对图片展示速度要求较高的场景下非常有用。
新增纹理缓存,与原生图片库缓存打通!统一图片缓存,避免原生图片混用带来的内存问题。
支持模拟器。在 flutter-1.23.0-18.1.pre
之前的版本,模拟器无法展示 Texture Widget
。
完善自定义图片类型通道。解决业务自定义图片获取诉求。
完善的异常捕获与收集。
支持动图。
原生 Image Widget 先通过 ImageProvider 得到 ImageStream,通过监听它的状态,进行各种状态的展示。比如frameBuilder
、loadingBuilder
,最终在图片加载成功后,会 rebuild
出 RawImage
,RawImage
会通过 RenderImage
来绘制,整个绘制的核心是 ImageInfo
中的 ui.Image
。
Image:负责图片加载的各个状态的展示,如加载中、失败、加载成功展示图片等。
ImageProvider:负责 ImageStream 的获取,比如系统内置的 NetworkImage、AssetImage 等。
ImageStream:图片资源加载的对象。
ui.Image
。我们把 native 内存地址、长度等信息传递给 flutter 侧,用于生成 ui.Image
。 _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;
@override
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.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;
}
decodeImageFromPixels
会有内存拷贝,在拷贝解码后的图片数据时,内存峰值会更加严重。解码前的图片数据给 flutter,由 flutter 提供的解码器解码,从而削减内存拷贝峰值。
与 flutter 官方讨论,尝试从内部减少这次内存拷贝。
ui.Image
只有 textureId。这里有几个问题需要解决:ui.Image
去 build RawImage
从而绘制,这在本文前面的Flutter 原生方案介绍中也提到了。ui.Image
的宽高进行 cache 大小计算以及缓存前的校验。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;
}
flutter 在 2.2.0 之后,ImageCache 提供了释放时机,可以直接复用,无需修改。
< 2.2.0 版本,需要修改 ImageCache,获取 cache 被丢弃的时机,在 cache 被丢弃的时候,通知 native 进行释放。
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 的调用,提供统一的加载与释放能力。
机型:iPhone 11 Pro,图片:300 张网络图,行为:在listView中手动滚动到底部再滚动到顶部,native Cache:100MB,flutter Cache:100MB
Texture:395MB波动,内存较平滑
FFI:480MB波动,内存有毛刺
内存水位:由于 Texture 方案在 flutter 侧的 cache 为占位空壳,没有实际占用内存,因此只在 native 图片库的内存缓存中存在一份,所以 flutter 侧内存缓存实际上比 ffi 方案少了 100MB
毛刺:由于 ffi 方案不能避免 flutter 侧内存拷贝,会有先拷贝再释放的过程,所以会有毛刺。
Texture 适用于日常场景,优先选择;
FFI 更适用于
flutter <= 1.23.0-18.1.pre 版本中,在模拟器上显示图片
获取 ui.Image 图片数据
flutter 侧解码,解码前的数据拷贝影响较小。(比如集团 Hummer 的外接解码库)
设备: 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了,其他方式加载的是原图。
OneFrameImageStreamCompleter
,对于动图,我们将替换为 MultiFrameImageStreamCompleter
,后面如何做,只是一些策略问题,并不难。顺便抛个另一种方案:可以把动图解码前的数据给 flutter 侧解码与渲染,但支持的格式不如原生丰富。[1]
ISSUE: https://github.com/flutter/flutter/issues/86402[2]
PR: https://github.com/flutter/flutter/pull/86555[3]
XianyuTech: https://github.com/XianyuTech