找回密码
 会员注册
查看: 24|回复: 0

Flutter图片内存优化实践

[复制链接]

8

主题

0

回帖

25

积分

新手上路

积分
25
发表于 2024-10-9 15:06:56 | 显示全部楼层 |阅读模式
Flutter图片内存优化实践 Flutter图片内存优化实践 裴伟 李珂 贝壳产品技术 贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容 2021年01月06日 15:58 1 背景相信很多移动端开发的同学都有遇到过由于图片内存引起的OOM问题,最近兴起的跨平台Flutter技术同样面临内存方面的挑战,尤其是图片场景引起的内存问题,困扰了很多使用Flutter技术的开发者。阿里闲鱼、头条等使用Flutter技术的开发公司也推出过了一些文章来介绍自己的图片方案,但是很遗憾,他们并没有将方案开源出来。本文将介绍贝壳移动端解决Flutter 图片内存问题的一些实践经验和优化思想,后续会将已经在贝壳经纪人端APP中得到使用和检验的贝壳Flutter图片库开源给大家,来共同学习和交流。2 图内存基础图片要显示在移动终端一般会经历加载、解码和渲染三个步骤,其中解码阶段是内存消耗最多的过程。那什么是解码?解码过程是什么样子的?我们以iOS端UIImageView为例,UIImageView 要显示在屏幕上的时候,需要以UIImage作为数据源,UIImage持有的数据是未解码的压缩数据,能节省较多的内存和加快存储。当UIImage数据被赋值给UIImageView的UIImage时(imageView.image = image),图像数据会被解码,变成代表RGB的颜色数据。解码是一个计算量较大的任务,主要需要CPU来执行;并且解码出来的图片所占内存与图片的宽高正相关,而与图片原来的大小无关。解压图片需要的内存算法为:图所占内存 = 图度(像素)* 图宽度(像素)* 个像素所占内存空间(单位:字节)安卓的图片加载和iOS是一样的,内存的消耗同样发生在解码过程中。了解了iOS和安卓加载图片的原理以及内存消耗发生的过程,那么在Flutter 的图片加载过程中又发生了什么,要生成GPU能够渲染的纹理数据,是怎么的一个过程呢?下面我们就一起来分析一下Flutter端图片的加载流程,从中找到我们优化Flutter图片内存占用的理论支撑。3 Flutter中图加载流程为了更好的了解Flutter图片的加载流程,先我们来了解涉及到的四个概念:ImageProvider:图的抽象概念(如下图NetworkImage、FileImage等),约定图唯性(key)、获取图字节数据(load),创建 ImageStream于监听结果。key于描述图的唯以及是否已有缓存。Image :显示图的Widget,通过ImageState管理ImageProvider的命周期。ImageStream:图的加载对象,通过 ImageStreamCompleter 最后会返回个 ImageInfo,ImageInfo 中的ui.Image是RenderObject的标绘制对象。ImageCache:图缓存单例PaintingBinding.instance.imageCache(默认100MB或1000张图满其,就标记最先缓存的对象给释放其引用)。下面简要介绍下图加载的示意图:以网络图片NetworkImage加载为例,主要流程是:ImageState调用ImageProvider的 resolve 最终得到ImageStream对象(第6步)然后resolve()通过ImageConfiguration的获取图片的精确描述(key)(第2,3步)而后通过key从ImageCache中查看是否有已缓存的ImageStreamCompleter并关联到ImageStream中。(第6步蓝色)如果ImageCache中没有该key对应的缓存,则从当前对应的类型NetworkImage中通过load() 返回新的ImageStreamCompleter(第6步红色),其中load()会通过http下载图片,再经过PaintingBinding.instance.instantiateImageCodec得到ui.Codec对象(engine层对应MultiFrameCodec), 如果传入采样宽高,engine将会返回裁剪后的ui.Image。第7步返回的ImageStream设置监听,拿到ImageInfo然后回调handleImageFrame。最后ui.Image交给RenderImage来完成绘制。以上是Image加载流程描述,从源码(1.12.13-hotfix.9)可以清楚地看出在ImageProvider.resolve(...)中,obtainKey 、load、imageCache 三者的关系,ImageProvider中给出了图片缓存复用的方式。举个例子,当我们业务场景中需要加载一张房源图片(3000*4000像素大小),在不设置cacheWidth和cacheHeight情况下,显示后内存大小占用为 45.77MB, 而当我们设置采样大小为300*400的情况下内存大小占用为 0.4577MB (根据最终返回ui.Image的width,height,再用前面图片内存公式计算)。Image.network(imageUrl_3000x4000_png,width:width,height:height,cacheWidth:300,cacheHeight:400,)结合上述Image加载流程,我们看到Flutter的图片加载流程从本质上和Native 是一致的,其内存消耗同样是在codec进行解码的过程中出现。了解这些后,我们就有了图片内存的优化方向(如何利用ImageCache)。内存缓存复用,避免重复的图片造成不必要的内存消耗内存及时释放,设置合适的内存淘汰策略,来处理目前不在屏幕上使用的图片降低大图的内存峰值,通过降采样的方式避免内存峰值出现而导致OOM。下面按照以上思路实施了具体的方案。4 提出解决案:AOP案基于上的分析,可只要我们能够将图加缓存,并设置合适的采样大小,似乎就可以解决我们遇到的内存问题。我们找到以下可代码:接下来Flutter引擎就会依据传如的宽下采样图,最终返回image。经我们测试发现确实可以减少内存。如原来在拍照图10张左右就出现OOM,然后Flutter屏,增加cache后,在Android Studio 和Xcode下查看内存有明显降低。原则上在Image和FadeInImage的Widget设置cacheWidth和cacheHeight就可以解决我们的问题。然实际尝试中我们又遇到了刷新时图会出现闪烁的情况,在FadeInImage中需要修改Flutter SDK源码才能解决。对Image来说可以直接通过gaplessPlayback = true///Whethertocontinueshowingtheoldimage(true),orbrieflyshownothing(false),///whentheimageproviderchanges.finalboolgaplessPlayback;对于FadeInImage Widget即便是Image直接设置gaplessPlayback = true,在滑动或者刷新时依旧会闪烁。经过进一步调试,我们发现了对ResizeImage的是否相等的判断,官存在的个问题(didUpdateChange中判断是否同个widget导致)。我们可以通过在ResizeImage 源码中增加如下代码(官方1.22.4正式版和master,这个问题还未有这方面的修复)来解决。classResizeImageextendsImageProvider{///省略代码@overridebooloperator==(Objectother)=>identical(this,other)||otherisResizeImage&runtimeType==other.runtimeType&imageProvider==other.imageProvider&width==other.width&height==other.height;@overrideintgethashCode=>imageProvider.hashCode^width.hashCode^height.hashCode;}但是,源码本地改可以,总不能每个都在本地机器改吧,还有就是在部署Jenkins服务的打包机上同样需要修改。那怎么在Flutter SDK源码中添加我们的代码,同时保证每个开发者和每台打包机的环境都有效呢?# AOP方案(借助beike_aspectd)通过编译期对Flutter SDK中dart代码修改和增加代码等操作就可以实现自动地把现有业务中图片功能代码改掉。这里我们只修改两个官方的Widget逻辑:Image: 根据widget宽高设置 cacheWidth、cacheHeight和gaplessPlaybackFadeInImage: 除了设置cacheWidth、cacheHeight,还需要在ResizeImage中增加唯一性的逻辑。安利下,除了常规aop能力, beike_aspectd在修改final成员变量值、对命名构造函数中普通变量赋值等方面的能力也给了我们很大帮助。# 发现方案不足目前为止一切似乎都很完美,然后业务方App中页面有一张超大尺寸图,内存被瞬间拉高到1GB(如下图右侧),图片内存瞬间峰值问题在该场景下被放大。按照我们之前分析的结论:图片内存占用与设置的宽高大小成正比, 但现在的理论值与实际情况不符。然后我们寻找原因发现在image_decoder.cc中先直接decode 原始的image,此时消耗的内存就与图片原始尺寸成正比, 而后再做rasterize时这个时候的内存消耗大小才与设置大小成正比,因此上述方案并没有解决内存峰值的问题。https://github.com/flutter/engine/blob/v1.12.13-hotfixes/lib/ui/painting/image_decoder.cc总结案存在的问题:对客户端开发同学来说,cachedWidth、cacheHeight设置多少合适呢?与原图宽高例不致易出现图模糊、变形。图释放及时性问题(image可以通过evict式释放dart等引, 但是法保证引擎持有的SkImage释放时机)超图没法解决内存峰值问题(在1.12.13上图峰值如下图右侧,以及图影响ImageCache频繁触发释放)。首先在机拍照上传图片业务场景下,用户拍摄的图片像素太(如3000*4000),采样例法单纯地与页面widget宽来计算,设置太小会显示模糊(下图左侧):5 是什么影响图内存释放官有个图命周期介绍文档:DownwardMemoryPressure (PUBLICLY SHARED).pdf(http://note.youdao.com/s/N2lDU85C)当ui.Image (引擎层对应SkImage)可时,RenderImage将更新并把ui.Image绘制到Picture (引擎层对应SkPicture), 这个时候会增加SkImage的引计数(因此对ui.Image的dispose并不会完全释放全部的内存), PictureLayer(Picture)最终被ui.EngineLayers持有(如下图Layer Tree)。Flutter渲染流程如下图:6 最终解决案:图外接纹理在Flutter 端做的优化目前看来并没有能支撑我们解决图片引起的OOM问题,对于内存峰值和内存及时释放,Flutter 端都无法给出完美的方案。那么是否有一种方式将问题转移到Native端呢,答案是肯定的。Flutter 端提供了一种外接纹理的解决方案。我们先来看一下官方提供的外接纹理机制。纹理可以理解为GPU内代表图像数据的个对象。Flutter直接提供了Texture控件将纹理绘制到显示屏。图中红色的部分是我们要编写的Native代码,而黄色部分是Flutter Engine 内部的逻辑。整体的流程分为注册纹理和纹理渲染逻辑。# iOS实现案注册纹理:创建对象实现FlutterTextrure 接口,该对象来管理具体的纹理数据通过FlutterTextureRegister 来注册第一步的FlutterTexture对象,获取一个纹理id将该id通过channel机制传递给dart侧,dart侧就能够通过Texture这个widget来使用纹理了,参数就是id纹理渲染:dart 端声明一个Texture Widget,表明该wdiget 实际渲染的是Native提供的纹理Flutter Engine 拿到 layerTree,layerTree的Texturelayer节点负责外接纹理的渲染首先通过dart 传递的id,找到先注册的Flutter texture,该texture是我们用Native的代码来实现的,其核心实现了copyPixelBuffer方法Flutter engine调用copyPixelbuffer拿到具体的纹理数据,交给gpu渲染。# Android案图外接纹理案主要核点: 复用原生图片解码能力和缓存能力,控制图片下采样策略。Android端解码和缓存能可以直接Glide帮助我们轻松实现,我们只需要做好采样以及图裁剪缩放, 主要流程示意如下。先Flutter有提供个“Texture” Widget控件, 通过接收个已完成的纹理id,后进渲染显示。接下来就是如何成Flutter侧需要的纹理id:让原Glide按照配置,确定图采样,缓存策略。当拿到图后,我们需要根据要求做的裁剪,缩放等。通过FlutterEngine获取当前SurfaceTexture,后构建Surface并绘制bitmap,这样纹理绘制完成后,将纹理id返回给“Texture” Widget渲染。通过外接纹理的方案,我们成功地将Flutter 面临的图片内存问题引向了Native侧。图片内存的复用和缓存策略得到了解决,我们同样在dart代码提供了一个接口,让用户侧来决定是否需要通过自己的设置的imageView的宽高来进行降采样处理图片,这样图片的内存峰值问题也解决了。从目前我们线上的数据来看OOM得到很大的改善,我们对大图列表也分别对iOS安卓进行了内存监控。数据也得到了明显改善。iOS 端采集的数据如下图:安卓端的数据对比:屏幕显示效果与原对图效果基本致(Flutter-OPPO R17 vs 原-魅族16S)从数据采集情况可以看到,内存的使用情况得到了明显的改善。也证明外接纹理方案的可行性。在整个方案的实施过程中,我们踩了很多坑,比如iOS端遇到的多线程引发的crash问题,Android端的重复reply导致crash问题,这些大家可以参考我们的代码,从中去了解我们的整体方案以及其中的细节。7 未来规划开源图外接纹理案。原生和Dart层资源共享, channel案本身还是需要涉及图Flutter engine解码。探索视频纹理案,寻找解决Flutter端PlatformView视频播放引起内存问题的方案。8 参考资料Flutter开发档https://github.com/flutter/flutterFlutter引擎档https://github.com/flutter/engineAliFlutter图片解决方案与优化https://developer.aliyun.com/article/765702 预览时标签不可点 Flutter15移动端37大前端69Flutter · 目录#Flutter上一篇Flutter UI自动化原理与实践下一篇贝壳Flutter动态化原理与实践关闭更多小程序广告搜索「undefined」网络结果
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-3 01:11 , Processed in 0.527098 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表