Flutter瘦内存瘦包之图片组件

背景

在电商类APP里,图片到现在为止仍然是最重要的信息承载媒介,不得不说逛淘宝的过程,其实就是一个看图片的过程。而商品详情页中的图片,通常是页面中内存占用最多的内容,占用了整个页面内存的超过 50%。
闲鱼在Flutter化的过程中,选择了商品详情页作为第一个落地的场景。通过多版本的迭代完善,基于Flutter的详情页已经在闲鱼稳定运行。然而正因为详情页的图片量大,导致Flutter里图片相关的问题一直挥之不去。

1:内存问题       ---   连续push flutter界面内存累积

2:安装包问题    ---   过渡时期两份重复资源文件。

3:寻址缓存问题 ---   原有的寻址缓存策略无法复用。

4:图片复用问题 ---   Native和Flutter重复下载相同图片。

解决方案----FXTexImage_V1

为了解决这些问题,我们尝试着寻找一种新的思路,一种能够将flutter与native串联起来的思路。而之前做视频播放器的方案给了我们启发。

熟悉Flutter的同学应该都知道,Flutter的视频组件是基于一个Flutter提供的一个叫“外接纹理”的技术实现的,关于flutter外接纹理,本人另外有一篇文章有更详细的论述,这里不再赘述。
https://mp.weixin.qq.com/s/KkCsBvnRayvpXdI35J3fnw

我们将每一张图片假想成一个:静态的视频。图片的内容由一个external_texture来负责显示,而这个external_texture则由native端提供具体的渲染数据。

image.png

通过这种方案,我们便可以通过external_texture这座桥梁,将flutter作为native端图片的一个最终展示场所。而所有的下载、缓存、裁剪等逻辑都可以复用原来的native图片库。

基于这个基本框架,我们形成了我们第一版本的图片渲染组件:FXTexImage----V1。这个组件很好的解决了Flutter引入的安装包、图片缓存、图片复用等问题。

但是图片最大的问题:内存问题,并没有得到解决。

内存优化----FXTexImage_V2

为了用户体验,通常会有连续push若干个界面的场景(比如闲鱼的详情页,点击底部的推荐列表,可以一直往下push新的详情页),这种场景下,每一个界面都有大量的图片展示。所以在引入flutter以后,闲鱼在iPhone 6P等机型上通常只能push10个左右详情页就挂了。

在考虑到在显示过程中,真正用户可见的页面,其实只有当前栈顶的两个页面,基于这个特征我们就做了优化逻辑:

1:在push详情页过程中,我们只保留了当前展示页和当前页的前一页的图片资源,而之前的资源全部都做了释放(只是图片资源的释放,整个页面还有页面中的其他元素还是做了保留)。

2:为了做到用户无感知,我们在pop过程中,会预先去加载当前界面下一个界面的图片资源。

插图2.png

通过这种方式,理论上我们可以释放掉不可见的资源,从而保证在持续Push界面过程中内存缓慢增长,但是实践过程中发现内存仍然持续增长。

经过排查,我们发现flutter 1.0版本以及0.8.2版本里,SurfaceTextureRegistry提供了release方法,这里将会把创建的SurfaceTexture进行释放。然而测试过程中发现,单单对SurfaceTexture释放,并没有完全释放内存,当反复创建对象时仍然会闪退。为此,我们在AndroidExternalTextureGL的析构函数中增加了纹理的释放glDeleteTextures逻辑。

然而,AndroidExternalTextureGL的析构是在flutter的GPU线程调用的,而external_texture的release方法通常是在主线程,也就是PlatForm线程调用的。不同线程调用的问题就是会导致一个诡异的问题:

插图3.png

推测是不同线程释放的逻辑影响了GL环境,导致文字渲染出了问题。

所以,为了解决该问题,我们删除了SurfaceTextureRegistry的release方法里面SufaceTexture的释放逻辑,并且将SurfaceTexture的释放,放到AndroidExternalTextureGL析构阶段,通过Jni调用java方法实现资源释放。

AndroidExternalTextureGL::~AndroidExternalTextureGL(){   if (state_ == AttachmentState::attached) {
      Detach();      if (texture_name_ != 0)
      {
          glDeleteTextures(1, &texture_name_);
          texture_name_ = 0;
      }
    }
   Release();
   state_ = AttachmentState::detached;
}
void AndroidExternalTextureGL::Release() {
    JNIEnv* env = fml::jni::AttachCurrentThread();
    fml::jni::ScopedJavaLocalRef<jobject> surfaceTexture =
    surface_texture_.get(env);    if (!surfaceTexture.is_null()) {
        SurfaceTextureRelease(env, surfaceTexture.obj());
    }
}
void SurfaceTextureRelease(JNIEnv* env, jobject obj) {
   env->CallVoidMethod(obj, g_release_method);
   FML_CHECK(CheckException(env));
}
  g_release_method = env->GetMethodID(g_surface_texture_class->obj(), release, ()V);

CPU优化----FXTexImage_V3

通过外界纹理渲染图片+不可见页面资源释放,我们解决了上述提出的一系列问题,但是又引入了新的问题:CPU偏高,滑动帧率偏低。通过测试,在详情页滑动过程中,IOS和Android的CPU都比Flutter原生组件高10%以上,这个显然无法应用。

经过排查,发现CPU高的原因是:

IOS端: iOS的IOSExternalTextureGL模型是一个拉数据的模型,native端register一个CVPixelBuffer的生产者,当需要绘制时,都会调用一次这个生产者的copyPixelbuffer方法去拉一次数据。然后将拉到的CVPixelBuffer对象转换成GPU Texture。这里每一次转换都换造成CPU较大开销。

并且这种拉数据的机制就要求这个生产者的必须一直保留着这个CVPixelBuffer对象(否则界面重刷以后,图片区域就显示白屏)。

Android端: android 的数据存储在SurfaceTexture中。每一次external_texture  layer需要绘制时候都会从SurfaceTexture中去update 数据到纹理中,由于SurfaceTexture使用基于EGLImage共享内存,所以虽然没有双份内存的问题,但是每一次update 都会带来较大的CPU开销。

在之前外接纹理的文章中,我们提出了一种新的基于共享上下文的外接纹理方案。并在我们视频的拍摄和编辑中得到了很好的应用。该方案当初提出来,是为了解决视频数据从CPU -> GPU -> CPU -> GPU 输送的问题而提出来的。

但是在图片这个场景下, 新的外接纹理方案下,一张图片在native端加载完成以后,立刻被转换成一个OpenGL的Texture,然后图片的资源马上被释放。当界面刷新时,对于同一张图片的重新渲染,IOSExternalTextureGL不需要再去做将数据(CVPixelBuffer或者SurfaceTexture)转换到Texture的逻辑,而是直接使用之前创建好的Texture。

经过这一步优化,我们很好的限制了iOS的CPU和内存,Android的CPU。

通过测试对比,V3版本的图片组件,相比于Flutter原生图片组件,在详情页正常滑动操作过程中,平均CPU高出3%左右,虽然仍差于原生组件,单相对是可以接受的。

结果

内存:   基于新图片组件,我们很好的限制住了连续push 下的内存增长速度,顺利的将iPhone 6P上的详情页push 最大数量从10个增加到了30个以上

在同一线上版本中,我们通过控制ABTest开关,测试新的图片渲染方案和Flutter自带图片组件方案的Abort率,发现新图片组件下闲鱼的Abort率降低20%

安装包: 新组件下,所有的资源组件与原来native资源共用,所有flutter期间新引入的资源出了gif图,全部删除,减少安装包900k+。并且后续可以不用继续新增

寻址策略:  复用native图片组件,基于阿里系自己的图片下载组件,这样可以做到随着集团组件升级版本,兼容版本过程中各种新的寻址方式和图片格式。

图片复用:复用native图片组件,当图片地址命中缓存,可直接缓存加载,尺寸不一致时可以预先返回缓存图同时加载大图,这样大大增强详情页大图预览的浏览体验。

遗留问题

图片组件已经在闲鱼上全量部署,然而还是有一些问题没有得到很好的解决,上文提到过CPU比原生图片组件高3%左右,虽然用户没有感官体验,但是还是有优化空间。

还有就是Flutter针对ExternalTexture的纹理渲染时没有开启抗锯齿,导致小图在大区域渲染时比原生组件效果要差。这里还需要继续排查原因。

最后,FXTexImage组件还在持续优化中,当解决上述遗留问题以后便会在Github上开源。

作者:闲鱼技术-炉军

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


这篇文章主要讲解了“FlutterComponent动画的显和隐怎么实现”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究...
这篇文章主要讲解了“flutter微信聊天输入框功能如何实现”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“f...
本篇内容介绍了“Flutter之Navigator的高级用法有哪些”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处...
这篇文章主要介绍“Flutter怎么使用Android原生播放器”,在日常操作中,相信很多人在Flutter怎么使用Android原生播放器问题上存在疑惑,小编查阅了各式资料,整...
Flutter开发的android端如何修改APP名称,logo,版本号,具体的操作步骤:修改APP名称找到文件:android\\app\\src\\main\\AndroidManifest.xml
Flutter路由管理初识路由概念一.路由管理1.1.Route1.2.MaterialPageRoute1.3.Navigator1.4.路由传值1.5 命名路由1.6.命名路由参数传递1.7.适配二、路由钩子三、onUnknownRoute四、结尾初识路由概念路由的概念由来已久,包括网络路由、后端路由,到现在广为流行的前端路由。无论路由的概念如何应用,它的核心是一个路由映射表。比如:名字 detail 映射到 DetailPage 页面等。有了这个映射表之后,我们就可以方便的根据名字来完成路由的转发
前提:针对Android开发者(windows系统下),已安装Git,AndroidStudio(建议4.0+版本)一.下载Flutter SDK地址:https://flutter.dev/docs/development/tools/sdk/releases,在 Stable channel (Windows)里面下最新版本即可。Flutter的渠道版本会不停变动,请以Flutter官网为准。在中国,要想正常获取安装包列表或下载安装包,可能需要翻墙,也可以去Flutter github项目下去下载安
一、变量变量是一个引用,根据Dart中“万物皆对象”原则,即变量存储的都是对象的引用,或者说它们都是指向对象。1.1.声明变量://1.不指定类型var name = 'aaa';//2.明确指定类型String name = 'aaa';因为有类型推导,所以两种实现效果一样,官方推荐在函数内的本地变量尽量使用var声明。在变量类型并不明确的情况下,可以使用dynamic关键字//3.使用dynamic关键字dynamic name = 'aaa';1.2.默认值未初始化的变量
前言Flutter2.0发布不久,对web的支持刚刚进入stable阶段。初学几天,构建web应用时候碰到一些问题,比如中文显示成乱码,然后加载图片出现图片跨域问题:Failed to load network image...Trying to load an image from another domain?1.开启web端构建:使用下面这个命令才可以开启Web端构建的支持flutter config --enable-web提示我们:重新启动编辑器,以便它们读取新设置。2.重
一.Flutter打Android release包的步骤:1.为项目创建一个.jks签名文件(很简单,跳过)2.创建一个文件key.properties,直接复制下面key.properties位置如图:在里面输入一下内容:storePassword=iflytekkeyPassword=iflytekkeyAlias=teachingmachinestoreFile=E:/teacher/app/keys/TeachingMachine.jks输入你自己的passwork以及
1 问题Android原生向js发消息,并且可以携带数据2 实现原理Android原生可以使用RCTEventEmitter来注册事件,然后这里需要指定事件的名字,然后在js那端进行监听同样事件的名字监听,就可以收到消息得到数据Android注册关键代码reactContext.getJSModule(DeviceEventManagerModule.RCT...
1 Flexbox布局1) flexDirection 可以决定布局的主轴,子元素是应该沿着水平轴(row)方向排列,还是沿着竖直轴(column)方向排列2) justifyContent 决定其子元素沿着次轴(与主轴垂直的轴,比如若主轴方向为row,则次轴方向为column)的排列方式 有flex-start、center、flex-end、space-around...
1 实现的功能在网上看React Native文档,我特码就想实现一个页面到另外一个页面的跳转,然后另外一个页面怎么获取参数,特么没找到一个说清楚的,要么太复杂,要么说了不理解,下面是我自己写的一个App.js文件,实现一个Home页面跳到另外Details页面,并且携带了参数怎么在Details页面获取,就是这么简单粗暴.2 测试DemoApp.js文件如下...
1 问题在一个文件构建一个对象,然后在另外一个文件里面new这个对象,通过构造方法传递参数,然后再获取这个参数2 测试代码Student.js文件如下'use strict';import React from 'react'import {NativeModules, NativeEventEmitter, DeviceEventEmitter,Ale...
1 简单部分代码export default class App extends Component&amp;lt;Props&amp;gt; { render() { return ( &amp;lt;View {styles.container}&amp;gt; &amp;lt;View {styles.welcome}&amp;gt; &amp;l...
1 怎么实现发送和接收事件理论上封装了Android原生广播的代码,需要注册和反注册,这里用DeviceEventEmitter实现//增加监听DeviceEventEmitter.addListener//取消监听//this.emitter.remove();这里可也可以通过安卓原生向页面js发送消息,可以参考我的这篇博客React Native之Android原生通过Dev...
1、Component介绍一般Component需要被其它类进行继承,Component和Android一样,也有生命周期英文图片如下2 具体说明1)、挂载阶段constructor()//构造函数,声明之前先调用super(props)componentWillMount()//因为它发生在render()方法前,因此在该方法内同步设置状态...
1 触摸事件普通点击我们可以使用onPress方法,我们可以使用Touchable 系列控件设计我们的按钮TouchableHighlight 背景会在用户手指按下时变暗TouchableNativeFeedback用户手指按下时形成类似墨水涟漪的视觉效果TouchableOpacity指按下时降低按钮的透明度,而不会改变背景的颜色TouchableWithoutFeedbac...
1 问题部分代码如下class HomeScreen extends React.Component { render() { return ( &amp;lt;View {{ flex: 1, alignItems: 'center', justifyContent: 'center' }}&amp;gt; &amp;lt;Text&amp;gt;Home Scre...
1 Props(属性)和State(状态)和简单样式简单使用App.js代码如下/** * Sample React Native App * https://github.com/facebook/react-native * * @format * @flow */import React, {Component} from 'react';import {Pla...