在没有 OOM 的情况下,在 Flutter 中加载手机图库的速度与原生 IOS 一样快
Posted
技术标签:
【中文标题】在没有 OOM 的情况下,在 Flutter 中加载手机图库的速度与原生 IOS 一样快【英文标题】:Load phone's gallery as fast as native IOS in Flutter without OOM 【发布时间】:2021-12-04 23:56:03 【问题描述】:我正在尝试将手机的图库(带有分页)加载到 GridView.builder
小部件中。
这是我使用photo_manager
包创建的issue。
我得到了一些帮助,这让我想到了一个可能的解决方案(请参阅我对这个问题的最后评论)。
我希望能够在不闪烁或出现白页的情况下加载资产。 在 ios 原生上它非常快速和流畅,我想在 Flutter 中实现同样的效果。
您将在上面的 github 链接中找到我制作的所有代码。我已经设法在内存中使用 Map
对象,但我需要改进算法以使其不在 OOM 中。
想要的解决方案(一个或另一个):
一种简单的方法,将手机的图库加载到与原生 IOS 一样快的GridView
,无论在工作时使用哪个包。
对我目前较差的算法的改进,例如,将 15 个资产保持在当前资产之上,15 个资产在内存中和滚动期间,不断更新这些值以围绕列表中的当前位置移动范围。
如果这还不够清楚,请告诉我,作为提醒,请查看我对 issue 的最后一条重要评论。
【问题讨论】:
在因为“需要更多关注”而要求关闭之前,请在评论中说明不清楚的地方,我会更新问题。用例很简单,我只需要手机的图库加载速度和IOS原生图库一样快。 如果你只需要创建一个图库作为图像选择器,你有没有考虑过使用image_picker插件? 我需要自定义图库。这意味着我必须构建一个自定义 UI 页面来加载项目 + 其他 UI 规范。image_picker
运行良好,但它会打开一个新的 Intent
/ 新页面,该页面不可自定义。如果我错了,请随时写一个答案。
【参考方案1】:
你可以使用这样的逻辑:
final Map<String, Uint8List?> _cachedMap = ;
void precacheAssets(int index) async
// Handle cache before index
for (int i = max(0, index - 50); i < 50; i++)
getItemAtIndex(i);
// Handle cache after index
for (int i = min(assetsList.length, index + 50); i < 50 + min(assetsList.length, index + 50); i++)
getItemAtIndex(i);
_cachedMap.removeWhere((key, value)
int currIndex = assetsList.indexWhere((element) => element.id == key);
return currIndex < index - 50 && currIndex > index + 50;
);
/// Get the asset from memory or fetch it if it doesn’t exist yet.
/// Called in the builder method to display assets, not to precache them.
Future<Uint8List?> getItemAtIndex(int index) async
AssetEntity entity = assetsList[index];
if (_cachedMap.containsKey(entity.id))
return _cachedMap[entity.id];
else
Uint8List? thumb = await entity.thumbDataWithOption(
ThumbOption.ios(
width: width,
height: height,
deliveryMode: DeliveryMode.highQualityFormat,
quality: 90));
_cachedMap[entity.id] = thumb;
return thumb;
您可以在 GridView.builder
中的特定索引处调用 precacheAssets
方法,例如 if (index % 25 == 0)
,它将告诉每 25 个项目,将 50 个下一个项目放入缓存中,这样它将向现有项目添加 25 个更多项目缓存。
另外,将Future.builder
中的getItemAtIndex
称为future
参数,如果资产在内存中,您将立即获得该资产,否则照常加载。
随意更改值并对其进行测试,我的 iPhone 中的这些值已经改进了它,但如果你滚动得非常快,你仍然会看到和以前一样。
在这种情况下,您可以添加FadeTransition
,这将导致 UI 不难看。
【讨论】:
感谢您的回答。由于它的加载速度仍然没有本地 IOS 完美,因此我还没有将其设置为可接受的答案,以防有人找到更好的方法,但我正在调整你的示例,我有一些非常令人满意的东西!跨度> 【参考方案2】:解决方案非常简单:用拇指而不是大图像填充您的 GridView
:
// Load [AssetEntity]s:
images = await album.getAssetListRange(start: 0, end: 50);
// Then build [GridView]:
GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 3,
mainAxisSpacing: 3,
),
itemCount: images.length,
itemBuilder: (context, index)
final image = images[index];
return Image(
image: DeviceImage(
image,
size: const Size(200, 200),
),
fit: BoxFit.cover,
);
,
);
如您所见,我将AssetEntity
传递给DeviceImage
提供者。 DeviceImage
加载大小为 (200, 200) 的原始字节,Image
显示拇指。
这是DeviceImage
的代码,我是基于local_image_provider
构建的:
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui' as ui show Codec;
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
import 'package:photo_manager/photo_manager.dart';
/// Decodes the given [LocalImage] object as an [ImageProvider], associating it with the given
/// scale.
///
///
/// In general only the constructor of this object should be used directly,
/// after that use the resulting object wherever an [ImageProvider] is
/// needed in Flutter. In particular with an [Image] widget.
///
class DeviceImage extends ImageProvider<DeviceImage>
static const int _kMaxSize = 1200;
static const String _kNoImageBase64 =
"iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAIAAABLixI0AAAAA3NCSVQICAjb4U/gAAADCUlEQVQ4ja2VT0zTYBTAX9dttB3b6AzFAEIlgEaH7gRxHIyBYGIkGMNAQpaBCdEYjiacPHD0QCQSlcSL8aaZhIN/4xBFlkVj5mY0hgMskwDyt5SNtdBu9VDp1m6AGt/p/fm+33vv+16/IpIkwX8S/d7hZCy2NTMtzs/rS0ryDlegZvPfs1IpZsQbuzeYYlcz3Yi5wNx1lex0I/ocG5HsHoWlxcUr3eLs9G75dYXF1NB9rLJyH5YkCHOdLnHme9qF6nU2SoqxEr+Zxh04WPLkGZqfr8qhYa/cvpUG6VBLb1+5P1z2Yrx8Mlh496GuoFCOpFZ/Lvff0NabafA8n3j9XDFN167j7R28IHAcx3GcruYEOXAnvTjwbovjcvSYSCS8Xi83NXX2lVcObOkMo+fbthFEk9z19BGWFGR9vKEFPW5vbW0lCAKUewyFQtFoFDBstLmDjMdNsY0UimaDiriEAkoCEsNxNhoNhUJOpzPNYhhGVqiq6tra2rW1Nb/fD/F4JsiYStmDHxSTMZMsqs/cqxoTDMNcLpfBYAAAk8k0MjKihMxJsf79WCG7LJsSwGzVMU3VKlZxcbEMAgCaptOgVPL0m5dkYl0Bfaup+3qIBrWoWJFIhGVZq9UKAOFwWGnNOeFTQADIRsulz4BClqhYkiQNDw/b7XaGYSKRiOys/zhJ7bQGANa+ftrVvuzzBQIBDUs7q9vb28FgcGFhQa6uQBRLF38oUUtvH+lqB4DGxsa6urp9WABAEITH4+np6aEoqmhlSfFjZ87Zui4rZlNTE4Zhe7EIgnC73RRF4TjudrtLd6YJAIw1JzWLEfUAqs4LRVEZpHCPnqrfnIvKZl71kewmdmUZjUYFJIvN023zdO+NyM0SBEFzO5a3Y8aJMVkX7Y71i22a9TlYJEkCgCiKPp8vM1wf+lSxPC/rK1/AZ7FllyPvBeXsHQ5H5qD/udA07XA4ZF31rvI8r3lm2aFB/vEDWTc4G2w3BzKjCIJkjoX229akRZov8FW/r89QVobj+B415vh3/LP8AvvVK04ZJmjyAAAAAElFTkSuQmCC";
static final Uint8List noImageBytes = base64Decode(_kNoImageBase64);
/// Creates an object that decodes a [LocalImage] as an image.
///
/// The arguments must not be null. [scale] returns a scaled down
/// version of the image. For example to load a thumbnail you could
/// use something like .1 as the scale. There's a convenience method
/// on [LocalImage] that can calculate the scale for a given pixel
/// size.
/// [minPixels] can be used to specify a minimum independent of the
/// requested scale. The idea is that scaling an image that you don't know
/// the original size of can result in some results that are too small.
/// If the goal is to display the image in a 50x50 thumbnail then you might
/// want to set 50 as the minPixels, then regardless of the image size and
/// scale you'll get at least 50 pixels in each dimension. This parameter
/// was added as a result of a strange result in iOS where an image with
/// a portrait aspect ratio was failing to load when scaled below 120 pixels.
/// Setting 150 as the minimum in this case resolved the problem.
const DeviceImage(this.assetEntity,
this.scale = 1.0, this.minPixels = 0, this.quality = 70, this.size);
/// The LocalImage to decode into an image.
final AssetEntity assetEntity;
/// The scale to place in the [ImageInfo] object of the image.
final double scale;
/// The minPixels to place in the [ImageInfo] object of the image.
final int minPixels;
/// Optional image quality (0-100), default is set to 70.
final int quality;
/// Optional image size. If null, then full size will be loaded.
final Size? size;
@override
Future<DeviceImage> obtainKey(ImageConfiguration? configuration)
return SynchronousFuture<DeviceImage>(this);
@override
ImageStreamCompleter load(DeviceImage key, DecoderCallback decode)
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: key.scale,
informationCollector: () sync*
yield ErrorDescription('Id: $assetEntity.id');
,
);
@override
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream,
DeviceImage key, ImageErrorListener handleError)
if (shouldCache())
super.resolveStreamForKey(configuration, stream, key, handleError);
return;
final ImageStreamCompleter completer =
load(key, PaintingBinding.instance!.instantiateImageCodec);
stream.setCompleter(completer);
int get height => max((assetEntity.height * scale).round(), minPixels);
int get width => max((assetEntity.width * scale).round(), minPixels);
@visibleForTesting
bool shouldCache()
return size == null;
Future<ui.Codec> _loadAsync(DeviceImage key, DecoderCallback decoder) async
assert(key == this);
try
final int width;
final int height;
if (size == null)
width = _kMaxSize;
height = _kMaxSize;
else
width = size!.width.toInt();
height = size!.height.toInt();
final bytes = await assetEntity.thumbDataWithSize(
width,
height,
quality: quality,
);
if (bytes == null || bytes.lengthInBytes == 0)
return decoder(noImageBytes);
return await decoder(bytes);
on PlatformException
return await decoder(noImageBytes);
@override
bool operator ==(dynamic other)
if (other.runtimeType != runtimeType) return false;
final DeviceImage typedOther = other;
return assetEntity.id == typedOther.assetEntity.id &&
scale == typedOther.scale;
@override
int get hashCode => assetEntity.hashCode;
@override
String toString() => '$runtimeType($assetEntity, scale: $scale)';
注意:代码不是生产就绪的。
【讨论】:
感谢您抽出宝贵时间回复。但是它没有按预期工作。你检查过我的 github 问题吗?我已经在使用缩略图甚至 64x64 进行测试,但它不起作用。我实际上也在 Github 上提供了一个解决方案的开始。有了你的,我看到的和我在 Github 上要求的一样:(我检查了你的代码,你使用的方法和我一样,不幸的是,项目需要太多时间才能加载到GridView
中:/。我需要的是流畅的用户体验,在滚动非常快时,很少有milliseconds
出现空屏。
那你需要在页面打开后在后台预缓存缩略图,使用album.getAssetListRange
你是对的,这就是我已经开始做的事情,这个问题实际上是关于这个解决方案的实现,使用 5k + 资产并且最多只预先缓存 20-30 个(向上滚动+向下滚动)最大限度地减少内存负载并避免 OOM。以上是关于在没有 OOM 的情况下,在 Flutter 中加载手机图库的速度与原生 IOS 一样快的主要内容,如果未能解决你的问题,请参考以下文章
在没有事件的情况下在 actionscript 中加载 xml 资产