Flutter 新图片库 PowerImage 架构

int width = map[‘width’];

int height = map[‘height’];

int rowBytes = map[‘rowBytes’];

ui.PixelFormat pixelFormat =

ui.PixelFormat.values[map[‘flutterPixelFormat’] ?? 0];

Pointer pointer = Pointer.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 会有内存拷贝,在拷贝解码后的图片数据时,内存峰值会更加严重。

这里有两个优化方向:

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

  2. 与 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;

@override

void dispose() {

// TODO: implement dispose

}

@override

int get height => _height;

@override

Future toByteData(

{ImageByteFormat format = ImageByteFormat.rawRgba}) {

// TODO: implement toByteData

throw UnimplementedError();

}

@override

int get width => _width;

}

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

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

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

  2. < 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 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;

});

}

整体架构

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

1969664742790f11025ad6415e2b2e3d.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 图片库提供解码前的数据。

数据对比

  Texture

685955497b68238b37448827b1ef052b.png

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

这里有两个现象:

Texture:395MB波动,内存较平滑

FFI:480MB波动,内存有毛刺

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

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

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

结论:

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

  2. FFI 更适用于

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

获取 ui.Image 图片数据

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

  滚动流畅性分析

39cb9c788e5f90f707ebd64fedb63695.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数据并分析。

结论:

  1. UI thread 耗时 texture 方式最好,PowerImage 略好于 IFImage,FFI方式波动比较大。

  2. Raster thread 耗时 PowerImage 好于 IFImage。Origin 原生方式好是因为对图片 resize了,其他方式加载的是原图。

  更精简的代码

d5324e2c76977bb7ba70d0f4b69c9881.png

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

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

数据对比

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

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

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

References

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
存中…(img-2RDjh1Sy-1715516570647)]

[外链图片转存中…(img-A9Z6Aldp-1715516570650)]

[外链图片转存中…(img-SL7x0UG1-1715516570651)]

[外链图片转存中…(img-7rQSoSUe-1715516570652)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值