Flutter 图片库高燃新登场

背景

去年,闲鱼图片库在大规模的应用下取得了不错的成绩,但也遇到了一些问题和诉求,需要进一步的演进,以适应更多的业务场景与最新的 flutter 特性。比如,因为完全抛弃了原生的 ImageCache,在与原生图片混用的场景下,会让一些低频的图片反而占用了缓存;比如,我们在模拟器上无法展示图片;比如我们在相册中,需要在图片库之外再搭建图片通道。

这次,我们巧妙地将外接纹理与 FFi 方案组合,以更贴近原生的设计,解决了一系列业务痛点。没错,Power 系列将新增一员,我们将新的图片库命名为 「PowerImage」!

我们将新增以下核心能力:

  • 支持加载 ui.Image 能力。在去年基于外接纹理的方案中,使用方无法拿到真正的 ui.Image 去使用,这导致图片库在这种特殊的使用场景下无能为力。

  • 支持图片预加载能力。正如原生precacheImage一样。这在某些对图片展示速度要求较高的场景下非常有用。

  • 新增纹理缓存,与原生图片库缓存打通!统一图片缓存,避免原生图片混用带来的内存问题。

  • 支持模拟器。在 flutter-1.23.0-18.1.pre之前的版本,模拟器无法展示 Texture Widget

  • 完善自定义图片类型通道。解决业务自定义图片获取诉求。

  • 完善的异常捕获与收集。

  • 支持动图。

去年图片方案可以参考《闲鱼Flutter图片框架架构演进(超详细)》。

Flutter 原生方案

在我们新方案开始之前,先简单回忆一下 flutter 原生图片方案。

3c060ee2521bb2ed4e2d54d9e8f8dc5d.png原生 Image Widget 先通过 ImageProvider 得到 ImageStream,通过监听它的状态,进行各种状态的展示。比如frameBuilderloadingBuilder,最终在图片加载成功后,会 rebuild 出 RawImageRawImage 会通过 RenderImage 来绘制,整个绘制的核心是 ImageInfo 中的 ui.Image

  • Image:负责图片加载的各个状态的展示,如加载中、失败、加载成功展示图片等。

  • ImageProvider:负责 ImageStream 的获取,比如系统内置的 NetworkImage、AssetImage 等。

  • ImageStream:图片资源加载的对象。

在梳理 flutter 原生图片方案之后,我们发现是不是有机会在某个环节将 flutter 图片和 native 以原生的方式打通?

新的方案

我们巧妙地将 FFi 方案与外接纹理方案组合,解决了一系列业务痛点。

FFI

正如开头说的那些问题,Texture 方案有些做不到的事情,这需要其他方案来互补,这其中核心需要的就是 ui.Image。我们把 native 内存地址、长度等信息传递给 flutter 侧,用于生成 ui.Image

首先 native 侧先获取必要的参数(以 iOS 为例):

_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;

dart 侧拿到后

@overrideFutureOr<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;}

我们可以通过 ffi 拿到 native 内存,从而生成 ui.Image。这里有个问题,虽然通过 ffi 能直接获取 native 内存,但是由于 decodeImageFromPixels 会有内存拷贝,在拷贝解码后的图片数据时,内存峰值会更加严重。

这里有两个优化方向:

  • 解码前的图片数据给 flutter,由 flutter 提供的解码器解码,从而削减内存拷贝峰值。

  • 与 flutter 官方讨论,尝试从内部减少这次内存拷贝。

FFI 这种方式适合轻度使用、特殊场景使用,支持这种方式可以解决无法获取 ui.Image 的问题,也可以在模拟器上展示图片(flutter <= 1.23.0-18.1.pre),并且图片缓存将完全交给 ImageCache 管理。

Texture

Texture 方案与原生结合有一些难度,这里涉及到没有 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;@overridevoid dispose() {// TODO: implement dispose}@overrideint get height => _height;@overrideFuture<ByteData> toByteData({ImageByteFormat format = ImageByteFormat.rawRgba}) {// TODO: implement toByteDatathrow UnimplementedError();}@overrideint get width => _width;
}

这样的话,TextureImage 实际上就是个壳,仅仅用来计算 cache 大小。实际上,ImageCache 计算大小,完全没必要直接接触到 ui.Image,可以直接找 ImageInfo 取,这样的话就没有这个问题了。这个问题可以具体看 @皓黯 的 ISSUE[1] 与 PR[2]

问题三:关于 native 侧感知 flutter image 释放时机的问题

  • flutter 在 2.2.0 之后,ImageCache 提供了释放时机,可以直接复用,无需修改。

  • < 2.2.0 版本,需要修改 ImageCache,获取 cache 被丢弃的时机,在 cache 被丢弃的时候,通知 native 进行释放。

修改的 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 awayscheduleMicrotask(() {waitingToBeCheckedKeys.forEach((key) {if (!_pendingImages.containsKey(key) &&!_cache.containsKey(key) &&!_liveImages.containsKey(key)) {if (key is ImageProviderExt) {key.dispose();}}});waitingToBeCheckedKeys.clear();isScheduledImageStatusCheck = false;});}

整体架构

我们将两种解决方案非常优雅地结合在了一起:

d3a32a07edb1724b720b9625e25364b2.png我们抽象出了 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:

ba91e4dc0335e80b1212533e0fde99a4.png

机型:iPhone 11 Pro,图片:300 张网络图,行为:在listView中手动滚动到底部再滚动到顶部,native Cache:100MB,flutter Cache:100MB

这里有两个现象:

Texture:395MB波动,内存较平滑
FFI:480MB波动,内存有毛刺

Texture 方案在内存方面表现优于 FFI,在内存水位与毛刺两方面:

  • 内存水位:由于 Texture 方案在 flutter 侧的 cache 为占位空壳,没有实际占用内存,因此只在 native 图片库的内存缓存中存在一份,所以 flutter 侧内存缓存实际上比 ffi 方案少了 100MB

  • 毛刺:由于 ffi 方案不能避免 flutter 侧内存拷贝,会有先拷贝再释放的过程,所以会有毛刺。

结论:

  1. Texture 适用于日常场景,优先选择;

  2. FFI 更适用于

    1. flutter <= 1.23.0-18.1.pre 版本中,在模拟器上显示图片

    2. 获取 ui.Image 图片数据

    3. flutter 侧解码,解码前的数据拷贝影响较小。(比如集团 Hummer 的外接解码库)

滚动流畅性分析:

4a2a4904b16bbc5013b90bdc533377d9.png

设备: 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了,其他方式加载的是原图。

更精简的代码:

3c8d87124d1927906acc8f9277534bbe.png

dart 侧代码有较大幅度的减少,这归功于技术方案贴合 flutter 原生设计,我们与原生图片共用较多代码。

FFI 方案补全了外接纹理的不足,遵循原生 Image 的设计规范,不仅让我们享受到 ImageCache 带来的统一管理,也带来了更精简的代码。

未来

相信很多人注意到了,上文中少了动图部分。当前动图部分正在开发中,内部的 Pre Release 版本中,在 load 的时候返回的实际上是 OneFrameImageStreamCompleter,对于动图,我们将替换为 MultiFrameImageStreamCompleter,后面如何做,只是一些策略问题,并不难。顺便抛个另一种方案:可以把动图解码前的数据给 flutter 侧解码与渲染,但支持的格式不如原生丰富。

我们希望能将 PowerImage 贡献给社区,为了实现这一目标,我们提供了详细的设计文档、接入文档、性能报告,另外我们也在完善单元测试,在代码提交后或者 CR 时,都会进行单元测试。

最后,也是大家最关心的:我们计划在今年十二月底将代码开源在 「XianyuTech[3]」。

References

[1] ISSUE: https://github.com/flutter/flutter/issues/86402
[2] PR: https://github.com/flutter/flutter/pull/86555
[3] XianyuTech: https://github.com/XianyuTech

d62c6890e44435d43a1152f0e50a7434.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/44662.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

参会记录|2023 上海 CDC 城市领航者之夜(第一期)

前言&#xff1a;2023年3月25日&#xff0c;受邀参加了CSDN旗下的CDC领航者之夜系列活动。CDC 全称为 City Developers Community&#xff0c;是 CSDN 发起的资深开发者圈子&#xff0c;重点围绕金融、互联网等赛道建立俱乐部&#xff0c;定期举办线下闭门会。本活动的与会者多…

60行代码就能构建GPT!网友:比之前的教程都要清晰|附代码

Pine 发自 凹非寺量子位 | 公众号 QbitAI 现在只用60行代码&#xff0c;就能从0构建GPT了&#xff01; 想当初&#xff0c;前特斯拉前AI总监的minGPT和nanoGPT也都还要300行代码。 这个60行代码的GPT也有名字&#xff0c;博主将它命名为PicoGPT。 不过和此前minGPT和nanoGPT的教…

Java实现腾讯云短信服务功能(保姆级,超详细,附源码)

百度可以搜索到很多短信服务提供商&#xff0c;这里以腾讯云的短信服务为例&#xff08;阿里云的也尝试了一下&#xff0c;可能是我运气不好&#xff0c;试了好几次都没有通过审核&#xff0c;阿里云的现在好像也需要资质了&#xff0c;企业或者商户&#xff09;&#xff0c;腾…

java实现腾讯云直播

云直播官方文档&#xff1a;https://cloud.tencent.com/document/product/267 云直播在线生成api&#xff1a;https://console.cloud.tencent.com/api/explorer?Productlive&Version2018-08-01&ActionUpdateLiveWatermark&SignVersion 对云直播api调用的主要目的…

java对接腾讯云短信平台详细代码

在项目中集成一下短信功能。对比了几个服务商&#xff0c;最终选择了腾讯云&#xff0c;因为他每个月免费送100条。 一、申请API 1. 注册腾讯云实名认证后&#xff0c;开通短信服务。 2. 获取AppID和AppK待用。 3. 创建短信签名模板和短信正文模板。系统说半日内审核&#xff…

【腾讯云 Finops Crane 集训营】深入了解 Crane 开源项目,集训营实验操作指南,体验过程总结

前言 最近有幸参与了腾讯云举办的Finops Crane的集训营。在这个过程中&#xff0c;老师认真指导&#xff0c;让我受益非浅&#xff0c;也让我真正理解了这一产品所带来的意义。 在听了老师们的介绍和讲解后&#xff0c;我马不停蹄地开始了自己摸索。首先是跟着视频和官方教程…

腾讯云对象存储COS及CDN加速配置

文章目录 相关文章1. 登陆腾讯云官网&#xff0c;进入腾讯云对象存储COS控制台2. 创建存储空间3. 添加自定义CDN加速域名4. 购买腾讯云免费SSL证书5. 腾讯云 CDN 域名部署 SSL证书6. 测试自定义域名HTTPS访问COS中的文件7. 创建腾讯云子账户&#xff0c;授予对象存储权限&#…

SpringBoot整合腾讯云COS对象存储实现文件上传

企业级项目开发中都会有文件、图片、视频等文件上传并能够访问的场景&#xff0c;对于初学者Demo可能会直接存储在应用服务器上&#xff1b;对于传统项目可能会单独搭建FastDFS、MinIO等文件服务来实现存储&#xff0c;这种方案可能对于企业成本较小&#xff0c;但缺点也是很多…

腾讯云区块链使用心得

浅谈一下最近体验TBaaS的感想。 TBaaS 区块链服务平台集成开发、管理和运维等功能&#xff0c;支持客户在云上快速部署联盟区块链网络环境。基于 TBaaS 区块链服务平台&#xff0c;客户可以降低对区块链底层技术的获取成本&#xff0c;专注在区块链业务模式创新及业务应用的开发…

阿里云和腾讯云全方位对比

一、竞品分析目的 本文旨在人工智能行业通过对云服务平台代表性产品阿里云、腾讯云的产品定位、核心功能、发展战略等方面的研究&#xff0c;探讨人工智能云服务平台产品的在国内的发展趋势。为之后根据实际情况利用具有较多优势的云服务平台研发应用层人工智能产品提供决策辅…

与腾讯云物联网对接

与腾讯云物联网对接 第一步&#xff1a;移植Tencenttinyos的相关文件 net platform&#xff1a;串口驱动框架&#xff0c;只要HAL层 drivers&#xff1a; 第二步&#xff1a;添加头文件路径 第三步&#xff1a;添加esp8266例程 文件夹路径&#xff1a;TencentOS-tiny-master…

python调用腾讯云API语音识别

一、登录腾讯云、开通语音识别接口、获取密钥 登录 - 腾讯云 (tencent.com) 二、打开 API Explorer 语音识别 录音文件识别请求 - API 文档 - 文档中心 - 腾讯云 选择 录音文件请求 三、打开参数说明&#xff0c;按需求填入参数。 其中Data输入的的是音频文件经过base64编码后…

腾讯云TRTC服务实现Web视频会议

腾讯云TRTC服务实现Web视频会议 背景 近期公司承接了某高校智慧校园的项目建设工作&#xff0c;其中在家校协作的板块中需要进行视频教学&#xff0c;以及线上屏幕共享&#xff0c;为了完成这一需求&#xff0c;我在自研与第三方服务的选择之间选择了第三方&#xff0c;主要因…

腾讯云HiFlow场景连接器

文章目录 &#xff08;一&#xff09;腾讯云HiFlow场景连接器是什么&#xff1f;&#xff08;二&#xff09;腾讯云HiFlow场景连接器对于我的工作/生活能有什么好处呢&#xff1f;添加企业微信机器人 &#xff08;一&#xff09;腾讯云HiFlow场景连接器是什么&#xff1f; 腾讯…

监控、无人机摄像头RTSP协议对接腾讯云直播

监控、无人机摄像头RTSP协议对接腾讯云直播 1. 需求与目标 传统监控高清摄像机ip camera&#xff08;如: 海康,大华等&#xff09;遵循监控行业标准&#xff0c;一般只支持rtsp传输协议&#xff0c;互联网直播通用标准为rtmp协议&#xff0c;将这些摄像机视频流引入互联网直播…

【虚拟人快讯】超写实虚拟人青鸟发布,脑白金推出数字人形象

1、3月17日&#xff0c;作为广西文化符号的“刘三姐”穿越时空&#xff0c;以国内首个省级超写实文旅数字推广大使的身份在全网公开亮相&#xff0c;广西壮族自治区文化和旅游厅厅长欧余军在“元宇宙世界”&#xff0c;宣布授予“刘三姐数字人”“广西文化旅游数字推广大使”称…

Gmail邮箱怎么获取授权码?熟悉一下

打开谷歌邮箱https://mail.google.com/mail/u/0/#settings/fwdandpophttps://mail.google.com/mail/u/0/#settings/fwdandpop 进入“转发和 POP/IMAP”&#xff0c;启用 IMAP。 点击“右上角Logo” > “管理您的google账号” 在回到“管理您的google账号” 设置应用专用密码…

【reCAPTCHA 】添加Google验证码

在网站登陆注册时常常需要用到验证码&#xff0c;来防止站点被攻击。 大概这个样子&#xff1a; 现在框架是前后端分离的&#xff0c;angularwebapi弄个验证码感觉有点麻烦 然后就找到google的reCAPTCHA 还挺好使的&#xff0c;记录一下&#xff1a; 地址&#xff1a;https:…

Google验证码ReCaptcha V3

因为工作的原因需要使用Google验证码ReCaptcha v3&#xff0c;所以我就上网了解了一下&#xff0c;下面是我的一些学习分享。 大家应该都是用过google的验证码&#xff0c;如 这种情况的需要我们手动去选择&#xff0c;ReCaptcha V3则不需要了&#xff0c;不需要用户去手动的验…

使用Google reCAPTCHA进行人机验证

reCAPTCHA是Google公司推出的一项验证服务&#xff0c;使用十分方便快捷&#xff0c;在国外许多网站上均有使用。它与许多其他的人机验证方式不同&#xff0c;它极少需要用户进行各种识图验证。 它的使用方式如下如所示&#xff0c;只需勾选复选框即可通过人机验证。 虽然简单…