背景:去年,闲鱼技术团队的新一代图片库PowerImage经过一系列的灰度、问题修复、代码优化,已经稳定应用于闲鱼。相比上一代IFImage,PowerImage进行了进一步的进化,适应了更多的业务场景和最新的颤振特性,解决了一系列痛点:比如因为完全抛弃了原生ImageCache,在与原生图像混合的场景中,一些低频图像反而会占用缓存;比如我们不能在模拟器上显示图片;比如我们在相册的时候,需要在图片库之外建立一个图片通道。
简介PowerImage是一个充分利用原生图像库,具有高扩展性的flutter图像库。我们巧妙地将外部纹理与ffi方案相结合,更加贴近原生设计,解决了一系列业务痛点。
特性支持加载ui.Image的能力,在基于外部纹理的方案中,用户无法获得真实的ui。图片来使用,这让图片库在这种特殊的使用场景下很无奈。
支持图像预加载能力。就像原生的前化学图像一样。这在一些画面显示速度较高的场景中非常有用。
添加纹理缓存,与原生图像库缓存打通!统一的图像缓存,以避免因混合原生图像而导致的内存问题。
支持模拟器。在flutter-1.23.0-18.1.pre之前,模拟器无法显示纹理小部件。
改进自定义图像类型通道。解决业务自定义图片获取请求。
完美的异常捕获和收集。
支持动画。(公关自淘特)
颤振原生方案在介绍新方案之前,简单回忆一下颤振原生图片方案。
原生图像小部件首先通过ImageProvider获取ImageStream,通过监控其状态来显示各种状态。例如,frameBuilder和loadingBuilder最终会在图像加载成功后重新构建RawImage,而RawImage将由RenderImage绘制。整个图的核心是ui。ImageInfo中的图像。
Image:负责显示图像加载的各种状态,如加载、失败、加载成功等。
Image provider:负责获取ImageStream,比如系统内置的NetworkImage和AssetImage。
Imagestream:图像资源加载的对象。
梳理了一下flutter的原生图片方案,发现在某个时候有机会以原生方式获取flutter图片和原生通过?
在新一代中,我们巧妙地将FFi方案与外部纹理方案相结合,解决了一系列业务痛点。
FFI如开头所说,有些事情是纹理方案做不到的,需要其他方案来补充,而其中的核心就是ui。Image我们将原生内存地址、长度等信息传递给flutter,用于生成ui。图像
首先原生端先获取必要的参数(以iOS为例):
_ row bytes=CGImageGetBytesPerRow(CG image);CGDataProviderRef data provider=cgimagegetdata provider(CG image);CFDataRef rawDataRef=CGDataProviderCopyData(data provider);_ handle=(long)CFDataGetBytePtr(rawDataRef);ns data * data=CFBridgingRelease(rawDataRef);self.data=数据;_长度=data.lengthdart得到它后,
@ override futureimageinfo create imageinfo(Map Map){ CompleterImageInfo completer=CompleterImageInfo();int handle=map[‘ handle ‘];int length=map[‘ length ‘];int width=map[‘ width ‘];int height=map[‘ height ‘];int row bytes=map[‘ row bytes ‘];用户界面.格式像素格式=ui .像素格式。值[map[‘ flutter pixel format ‘]?0];指针8指针=指针发件人地址(句柄);uint 8列表像素=指针。a类型化列表(长度);ui.decodeImageFromPixels(像素,宽度,高度,像素格式,(ui .image image){ ImageInfo ImageInfo=ImageInfo(image : image);完成者。完成(imageInfo);//释放当地的内存强大的图像加载器。实例。发布图像请求(选项);},行字节:行字节);返回completer.future}我们可以通过致死性家族性失眠症拿到当地的内存,从而生成用户界面.图片。这里有个问题,虽然通过致死性家族性失眠症能直接获取当地的内存,但是由于decodeImageFromPixels会有内存拷贝,在拷贝解码后的图片数据时,内存峰值会更加严重。
这里有两个优化方向:
1.解码前的图片数据给颤动,由摆动提供的解码器解码,从而削减内存拷贝峰值。
2.与摆动官方讨论,尝试从内部减少这次内存拷贝。
自由流体指数这种方式适合轻度使用、特殊场景使用,支持这种方式可以解决无法获取用户界面.图像的问题,也可以在模拟器上展示图片(颤振=1.23.0-18.1.pre),并且图片缓存将完全交给图片缓存管理。
纹理方案与原生结合有一些难度,这里涉及到没有用户界面.图像只有textureId。这里有几个问题需要解决:
问题一:图像小部件需要用户界面.图像去构建原始图像从而绘制,这在本文前面的摆动原生方案介绍中也提到了;问题二:图像缓存依赖图像信息中用户界面.图像的宽高进行躲藏大小计算以及缓存前的校验;问题三:本机侧纹理生命周期管理。分别都有解决方案:
问题一:通过自定义图像解决,透出图像生成器来让外部自定义图片小部件
问题二:为纹理自定义ui.image,如下:
导入”dart : typed _ data”;将” dart:ui “作为用户界面显示图像导入;导入”dart : ui”;类纹理图像实现用户界面.image { int _ width int _ height int textureIdTextureImage(this . texture id,int width,int height) : _width=width,_ height=height @ override void dispose(){//todo : implement dispose } @ override int get height=_ height;@ override FutureByteData toByteData({ ImageByteFormat format=ImageByteFormat。raw rgba }){//todo : implemented toByteDatathrow implemented error();} @ overrideint get width=_ width}这样的话,纹理图像实际上就是个壳,仅仅用来计算躲藏大小。实际上,图像缓存计算大小,完全没必要直接接触到用户界面.图像,可以直接找图像信息取,这样的话就没有这个问题了。
问题三:关于当地的侧感知颤动图像释放时机的问题。
修改的图片缓存释放如下(部分代码):
typedef void HasRemovedCallback(动态键,动态值);类RemoveAwareMapK,V实现MapK,V { HasRemovedCallback HasRemovedCallback;}//- final RemoveAwareMapObject,_ pending image _ pending images=RemoveAwareMapObject,_ pending image();//- void hasImageRemovedCallback(动态键,动态值){如果(键为ImageProviderExt){ waitingbecheckedkeys。添加(键);} if(isscheduledimagestuscheck)返回;isscheduledimagestuscheck=true;//我们应该在微任务中进行检查以避免图像被删除,并立即添加调度微任务((){ waitingbecheckedkeys。foreach((key){ if(!_pendingImages.containsKey(key)!_cache.containsKey(key)!_实时图像。包含key(key)){ if(key为ImageProviderExt){ key。dispose();} } });waitingbecheckedkeys。clear();isscheduledimagestuscheck=false;});}整体架构我们将两种解决方案非常优雅地结合在了一起:
010-350000我们抽象了PowerImageProvider。对于外部(ffi)和纹理,我们可以分别产生自己的ImageInfo。它将通过调用PowerImageLoader提供统一的加载和释放功能。
实线ImageExt是自定义图像小部件,它以纹理的方式显示imageBuilder。
虚线ImageCacheExt是ImageCache的扩展,只在Flutter 2 . 2 . 0版本中需要,它会为ImageCache的发布时间提供回调。
这一次,我们还设计了超级扩展能力。除了支持网络地图、本地图、flutter资源、原生资源,我们还提供了自定义图像类型的通道。flutter可以将任何自定义的参数组合传递给native。只要native注册了相应的类型加载器,比如“album”,用户就可以自定义imageType到album,native使用自己的逻辑加载图片。有了这个自定义通道,甚至连图像滤镜都可以通过PowerImage显示和刷新。
除了图片类型的扩展,渲染类型也可以自定义。比如上面ffi中提到的,为了减少内存拷贝带来的峰值问题,用户可以在颤振侧解码。当然,这需要原生图像库在解码前提供数据。
数据FFI vs纹理010-350000型号:iPhone 11 Pro;图片:300张网络图;行为:在listView中手动滚动到底部,然后滚动到顶部;本机缓存:20 maxMemoryCountflutter缓存:30MBflutter版本2 . 5 . 3;在发布模式下有两种现象:
FFI: 186MB波动纹理:194MB波动在2.5.3版本中,纹理方案和FFI在内存级别上差别不大,内存波动与flutter 1.22结论相反。
打开checkerboardRasterCacheImages后显示中国棋盘图。可以看出,ffi方案会缓存整个单元格,而在texture方案中,只缓存单元格中的单词。RasterCache会让ffi在流畅度上有一定优势。
滚动流畅度分析010-350000设备:安卓一加8t,CPU和GPU都锁频。Case: GridView每排4张图片,300张图片,从上到下,再从下到上,滑动范围从500,1000,1500,2000,2500,5个轮子。重复20次。{ 1 }中I的模式:20};do颤振drive-target=test _ driver/app . dart-profile;Done运行数据,获取时间线数据并对其进行分析。结论:
Uithread是耗时最好的纹理模式,PowerImage略好于IFImage,FFI模式波动较大。
光栅线程需要时间。PowerImage比IFImage好。Origin native模式很好,因为图像是调整大小的,其他模式加载原始图像。
精简代码010-350000dart侧码大幅减少,这是由于技术方案贴合了flutter原生设计,我们分享了更多有原生图片的代码。
FFI方案补充了外部纹理的不足,遵循了原生图像的设计规范,不仅让我们享受到了ImageCache带来的统一管理,还带来了更加精简的代码。
单测010-350000为了保证核心代码的稳定性,我们有比较完善的单测,行覆盖率接近95%。
关于开源,我们期待通过社区的力量让PowerImage变得更加完善和强大,希望PowerImage能在工程研发上给大家带来好处。
问题关于问题,我们希望您在使用PowerImage遇到问题和需求时,能够积极与对方沟通,在提出问题时提供尽可能详细的信息,以减少沟通成本。在提出问题之前,请确保您已阅读自述文件。
010-350000对于Bug的问题,我们定制了模板(Bug report),可以很方便的填写一些必要的信息。其他类型可以选择开空白问题。
我们每周会花一些时间统一处理问题,也期待你和PR的讨论。
为了保持PowerImage核心功能的稳定性,PR进行了完善的单项测试,线路覆盖率达到95%(power_image库)。
提交PR时,请确保提交的代码被单个测试覆盖,同时提交涉及的单个测试代码。
010-350000得益于Github的Actions能力,当我们推送主分支的代码,对主分支进行PR操作时,会触发颤振测试任务,只有单次测试通过才能关闭。
开源是未来PowerImage的开始,而不是结束。PowerImage可以做的事情很多,很有趣,也很丰富。比如如何实现第一期描述的loadingBuilder?比如ffi方案如何支持动画?像科特林和斯威夫特.
PowerImage将在未来继续发展。在当前纹理方案和ffi方案并存的情况下,随着颤振本身的迭代,我们会更倾向于向ffi发展。如上对比,ffi方案自然可以享受光栅缓存带来的流畅度优势。
PowerImage将继续跟随flutter的脚步,始终符合最初的设计理念,不断进步。我们希望有更多的学生加入进来,共同成长。
其他四个颤振开源项目:闲鱼科技微信官方账号-闲鱼开源
PowerImageGitHub:(星形)
https://github.com/alibaba/power_image
扑扑pub:(喜欢)
https://pub.dev/packages/power_image