背景
开发图片、视频相关功能时,相册是一个绕不开的话题,因为大家基本都有从相册获取图片或者视频的需求。最直接的方式是调用系统相册接口,虽然基本功能是满足的,却无法满足一些高级功能,例如自定义UI、多选图片等。
设计思路
闲鱼这套相册组件API使用简单,功能丰富灵活,具有较高的订制性。业务方可以选择完全接入组件,也可以选择在组件上面进行UI定制。
Flutter做UI展现层,具体的数据由各Native平台提供。这种模式,天然从工程上把UI代码和数据代码进行了隔离。我们在开发一个native组件的时候常常会使用MVC架构。Flutter组件的开发的思路也基本类似。整体架构如下:
可以看出,在Flutter侧是一个典型的MVC架构,这里Widget就是View,View和Model绑定,在Model改变的时候View会重新build反映出Model的变化。View的事件会触发Controller去Native获取数据然后更新Model。Native和Flutter通过Method Channel进行通信,两层之间没有强依赖关系,只需要按约定的协议进行通信即可。
Native侧的组成部分,UIAdapter主要是负责机型的适配、刘海屏、全面屏之类的识别。Permission负责媒体读写权限的申请处理。Cache主要负责缓存GPU纹理,在大图预览的时候提高响应速度。Decoder负责解析Bitmap,OpenGL负责Bitmap转纹理。
需要说明的是:我们的这一套实现依赖于flutter外接纹理。在整个相册组件看到的大多数图片都是一个GPU纹理,这样给java堆内存的占用相对于以前的相册实现有大幅的降低。在低端机上面如果使用原生的系统相册,由于内存的原因,app有被系统杀掉的风险。现象就是,从系统相册返回,app重新启动了。使用Flutter相册组件,在低端机上面体验会有所改观。
一些难点
分页加载
相册列表需要加载大量图片,Flutter的GridView组件有好几个构造函数,比较容易犯的错误是使用了第一个函数,这需要在一开始就提供大量的widget。应该选择第二个构造函数,GridView在滑动的时候会回调IndexedWidgetBuilder来获取widget,相当于一种懒加载。
GridView.builder({...List children = const [],...})GridView.builder({...@required IndexedWidgetBuilder itemBuilder,int itemCount,...})
滑动过程中,图片滑过后,也就是不可见的时候要进行资源的回收,我们这里这里对应的就是纹理的删除。不断的滑动GridView,内存在上升后会处于稳定,不会一直增长。如果快速的来回滑动纹理会反复的创建和删除,这样会有内存的抖动,体验不是很好。
于是,我们维护了一个图片的状态机,状态包括None,Loading,Loaded,Wait_Dispose,Disposed。开始加载的时候,状态从None进入Loading,这个时候用户看到的是空白或者是占位图,当数据回调回来会把状态设置为Loaded的这时候会重新build widget树来显示图片icon,当用户滑走的时候状态进入 Wait_Dispose,这时候并不会马上Dispose,如果用户又滑回来则会从Wait_Dispose进入Loaded状态,不会继续Dispose。如果用户没有往回滑则会从Wait_Dispose进入Disposed状态。当进入Disposed状态后,再需要显示该图片的时候就需要重新走加载流程了。
相册大图展示
当点击GridView的某张图片的时候会进行这张图片的大图展示,方便用户查看的更清楚。我们知道相机拍摄的图片分辨率都是很高的,如果完全加载,内存会有很大的开销,所以我们在Decode Bitmap的时候进行了缩放,最高只到1080p。Android原生的Bitmap Decode经验同样适用,先Decode出Bitmap的宽高,然后根据要展示的大小计算出缩放倍数, 然后Decode出需要的Bitmap。
Android相册的图片大多是有旋转角度的,如果不处理直接显示,会出现照片旋转90度的问题,所以需要对Bitmap进行旋转,采用Matrix旋转一张1080p的图片在我的测试机器上面大概需要200ms,如果使用OpenGL的纹理坐标进行旋转,大于只需要10ms左右,所以采用OpenGl进行纹理的旋转是一个较好的选择。
在进行大图预览的时候会进入一个水平滑动的PageView,Flutter的PageView一般来说是不会去主动加载相邻的page的。这里有一个取巧的办法,对于PageController的viewportFraction参数我们可以设置成为0.9999,如下所示:
PageController(viewportFraction=0.9999)
还有另外一种办法,就是在Native侧做预加载。例如:在加载第5张图片的时候,相邻的4,6的图片纹理提前进行加载,当滑动到4,6的时候直接使用缓存的纹理。
内存
相册图片使用GPU纹理,会大幅减少Java堆内存的占用,对整个app的性能有一定的提升。需要注意的是,GPU的内存是有限的需要在使用完毕后及时删除,不然会有内存的泄漏的风险。另外,在Android平台删除纹理的时候需要保证在GPU线程进行,不然删除是没有效果的。
在华为P8,Android5.0上面进行了对比测试,Flutter相册和原native相册总内存占用基本一致,在GridView列表页面,新增最大内存13M左右。它们的区别在于原native相册使用的是Java堆内存,Flutter相册使用的是Native内存。
总结
这套相册组件API简单、易用,高度可定制。Flutter侧层次分明,有UI订制需求的可以重写Widget来达到目的。另外这是一个不依赖于系统相册的相册组件,自身是完备的,能够和现有的app保持UI、交互的一致性。同时为后面支持更多和相册相关的玩法打好基础。
作者:闲鱼技术 链接:https://juejin.im/post/5d13560ce51d4555e372a629