写点什么

混合栈开发,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:002812

评论

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

电商数据整合新篇章:京东商品详情API返回值应用实践

技术冰糖葫芦

api 网关 API Explorer API 策略 pinduoduo API

短剧APP遭遇DDoS攻击的解决方法

网络安全服务

负载均衡 IP DDoS DDoS 攻击 短剧app开发

Go 必知必会:探索 Go 语言中的数组和切片深入理解顺序集合

王中阳Go

数组 go语言 切片 go基础

数据库系列: 主流分库分表中间件介绍(图文总结)

EquatorCoco

数据库

MiniMax 首个文生视频模型发布,可生成 6s,限时免费;阿里 Qwen2-VL 第二代视觉语言模型开源丨RTE 开发者日报

声网

Office 2021全套包支持mac和win安装下载

你的猪会飞吗

Office 2021 Office LTSC 2021 office2021 Mac破解版

浅析前端数据埋点监控:用户行为与性能分析的桥梁

不在线第一只蜗牛

数据库 前端开发

人工智能 | Mistral 大语言模型

测吧(北京)科技有限公司

测试

AI图像放大工具,图片放大无所不能

程序那些事

程序那些事 Stable Diffusion

第七在线携手美国男士内衣品牌Tommy John,共启智能订货新篇章

第七在线

采取预测分析最佳战术,指点企业未来精准决策

智达方通

企业管理 全面预算管理 财务规划 财务预测 财务预测分析

Screaming Frog SEO Spider(尖叫青蛙网络爬虫软件)永久激活版

Rose

iStat Menus 系统状态监控工具 :全面掌控你的Mac

Rose

XLD音频无损解码 X Lossless Decoder for mac 中文版

Rose

Gauss游马会openGauss-全局临时表

Gauss松鼠会

简单实用的苹果解压缩软件推荐

Rose

TikTok直播为什么要用独立IP

Ogcloud

海外直播 tiktok直播 tiktok直播网络 海外IP 海外直播IP

Hidden Bar for mac 菜单栏图标隐藏工具|帮你收纳不常用的菜单栏项目

Rose

文献解读-Genome-wide imputation using the practical haplotype graph in the heterozygous crop cassava

INSVAST

基因数据分析 生信服务

梦中情池!账单自动化#运营通知

开源物联卡管理平台-设备管理

物联网 IoT eSIM安全 java 技术提升

Translatium for Mac翻译软件(支持150多种语言和方言)

Rose

犀牛Rhinoceros 7:专业的3D建模软件

Rose

行业首家!百度智能云通过中国信通院「H5 端人脸识别安全能力」测评

Baidu AICLOUD

人脸识别

加入华为云开源共创,让优秀开发者支持更优秀开发者

华为云开发者联盟

阿里巴巴商品搜索API返回值中的图片与详情链接

技术冰糖葫芦

api 网关 API Explorer API 策略 pinduoduo API

FTP客户端工具 Yummy FTP Pro for Mac中文直装版

Rose

Spring框架之IOC介绍

快乐非自愿限量之名

spring ioc

草图大师SketchUp Pro 2023中文版安装包分享【mac/win】

Rose

有奖问卷调研|OpenTiny给你送天命人好礼啦!

OpenTiny社区

开源 低代码 用户体验 OpenTiny 前端组件库

一场 Kafka CRC 异常引发的血案

不在线第一只蜗牛

kafka 分布式

开发拍卖平台必知的技术风险,如何用最低成本规避?

软件开发-梦幻运营部

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