HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

混合栈开发,AliFlutter 如何解决图片问题

  • 2020-04-13
  • 本文字数:5822 字

    阅读完需:约 19 分钟

混合栈开发,AliFlutter如何解决图片问题

前言

在 Flutter 官方体系内,对混合栈开发支持不够友好。比如对于图片资源管理,以及如何对接 Native 图片库的问题,社区上已经有一些方案,但或多或少存在一些问题,或与 Flutter 图片加载流程背离较大,难以融合。


与此同时,在电商类应用中,使用 Flutter 实现的长列表多图页面,往往面临着严重的性能问题。例如滚动过程,过多的并发图片请求阻塞了网络,造成 CPU、内存飙高。在淘宝特价版 Flutter 商品详情页面里,还遇到了更棘手的大 Cell 问题,Flutter List 的回收机制对大 Cell 无能为力,造成内存疯涨极易 OOM。


为解决这些问题,AliFlutter 基础容器在 Flutter 官方的 Image Widget 体系里进行扩展,实现了一套完整的图片解决方案。具备的能力如下:


  1. 外接原生图片库,共享本地文件缓存、内存缓存。

  2. 图片请求取消功能,解决网络并发限制引起的排队加载缓慢,以及无效的解码、纹理上传造成资源浪费的情况。

  3. 图片解码并发管理,降低 CPU、内存峰值。

  4. 支持 GIF,在播放 GIF 时逐帧上传纹理,降低内存占用。

  5. 简单易用的 Placeholder。

  6. 允许将 Flutter 内置的各种图片解码库剥离,减小包大小。

  7. 业务无感的方式解决 List 滚动时,大 Cell 中的图片不能动态加载、回收的问题。解决 Native、Weex 体系下的顽疾。


关于大 Cell 问题的解决方案,下周将会推出文章:《细化 Flutter List 内存回收,解决大 Cell 问题》

Flutter 的图片加载过程

首先介绍一下 Flutter 里图片相关的加载逻辑。显示图片使用 Image Widget。Image Widget 创建时,可以指定不同的图片来源:


  • 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 重绘。


  • Flutter 自身提供的 ImageCache,以 ImageProvider 作为 Key 缓存了 ImageStreamCompleter。对于相同的图片,以及正在下载中的图片,不会重复加载。当图片上传 GPU 完成后,会以图片的 W * H * 4 更新缓存状态。所以实际缓存的是 GPU 纹理。使用 Flutter 原始 Image 组件开发时,将这个缓存大小设置为 0,可以一定程度缓解内存压力(不多余缓存任何纹理,Widget 销毁,纹理释放),但是会造成图片的反复下载、解码、上传 GPU,系统开销较大。

AliFlutter 方案

Flutter 的图片加载流程抽象完备,我们自上而下进行定制化,在不修改原来链路任何代码的情况下,实现自己的 ImageProvider 和 Codec 对象,对接外部图片库。同时,图片纹理仍然可以保存到 Flutter 的 ImageCache 中,与 Flutter 原始方案完美融合。


Flutter Widget 层扩展

扩展 Image Widget,指定使用外接图片库作为图片 Provider。

// File: lib/src/widgets/image.dartImage.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。例如


// 使用本地资源作为 placeholderImage.external_adapter(  'https://gw.alicdn.com/tfs/TB1Aa0UcF67gK0jSZPfXXahhFXa-750-140.png',  placeholderProvider: AssetImage("assets/placeholder.jpg"),) // 使用另一个网络资源作为 placeholderImage.external_adapter(  'https://gw.alicdn.com/tfs/TB1Aa0UcF67gK0jSZPfXXahhFXa-750-140.png',  placeholderProvider: ExternalAdapterImage("https://alicdn.com/image1024.jpg"),)
复制代码

ExternalAdapterImage

该类继承自 ImageProvider,并在 @override load 方法中创建 ExternalAdapterImageStreamCompleter。load 方法由 ImageProvider 的 resolve 方法调用,返回图片数据流管理类。

ExternalAdapterImageStreamCompleter

该类负责图片的加载,回调逻辑,其主要职责如下:


  • 处理 placeholderProvider,在主图返回前,让 Image Widget 显示 placeholder 图片。

  • 创建 C++ 层 ExternalAdapterImageFrameCodec 对象,调用 getNextFrame 获取图片信息(是否为动图、帧数、播放时间),以及纹理对象 ui.Image 并通知 Widget 显示。

  • 对于 GIF 等多帧图片,循环调用 ExternalAdapterImageFrameCodec 对象的 getNextMultiframe 接口获取动图的每一帧 ui.Image 并通知 Widget 显示。

  • 当无监听者时,调用 ExternalAdapterImageFrameCodec 的 cancel 接口取消图片任务。

Flutter Engine 层扩展

ExternalAdapterImageFrameCodec

该类为 C++ 实现,继承自 DartUI 库中的 Codec 类,被 Dart 类 ExternalAdapterImageStreamCompleter 持有、管理、调用。


该类与 ExternalAdapterImageProvider 进行交互。主要方法是 getNextFrame , getNextMultiframe,cancel。

ExternalAdapterImageProvider

该类为 Abstract C++ 接口类,定义了需要各平台适配层实现的接口。主要接口如下:


  • 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 调用,控制多图加载时的资源消耗。


其中 PlatformImage 结构体定义如下


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};
复制代码


执行伪码如下,多次切换线程也是符合 Flutter 的纹理加载管线。多次判断 cancel,避免了大量无效操作,降低了列表滚动时的资源消耗。


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  }}
复制代码


执行时序图:



直接将 C++ 接口公开,理论上就可以直接对接手淘图片库了。但是 C++ 接口使用起来不太方便,且不符合 Flutter 规范(对 iOS/Mac 平台应该提供 ObjC 类,对 Android 平台应该只提供 Java 类),而且对于平台层图片对象的处理,由 Engine 提供统一实现更为安全。因此,在 Engine 内部,针对 iOS/Mac,以及 Android 平台各提供了一套封装。


以 iOS 为例,最终在 Flutter.framework 里对外公开的 ObjC 接口为:


@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
复制代码


由外部注册 id<FlutterExternalAdapterImageProvider> 类对接手淘图片库,在每次请求时,返回一个支持 cancel 方法的对象用于取消请求。完成后通过 callback 返回 UIImage 对象,可以为 GIF 图。


对于 Android,最终公开的也是非常简单的一个 Java 类供外部实现。

AliFlutter 方案的优化

延迟加载

在 ExternalAdapterImageStreamCompleter 中,真正调用 Codec 加载图片前,会做短暂等待。如果此时 Widget 已经被回收,会将自己从 Completer 的 listeners 中移除(实际添加的 listener 为 Widget 的 State)。等待过后,如果监听者为空,不会做真实请求。


Flutter 最新代码(2020.1.30)中,貌似对快速滚动过程图片的加载也做了优化,避免一些不必要的图片请求。Commit 见 https://github.com/flutter/flutter/commit/169529c37064568a17b634968c73a7ff79029dfb

图片取消

前面提到,当 ExternalAdapterImageStreamCompleter 无监听者时,会调用 ExternalAdapterImageFrameCodec 的 cancel 方法。


Codec 从平台图片库获取到图片并最终上传为纹理(ui.Image)的过程,需要切换多次线程。


在 cancel 方法中,不但会通知图片库取消网络请求,而且记录标志位。在切换线程的整个过程中,多次检查标记位。


经过实际测试,在列表快速滑动或网络、机器性能较慢时,可以避免大量无效图片下载、解码、上传 GPU 等动作。

UIImage 转 Bitmap 并发控制

iOS 平台上,将 UIImage 转换为 Bitmap 不可避免要进行像素的拷贝。一些时候,CGImageGetBitmapInfo(UIImage.CGImage) 获取到的位图格式需要进行转换才可以送给 OpenGL。完成纹理上传后,拷贝的内存会被释放。此时,如果过多的图片同时进行转换,难免产生内存尖刺。解码过程复用的 Flutter ConcurrentTaskRunner,该 Runner 并发数量仍然过高(6 个左右)。


因此在解码时,Codec 会动态调用 ExternalAdapterImageProvider 的 evaluateDeviceStatus 接口评估内存状态,再次控制并发数量。实际使用发现,2~3 个并发,图片的加载速度仍然非常快,同时可以较好地控制解码过程的临时内存占用。

GIF 逐帧上传

GIF/APNG 动图是内存消耗大户,AliFlutter 方案在显示动图时,通过 ExternalAdapterImageFrameCodec 的 getNextMultiframe 接口逐帧获取纹理对象。每个时刻,只会有一帧上传 GPU,达到节省内存的目的。

开发过程的插曲:Flutter 1.9.1 版本的内存泄漏

在调试外接图片库的过程中,通过对底层纹理的计数,发现有内存泄漏的情况。淘宝特价版详情页面接入 Flutter,并且使用了 Boost。现象为


  • 进入详情页面,并退出,反复进入退出。无内存泄漏。(不进入二级详情)

  • 进入详情页面,点宝贝推荐再进入一个详情页面,返回,再返回。产生内存泄漏。

  • 也就是说使用 Boost 管理多个 Flutter 栈时,只要有二级 Flutter 页面,就会产生内存泄漏。看上去是整个 Widget 树泄漏,导致底层的 ui.Image 纹理对象不能释放。


这个问题排查过程比较困难,主要的方法是不断简化详情页面,并最终定位出问题的组件。最终发现业务代码里只要使用 RaisedButton 就会产生问题。通过一层层的剥去代码,最终发现了有 Bug 的组件是官方的 InkWell。RaisedButton 通过多层关系最终使用到了 InkWell 组件。


在 _InkResponseState 类中,didChangeDependencies 方法未从 focusManager 里移除 listener(其实也就是自己)。导致在 Boost 管理的堆栈中,二级 Flutter 页面返回时,前一个页面组件的该方法多次执行,造成泄漏。


// Class _InkResponseStatevoid 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 新版中已经修复了,整个代码完全变了,官方用其它方式避免了这种情况。


这里走了一些弯路。事后,通过 Dart 调试工具,可以看到出问题的时候 FocusManager 对象不断增长。提前用 Dart 工具,应该可以更早到定位到问题与使用 FocusManager 有关。


总结

这个方案完整探索了如何遵循 Flutter 官方的图片加载逻辑,对接外接图片库。同时整体方案对官方代码只添加、不修改,并提供了 ObjC、Java 语言的接口。方案完整度较高,后续可以与官方沟通合入主干。在图片加载的完整过程中,多次介入判断,较好地避免了无效的图片下载、解码、上传纹理工作,减少了系统资源的消耗。


为了避免对手淘图片库进行修改,且复用其内存缓存,目前的方案接收平台层解码后的 UIImage、AndroidBitmap 对象,再获取其位图数据上传纹理。后续可以让图片库返回未解码的文件数据,交给 Flutter 解码,整体流程可以再简化一些。不过目前的方案可以将所有图片解码库从 Flutter 里剥离,减小包大小,各有利弊。


基于该方案,同时探索了如何在 Flutter 中解决大 Cell 中多张图片同时加载产生的内存飙高问题,下周将会推出:《细化 Flutter List 内存回收,解决大 Cell 问题》 敬请期待。


本文转载自公众号淘系技术(ID:AlibabaMTT)。


原文链接


https://mp.weixin.qq.com/s/0dJzviKLYXT4j46u3oOwGg


2020-04-13 10:002780

评论

发布
暂无评论
发现更多内容

完整的APP开发流程|软件定制

Geek_16d138

app定制开发 软件开发定制

tools-重命名

鸿蒙组件化构建

SkyFire

鸿蒙 OpenHarmony

设计模式-建造者模式

小程序是什么?如何开发小程序?

Geek_16d138

小程序开发,软件开发

Things 3 for Mac(日程和任务管理工具)

展初云

Mac软件 任务管理软件 待办记事软件 Things

稳定扩散AI 纹理生成器

3D建模设计

AI Stable Diffusion AI自动纹理 AI 绘图 AI贴图

Macos数据备份工具:BeLight Get Backup Pro 3 附 注册机

繁星

数据备份 数据备份工具 Get Backup Pro 3

Macos人工智能转录工具:AI Transcription for mac激活版 支持M1/M2

加油,小妞!

AI Transcription 人工智能语音转录工具

Util应用框架基础(五)- 异常处理

何镇汐

开源 后端

英特尔CEO:将按计划或提前完成“四年五个制程节点”,明年将公布新计划

E科讯

合成数据如何改变制造业

3D建模设计

机器学习 机器视觉 合成数据 人工自能 合成数据生成

如果有一款专门用于3D纹理贴图的工具,大家会愿意用吗?

3D建模设计

Stable Diffustion AI纹理 AI 贴图 稳定扩散模型

一文读懂分布式 NFT数据网络 Bitscrunch

股市老人

Mac电脑触控板增强软件 Bettertouchtool激活中文版

mac大玩家j

Mac软件

合成数据加速机器视觉学习 合成数据即将改变人工智能。如今,机器学习被用于各种基于视觉的

3D建模设计

机器学习 机器视觉 合成数据 合成数据生成

什么是UV贴图?

3D建模设计

人工智能 UV贴图 AI纹理

Mac电脑窗口辅助管理推荐 Magnet激活中文版

胖墩儿不胖y

Mac软件 窗口管理工具 窗口辅助软件

目前软件定制app开发方式主要是那种?

Geek_16d138

app定制开发 软件开发定制

2023年10 种用于最佳稳定扩散最佳方案

3D建模设计

人工智能 Stable Diffusion Ai绘图 AI纹理 AI 贴图

Inklet for Mac(触控板绘画工具)

展初云

Mac软件 触控板增强 绘画工具

虚拟服务器的应用场景

Geek_f19a80

服务器

最高2000万元扶持!2024 年度宁波市甬江人才工程公告

科兴未来News

宁波甬江 甬江工程 人才政策 创业政策 创业资源

Macos音乐制作工具:Ableton Live 11 Suite 附 完整安装教程

繁星

音乐制作 Ableton Live 11

Linux RPM包安装、卸载和升级

梦笔生花

Get Backup Pro 3 注册激活版 附 完整安装教程 支持M1

加油,小妞!

数据恢复 Get Backup Pro 3

运行ps\ai弹出This non-genuine Adobe app will be disabled soon,尝试解决办法

展初云

adobe Mac软件

Util应用框架基础(四)- 验证

何镇汐

开源 后端

混合栈开发,AliFlutter如何解决图片问题_容器_王乾元(神漠)_InfoQ精选文章