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

学习福利

【Android 详细知识点思维脑图(技能树)】

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
droid 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

[外链图片转存中…(img-O238Ko7i-1714703022350)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 28
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值