回留应用实战经验
Flutter 相册整理应用的性能优化实践:从卡顿到丝滑
本文分享在开发「回留」相册整理应用过程中,针对大量图片处理场景的性能优化实践,包括 Isolate 并行计算、智能预加载、感知哈希算法等核心技术。
背景
手机相册动辄几千上万张照片,如何让用户快速、流畅地整理这些照片是一个技术挑战。我们面临的核心问题:
- UI 卡顿:图片解码、哈希计算等耗时操作阻塞主线程
- 切换闪烁:卡片滑动时图片加载不及时导致白屏闪烁
- 相似检测慢:数千张照片的相似度计算耗时过长
- 内存压力:大量高清图片同时加载导致 OOM
本文将逐一分享我们的解决方案。
一、Isolate 池:并行计算的正确姿势
问题
Flutter 是单线程模型,耗时计算会阻塞 UI。虽然可以用 compute() 函数开启 Isolate,但频繁创建/销毁 Isolate 的开销很大。
解决方案:Isolate 池
我们实现了一个 Isolate 池,预先创建多个 Isolate 工作者,通过轮询调度分配任务:
class IsolatePool {
static IsolatePool? _instance;
static IsolatePool get instance => _instance ??= IsolatePool._();
final List<_IsolateWorker> _workers = [];
int _poolSize = 2;
int _nextWorkerIndex = 0;
/// 初始化 Isolate 池
Future<void> initialize({int? poolSize}) async {
// 根据 CPU 核心数动态设置池大小,最少2个,最多6个
_poolSize = poolSize ?? (Platform.numberOfProcessors ~/ 2).clamp(2, 6);
for (int i = 0; i < _poolSize; i++) {
final worker = await _IsolateWorker.spawn(i);
_workers.add(worker);
}
}
/// 计算哈希(轮询调度)
Future<String?> computeHash(Uint8List imageData) async {
final worker = _workers[_nextWorkerIndex];
_nextWorkerIndex = (_nextWorkerIndex + 1) % _poolSize;
return worker.computeHash(imageData);
}
/// 批量并行计算
Future<List<String?>> computeHashBatch(List<Uint8List> imageDataList) async {
final futures = <Future<String?>>[];
for (int i = 0; i < imageDataList.length; i++) {
final workerIndex = i % _poolSize;
futures.add(_workers[workerIndex].computeHash(imageDataList[i]));
}
return Future.wait(futures);
}
}
效果
- 避免频繁创建 Isolate 的开销
- 充分利用多核 CPU 并行计算
- 1000 张照片的哈希计算从 60s 降到 15s
二、智能预加载:消灭切换闪烁
问题
卡片式滑动整理照片时,下一张照片还没加载完就切换过去,会出现白屏闪烁。
解决方案:智能预加载器
class PhotoPreloader {
final PhotoCleanController _controller;
final Set<String> _preloadingIds = {};
static const int _maxConcurrentPreloads = 6; // 并发数
static const int _preloadRadius = 4; // 预加载半径
bool _isPaused = false; // 滑动时暂停预加载
/// 预加载照片整理页面的相邻照片
Future<void> preloadForCleanView(List<AssetEntity> photos, int currentIndex) async {
if (_isPaused) return; // 滑动时暂停,避免资源竞争
final indicesToPreload = <int>[];
// 当前照片(最高优先级)
indicesToPreload.add(currentIndex);
// 后续5张(卡片堆叠显示3-4张,多预加载1张)
for (int i = 1; i <= 5; i++) {
if (currentIndex + i < photos.length) {
indicesToPreload.add(currentIndex + i);
}
}
// 前3张(用于撤销功能)
for (int i = 1; i <= 3; i++) {
if (currentIndex - i >= 0) {
indicesToPreload.add(currentIndex - i);
}
}
await _batchPreload(photos, indicesToPreload);
}
/// 滑动开始时暂停预加载
void pause() => _isPaused = true;
/// 滑动结束时恢复预加载
void resume({int? currentIndex, List<AssetEntity>? photos}) {
_isPaused = false;
if (currentIndex != null && photos != null) {
Future.microtask(() => preloadForCleanView(photos, currentIndex));
}
}
}
关键优化点
- 优先级排序:当前 > 下一张 > 上一张 > 其他
- 滑动时暂停:避免预加载和滑动动画抢占资源
- 并发控制:限制同时预加载数量,避免内存压力
- 节流控制:50ms 内不重复触发预加载
三、感知哈希:相似照片检测的核心
问题
如何快速判断两张照片是否相似?逐像素比较太慢,且对缩放、裁剪敏感。
解决方案:多算法组合的感知哈希
我们组合了 5 种哈希算法,生成 320 位(80 字符)的指纹:
class HashComputeCore {
/// 计算感知哈希
/// 使用 aHash + dHash + vHash + cHash + pHash 组合
static String? computeHash(Uint8List imageData) {
final image = img.decodeImage(imageData);
final grayscale = img.grayscale(image);
// 1. aHash (Average Hash) - 64位
// 缩放到 8x8,比较每个像素与平均亮度
final aHashBits = _computeAHash(grayscale);
// 2. dHash (Difference Hash) - 64位
// 缩放到 9x8,比较相邻像素的水平差异
final dHashBits = _computeDHash(grayscale);
// 3. vHash (Vertical Difference Hash) - 64位
// 缩放到 8x9,比较相邻像素的垂直差异
final vHashBits = _computeVHash(grayscale);
// 4. cHash (Color Histogram Hash) - 64位
// 分块统计颜色直方图特征
final cHashBits = _computeCHash(image);
// 5. pHash (DCT Perceptual Hash) - 64位
// 使用离散余弦变换提取低频特征
final pHashBits = _computePHash(grayscale);
return _bitsToHex([...aHashBits, ...dHashBits, ...vHashBits, ...cHashBits, ...pHashBits]);
}
}
pHash 的 DCT 实现
pHash 是最精确的感知哈希算法,核心是离散余弦变换(DCT):
/// 计算 pHash - 64位
static List<int> _computePHash(img.Image grayscale) {
// 1. 缩放到 32x32
final small32x32 = img.copyResize(grayscale, width: 32, height: 32);
// 2. 构建亮度矩阵
final matrix = List.generate(32, (y) {
return List.generate(32, (x) {
return small32x32.getPixel(x, y).luminance.toDouble();
});
});
// 3. 计算 2D DCT
final dctMatrix = _computeDCT(matrix);
// 4. 取左上角 8x8 低频系数(去掉 DC 分量)
final lowFreq = <double>[];
for (var y = 0; y < 8; y++) {
for (var x = 0; x < 8; x++) {
if (x == 0 && y == 0) continue; // 跳过 DC 分量
lowFreq.add(dctMatrix[y][x]);
}
}
// 5. 计算中位数,生成二值哈希
final sorted = List<double>.from(lowFreq)..sort();
final median = sorted[sorted.length ~/ 2];
final bits = <int>[];
for (var y = 0; y < 8; y++) {
for (var x = 0; x < 8; x++) {
bits.add(dctMatrix[y][x] > median ? 1 : 0);
}
}
return bits;
}
相似度计算
使用汉明距离判断相似度:
int hammingDistance(String hash1, String hash2) {
int distance = 0;
for (int i = 0; i < hash1.length; i++) {
if (hash1[i] != hash2[i]) distance++;
}
return distance;
}
// 320位哈希,距离 < 32(10%)认为相似
bool isSimilar(String hash1, String hash2) {
return hammingDistance(hash1, hash2) < 32;
}
为什么组合多种算法?
| 算法 | 特点 | 擅长场景 |
|---|---|---|
| aHash | 简单快速 | 整体亮度相似 |
| dHash | 对缩放鲁棒 | 水平结构相似 |
| vHash | 补充 dHash | 垂直结构相似 |
| cHash | 保留颜色信息 | 色彩相似 |
| pHash | 最精确 | 内容相似 |
组合使用可以大幅降低误判率。
四、其他优化技巧
1. 日期时间的正确获取
Android 系统的 createDateTime 可能是文件添加到相册的时间,而非实际拍摄时间:
/// 获取最佳日期时间
DateTime _getBestDateTime() {
final createDt = photo.createDateTime;
final modifiedDt = photo.modifiedDateTime;
// 如果 modifiedDateTime 早于 createDateTime,使用 modifiedDateTime
// 这种情况通常发生在图片被复制/移动后
if (modifiedDt.isBefore(createDt)) {
return modifiedDt;
}
return createDt;
}
2. 成就系统的事件驱动设计
class AchievementService {
final Rx<UserStats> userStats = UserStats().obs;
final RxList<AchievementDefinition> newlyUnlocked = <AchievementDefinition>[].obs;
/// 增加统计值并检查成就
Future<void> incrementStat(String statKey, {int amount = 1}) async {
// 更新统计
await _updateUserStats(...);
// 检查相关成就
await _checkAchievementsByStatKey(statKey);
}
/// 检查并解锁成就
Future<void> _checkAndUnlockAchievement(AchievementDefinition achievement) async {
final currentValue = userStats.value.getStatValue(achievement.statKey!);
if (currentValue >= achievement.targetValue) {
// 解锁成就,添加到通知列表
newlyUnlocked.add(achievement);
}
}
}
3. Mixin 分离关注点
将控制器按功能拆分为多个 Mixin,提高可维护性:
class PhotoCleanController extends GetxController
with PhotoCacheMixin, // 缓存管理
PhotoDeleteMixin, // 删除操作
PhotoSimilarMixin, // 相似照片处理
PhotoUndoMixin, // 撤销功能
PhotoSwipeMixin, // 滑动手势
PhotoBatchMixin, // 批量操作
PhotoUtilsMixin { // 工具方法
// 主控制器只负责协调各 Mixin
}
总结
通过以上优化,我们实现了:
- 流畅的滑动体验:预加载 + 暂停机制消除闪烁
- 快速的相似检测:Isolate 池 + 多算法哈希,1000 张照片 15s 完成
- 低内存占用:并发控制 + 缓存淘汰策略
- 良好的可维护性:Mixin 分离 + 事件驱动架构
希望这些实践对你有所帮助。如果你也在开发图片处理类应用,欢迎交流讨论。
本文基于「回留」相册整理应用的实际开发经验,代码已在生产环境验证。
分类:
Flutter实战经验
标签:
版权申明
本文系作者 @MrZhang1899 原创发布在回留应用实战经验。未经许可,禁止转载。
评论
-- 评论已关闭 --
全部评论