其他
细化 Flutter List 内存回收,解决大 Cell 问题
重构业务层代码,把图片分散在多个 Cell 里。但是因为缺乏高度信息,Cell 仍然会一次性全部出现,带来内存问题。
细化 Flutter List 的回收能力,在 Cell 回收的基础上,可以做到以图片为单位进行回收。
方案探索过程
▐ 绘制图片的坐标信息
2020-02-06 Runner[45049:2962074] flutter: [AA] Render offset: Offset(0.0, 74.4)
2020-02-06 Runner[45049:2962074] flutter: [AA] Render offset: Offset(0.0, 449.4)
2020-02-06 Runner[45049:2962074] flutter: [AA] Render offset: Offset(0.0, 824.4)
2020-02-06 Runner[45049:2962074] flutter: [AA] Render offset: Offset(0.0, 1199.4)
2020-02-06 Runner[45049:2962074] flutter: [AA] Render offset: Offset(0.0, 1574.4)
....
▐ 提根据坐标判断图片是否在屏幕内
void paint(PaintingContext context, Offset offset) {
// Check if Rect(offset & size) intersects with screen bounds.
final double screenWidth = ui.window.physicalSize.width / ui.window.devicePixelRatio;
final double screenHeight = ui.window.physicalSize.height / ui.window.devicePixelRatio;
if (offset.dy >= screenHeight - 1 || offset.dy <= -size.height + 1 ||
offset.dx >= screenWidth - 1 || offset.dx <= -size.width + 1) {
// 在屏幕外
}
....
}
▐ 强制每帧重新绘制该 Cell
// Class SliverChildBuilderDelegate
/// Whether to wrap each child in a [RepaintBoundary].
///
/// Typically, children in a scrolling container are wrapped in repaint
/// boundaries so that they do not need to be repainted as the list scrolls.
/// If the children are easy to repaint (e.g., solid color blocks or a short
/// snippet of text), it might be more efficient to not add a repaint boundary
/// and simply repaint the children during scrolling.
///
/// Defaults to true.
final bool addRepaintBoundaries;
/// A widget that tells sliver not to create repaint boundary for a cell content.
abstract class NoRepaintBoundaryHint {
}
if (addRepaintBoundaries && (child is! NoRepaintBoundaryHint)) {
child = RepaintBoundary(child: child);
}
class MyListItem extends StatefulWidget implements NoRepaintBoundaryHint {
}
▐ 添加通知进行图片加载与回收
/// On drawing image, AutoreleaseRenderImage will notify image moving inside or outside screen event to owner.
typedef SetNeedsImageCallback = void Function(bool value);
在入屏时,调用 SetNeedsImageCallback(true),重新请求图片。代码大致如下(省略了一部分):
// Class _ImageState
void didChangeDependencies() {
_updateInvertColors();
if (_releaseImageWhenOutsideScreen) {
return; // 如果有标记,不再加载图片,等待绘制指令
}
.... 请求图片
super.didChangeDependencies();
}
void __setNeedsImage(bool value) {
if (value) {
if (_imageStream == null) {
请求图片
}
}
else {
清空图片
}
}
void _setNeedsImage(bool value) { // AutoreleaseRenderImage 回调该方法
Future<void>(() {
__setNeedsImage(value); // 在 paint 过程,不允许 setState,所以需要异步一下
});
}
▐ Demo 测试运行
Widget build(BuildContext context) {
if (widget.index % 10 == 0) {
final images = <Widget>[];
for (var i = 0; i < 10; i++) {
images.add(new Image.external_adapter(
'https://i.picsum.photos/id/' + (widget.index + i).toString() + '/1000/1000.jpg',
height: 375,
width: 375,
));
}
return Column(
children: images
);
}
else {
return Container(
width: 375,
height: 375,
child: Text(widget.index.toString()),
);
}
}
▐ 真实业务场景测试
// Class RenderImage
Size _sizeForConstraints(BoxConstraints constraints) {
constraints = BoxConstraints.tightFor(
width: _width, // 为 null
height: _height, // 为 null
).enforce(constraints);
if (_image == null)
return constraints.smallest; // 图片也没有加载完成时,该 Widget 根本没有尺寸
return constraints.constrainSizeAndAttemptToPreserveAspectRatio(Size(
_image.width.toDouble() / _scale,
_image.height.toDouble() / _scale,
));
}
图片不存在,无法排版,无法显示。
加载图片,导致本应在屏幕外的图片纹理全部上传到 GPU;然后才能完成排版,再次绘制时发现在屏幕外,再删除纹理。
▐ 提前获取图片尺寸
// Class _ImageState
void didChangeDependencies() {
if (_releaseImageWhenOutsideScreen) {
if (widget.width == null || widget.height == null) {
_resolveImage(true); // 只获取图片尺寸,不上传纹理
_listenToStream();
}
}
.... 以下略
}
void _handleImageInfo(int width, int height, int frameCount, int durationInMs, int repetitionCount) {
setState(() { // 获取到图片尺寸后,记录下来,并更新给 RenderObject
_imageWidth = width;
_imageHeight = height;
});
}
在获取到图片尺寸后,记录下来,并通过 setState 告知给 AutoreleaseRenderImage。
Size _sizeForConstraints(BoxConstraints constraints) {
constraints = BoxConstraints.tightFor(
width: _width,
height: _height,
).enforce(constraints);
// No intrinsic from image itself or image pixel dimension info.
if (_image == null && (_imageWidth == null || _imageHeight == null))
return constraints.smallest;
// Use _image if not null
if (_image != null) {
return constraints.constrainSizeAndAttemptToPreserveAspectRatio(Size(
_image.width.toDouble() / _scale,
_image.height.toDouble() / _scale,
));
}
// Or else use image dimension info.
return constraints.constrainSizeAndAttemptToPreserveAspectRatio(Size(
_imageWidth.toDouble(),
_imageHeight.toDouble(),
));
}
▐ 进一步优化
当 Image Widget 拥有确定宽、高时,依赖绘制阶段的在屏判断进行图片加载。
当 Image Widget 缺失宽、高信息时,如果有排版的虚拟尺寸,以这个虚拟尺寸进行预排版。排版后首次绘制时,如果在屏,进行图片真正加载。图片加载完成后,如果尺寸与虚拟尺寸不符合,会重新排版。
▐ 效果
总结
END