前言
外接原生图片库,共享本地文件缓存、内存缓存。
图片请求取消功能,解决网络并发限制引起的排队加载缓慢,以及无效的解码、纹理上传造成资源浪费的情况。
图片解码并发管理,降低 CPU、内存峰值。
支持 GIF,在播放 GIF 时逐帧上传纹理,降低内存占用。
简单易用的 Placeholder。
允许将 Flutter 内置的各种图片解码库剥离,减小包大小。
业务无感的方式解决 List 滚动时,大 Cell 中的图片不能动态加载、回收的问题。解决 Native、Weex 体系下的顽疾。
Flutter 的图片加载过程
Image.network
Image.file
Image.asset
这些方法创建了背后不同的 ImageProvider。当 Widget 构建并更新 State 时,调用相应的 ImageProvider 进行解析。ImageProvider 返回一个 ImageStream 对象,并让这些 Stream 对象共同监听一个 ImageStreamCompleter。与此同时,ImageProvider 为这个 Completer 提供不同的 load 方法加载来自网络、文件或资源中的图片数据(未解码)。当数据加载好后,调用 Engine 的 instantiateImageCodec 方法创建 C++ Codec(ui.Codec) 对象。由 Codec 负责解码,上传 GPU 纹理,生成 ui.Image。全部完成后,回调 Completer,以 Provider 作为 Key 将 Completer 加入缓存,并通知 Widget 重绘。
AliFlutter 方案
▐ Flutter Widget 层扩展
扩展 Image Widget,指定使用外接图片库作为图片 Provider。
// File: lib/src/widgets/image.dart
Image.external_adapter(
String src, {
Key key,
....
int targetWidth, // 请求的图片的宽
int targetHeight, // 请求的图片的高
Map<String, String> parameters, // 透传给图片库的参数
Map<String, String> extraInfo,
ImageProvider placeholderProvider, // placeholder 可以指定为其它 Provider
}) : image = ExternalAdapterImage(src, // 创建自定义的 ExternalAdapterImage Provider
targetWidth: targetWidth, targetHeight: targetHeight,
placeholderProvider: placeholderProvider,
parameters: parameters, extraInfo: extraInfo),
super(key: key);
这个方法中的 placeholderProvider 提供了更简单直观的方式为图片指定 placeholder。例如
// 使用本地资源作为 placeholder
Image.external_adapter(
'https://gw.alicdn.com/tfs/TB1Aa0UcF67gK0jSZPfXXahhFXa-750-140.png',
placeholderProvider: AssetImage("assets/placeholder.jpg"),
)
// 使用另一个网络资源作为 placeholder
Image.external_adapter(
'https://gw.alicdn.com/tfs/TB1Aa0UcF67gK0jSZPfXXahhFXa-750-140.png',
placeholderProvider: ExternalAdapterImage("https://alicdn.com/image1024.jpg"),
)
ExternalAdapterImage
ExternalAdapterImageStreamCompleter
处理 placeholderProvider,在主图返回前,让 Image Widget 显示 placeholder 图片。
创建 C++ 层 ExternalAdapterImageFrameCodec 对象,调用 getNextFrame 获取图片信息(是否为动图、帧数、播放时间),以及纹理对象 ui.Image 并通知 Widget 显示。
对于 GIF 等多帧图片,循环调用 ExternalAdapterImageFrameCodec 对象的 getNextMultiframe 接口获取动图的每一帧 ui.Image 并通知 Widget 显示。
当无监听者时,调用 ExternalAdapterImageFrameCodec 的 cancel 接口取消图片任务。
▐ Flutter Engine 层扩展
ExternalAdapterImageFrameCodec
该类与 ExternalAdapterImageProvider 进行交互。主要方法是 getNextFrame , getNextMultiframe,cancel。
ExternalAdapterImageProvider
void request``(requestId, requestInfo, callback(platformImage, releaseFunc))
该方法向图片库请求图片,图片库完成后,通过 callback 异步返回。platformImage 封装平台层的图片对象(如 UIImage),callback 同时返回一个 releaseFunc,Flutter 使用完成该图片后,调用该方法释放图片。
void cancel(requestId)
通知图片库取消某个请求
Bitmap decode(platformImage, frameIndex)
解码图片的某一帧,并返回 Bitmap 数据。
evaluateDeviceStatus(&cpuCount, &maxMemory)
允许并发的图片解码任务数量,以及解码数据的内存使用量。这个方法会经常被 ExternalAdapterImageFrameCodec 调用,控制多图加载时的资源消耗。
struct PlatformImage {
uintptr_t handle = 0;
int width = 0; // width in pixel
int height = 0; // height in pixel
int frameCount = 1; // multiframe image such as GIF
int repetitionCount = -1; // infinite
int durationInMs = 0; // in milliseconds
};
class ExternalAdapterImageFrameCodec {
ExternalAdapterImageProvider provider;
void getNextFrame() {
async(provider.request([](image) {
if (cancelled) {
return;
}
async(workerThread, {
if (cancelled) {
return;
}
bitmap = provider.decode(image);
async(ioThread, {
if (cancelled) {
return;
}
ui.Image texture = uploadToGPU(bitmap);
async(uiThread, {
if (cancelled) {
return;
}
callbackDart(texture);
})
})
})
}))
}
void cancel() {
provider.cancel()
cancelled = true
}
}
执行时序图:
@protocol FlutterExternalAdapterImageRequest <NSObject>
- (void)cancel;
@end
@protocol FlutterExternalAdapterImageProvider <NSObject>
- (id<FlutterExternalAdapterImageRequest>)request:(NSString*)url
targetWidth:(NSInteger)targetWidth
targetHeight:(NSInteger)targetHeight
parameters:(NSDictionary<NSString*, NSString*>*)parameters
extraInfo:(NSDictionary<NSString*, NSString*>*)extraInfo
callback:(void(^)(UIImage* image))callback;
@end
对于 Android,最终公开的也是非常简单的一个 Java 类供外部实现。
▐ AliFlutter 方案的优化
延迟加载
图片取消
Codec 从平台图片库获取到图片并最终上传为纹理(ui.Image)的过程,需要切换多次线程。
在 cancel 方法中,不但会通知图片库取消网络请求,而且记录标志位。在切换线程的整个过程中,多次检查标记位。
经过实际测试,在列表快速滑动或网络、机器性能较慢时,可以避免大量无效图片下载、解码、上传 GPU 等动作。
UIImage 转 Bitmap 并发控制
GIF 逐帧上传
开发过程的插曲:Flutter 1.9.1版本的内存泄漏
进入详情页面,并退出,反复进入退出。无内存泄漏。(不进入二级详情)
进入详情页面,点宝贝推荐再进入一个详情页面,返回,再返回。产生内存泄漏。
也就是说使用 Boost 管理多个 Flutter 栈时,只要有二级 Flutter 页面,就会产生内存泄漏。看上去是整个 Widget 树泄漏,导致底层的 ui.Image 纹理对象不能释放。
// Class _InkResponseState
void didChangeDependencies() {
super.didChangeDependencies();
_focusNode?.removeListener(_handleFocusUpdate);
_focusNode = Focus.of(context, nullOk: true);
_focusNode?.addListener(_handleFocusUpdate);
// 原来的代码缺少这一行,导致多次添加 listener 造成组件泄漏。
WidgetsBinding.instance.focusManager.removeHighlightModeListener(_handleFocusHighlightModeChange);
WidgetsBinding.instance.focusManager.addHighlightModeListener(_handleFocusHighlightModeChange);
}
该问题在 Flutter 新版中已经修复了,整个代码完全变了,官方用其它方式避免了这种情况。