问题背景
在开发相册清理应用时,需要展示用户手机中的所有照片。一个普通用户可能有几千甚至上万张照片,如何流畅地展示这些图片是一个很大的挑战。
最初的实现方案在照片数量超过1000张时就开始出现明显卡顿,内存占用也会飙升到几百MB。
性能瓶颈分析
通过Flutter DevTools分析,发现主要问题:
- 一次性加载所有图片元数据导致初始化慢
- 缩略图没有缓存,重复加载浪费资源
- 滚动时频繁创建销毁Widget
- 图片解码在主线程执行,阻塞UI
优化方案一:分页加载
使用photo_manager的分页API,按需加载图片资源:
class PhotoRepository {
static const int pageSize = 100;
Future<List<AssetEntity>> loadPage(int page) async {
final albums = await PhotoManager.getAssetPathList(
type: RequestType.image,
);
if (albums.isEmpty) return [];
final album = albums.first;
return await album.getAssetListPaged(
page: page,
size: pageSize,
);
}
}
配合ListView的滚动监听,实现无限滚动加载:
class PhotoListController extends GetxController {
final photos = <AssetEntity>[].obs;
int _currentPage = 0;
bool _isLoading = false;
bool _hasMore = true;
Future<void> loadMore() async {
if (_isLoading || !_hasMore) return;
_isLoading = true;
final newPhotos = await PhotoRepository.loadPage(_currentPage);
if (newPhotos.length < PhotoRepository.pageSize) {
_hasMore = false;
}
photos.addAll(newPhotos);
_currentPage++;
_isLoading = false;
}
}
优化方案二:缩略图缓存
自定义缩略图缓存管理器,避免重复加载:
class ThumbnailCacheManager {
static final _cache = LruCache<String, Uint8List>(maximumSize: 200);
static Future<Uint8List?> getThumbnail(AssetEntity asset) async {
final key = asset.id;
// 先查缓存
var thumb = _cache.get(key);
if (thumb != null) return thumb;
// 缓存未命中,加载缩略图
thumb = await asset.thumbnailDataWithSize(
const ThumbnailSize(200, 200),
quality: 80,
);
if (thumb != null) {
_cache.put(key, thumb);
}
return thumb;
}
static void clear() {
_cache.clear();
}
}
优化方案三:使用GridView.builder
GridView.builder只会构建可见区域的Widget,大大减少内存占用:
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 2,
mainAxisSpacing: 2,
),
itemCount: controller.photos.length,
itemBuilder: (context, index) {
return PhotoThumbnail(
asset: controller.photos[index],
key: ValueKey(controller.photos[index].id),
);
},
)
优化方案四:图片组件优化
使用FadeInImage实现占位图和渐入效果,提升视觉体验:
class PhotoThumbnail extends StatelessWidget {
final AssetEntity asset;
const PhotoThumbnail({super.key, required this.asset});
@override
Widget build(BuildContext context) {
return FutureBuilder<Uint8List?>(
future: ThumbnailCacheManager.getThumbnail(asset),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
return Image.memory(
snapshot.data!,
fit: BoxFit.cover,
gaplessPlayback: true, // 防止闪烁
);
}
return Container(color: Colors.grey[200]);
},
);
}
}
优化方案五:预加载策略
在用户滚动时,提前加载即将显示的图片:
void onScroll(ScrollController controller) {
final position = controller.position;
final maxScroll = position.maxScrollExtent;
final currentScroll = position.pixels;
// 当滚动到80%时,预加载下一页
if (currentScroll > maxScroll * 0.8) {
loadMore();
}
}
优化效果
经过以上优化后:
- 初始加载时间从3秒降到0.5秒
- 内存占用从300MB降到80MB左右
- 滚动帧率稳定在60fps
- 支持展示10万+照片无压力
总结
移动端大量图片加载的核心优化思路:
- 按需加载,不要一次性加载所有数据
- 合理使用缓存,避免重复计算
- 使用Builder模式的列表组件
- 异步加载,不阻塞主线程
- 预加载提升用户体验