dispose images correctly
This commit is contained in:
@@ -20,10 +20,28 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
class EquatableImage {
|
class EquatableImage {
|
||||||
final Image value;
|
Image? _value;
|
||||||
final int _imageHash;
|
final int _imageHash;
|
||||||
|
bool _isDisposed = false;
|
||||||
|
|
||||||
EquatableImage(this.value, {required int hash}) : _imageHash = hash;
|
EquatableImage(Image value, {required int hash})
|
||||||
|
: _value = value,
|
||||||
|
_imageHash = hash;
|
||||||
|
|
||||||
|
/// The underlying ui.Image. Returns null if disposed.
|
||||||
|
Image? get value => _isDisposed ? null : _value;
|
||||||
|
|
||||||
|
/// Whether this image has been disposed.
|
||||||
|
bool get isDisposed => _isDisposed;
|
||||||
|
|
||||||
|
/// Disposes the underlying ui.Image to free GPU memory.
|
||||||
|
/// This is safe to call multiple times.
|
||||||
|
void dispose() {
|
||||||
|
if (_isDisposed) return;
|
||||||
|
_isDisposed = true;
|
||||||
|
_value?.dispose();
|
||||||
|
_value = null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => _imageHash.hashCode;
|
int get hashCode => _imageHash.hashCode;
|
||||||
|
|||||||
@@ -86,11 +86,14 @@ class GenericWebsiteService extends _$GenericWebsiteService {
|
|||||||
//Global icon cache
|
//Global icon cache
|
||||||
late CacheRepository _cacheRepository;
|
late CacheRepository _cacheRepository;
|
||||||
//Local decoded icon cache
|
//Local decoded icon cache
|
||||||
final LRUCache<String, BrowserIcon> _browserIconCache;
|
late final LRUCache<String, BrowserIcon> _browserIconCache;
|
||||||
|
|
||||||
GenericWebsiteService()
|
GenericWebsiteService()
|
||||||
: _iconsService = GeckoIconService(),
|
: _iconsService = GeckoIconService(),
|
||||||
_browserIconCache = LRUCache(50);
|
_browserIconCache = LRUCache(
|
||||||
|
50,
|
||||||
|
onEvict: (icon) => icon.image.dispose(),
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void build() {
|
void build() {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ final class GenericWebsiteServiceProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$genericWebsiteServiceHash() =>
|
String _$genericWebsiteServiceHash() =>
|
||||||
r'44d6e4cc161b1200070d99e29c1db04cd18ca091';
|
r'32a561820bda432f3c17e05b9da9c85871efffb4';
|
||||||
|
|
||||||
abstract class _$GenericWebsiteService extends $Notifier<void> {
|
abstract class _$GenericWebsiteService extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -45,6 +45,24 @@ part 'tab_state.g.dart';
|
|||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class TabStates extends _$TabStates {
|
class TabStates extends _$TabStates {
|
||||||
|
/// Disposes images from a TabState to free GPU memory.
|
||||||
|
void _disposeTabImages(TabState tab) {
|
||||||
|
tab.icon?.dispose();
|
||||||
|
tab.thumbnail?.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates state while disposing images from removed tabs.
|
||||||
|
void _updateState(Map<String, TabState> newState) {
|
||||||
|
// Find and dispose images from tabs that are being removed
|
||||||
|
for (final tabId in state.keys) {
|
||||||
|
if (!newState.containsKey(tabId)) {
|
||||||
|
_disposeTabImages(state[tabId]!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state = newState;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onTabContentStateChange(TabContentState contentState) async {
|
Future<void> _onTabContentStateChange(TabContentState contentState) async {
|
||||||
final current = await patchedState(contentState.id);
|
final current = await patchedState(contentState.id);
|
||||||
|
|
||||||
@@ -71,7 +89,7 @@ class TabStates extends _$TabStates {
|
|||||||
isLoading: contentState.isLoading,
|
isLoading: contentState.isLoading,
|
||||||
);
|
);
|
||||||
|
|
||||||
state = {...state}..[contentState.id] = newState;
|
_updateState({...state}..[contentState.id] = newState);
|
||||||
|
|
||||||
if (newState.isFinishedLoading) {
|
if (newState.isFinishedLoading) {
|
||||||
ref
|
ref
|
||||||
@@ -104,6 +122,9 @@ class TabStates extends _$TabStates {
|
|||||||
Future<void> _onIconChange(IconChangeEvent event) async {
|
Future<void> _onIconChange(IconChangeEvent event) async {
|
||||||
final IconChangeEvent(:tabId, :bytes) = event;
|
final IconChangeEvent(:tabId, :bytes) = event;
|
||||||
|
|
||||||
|
// Dispose old icon before replacing
|
||||||
|
state[tabId]?.icon?.dispose();
|
||||||
|
|
||||||
final image = await bytes.mapNotNull((bytes) => tryDecodeImage(bytes));
|
final image = await bytes.mapNotNull((bytes) => tryDecodeImage(bytes));
|
||||||
|
|
||||||
final current = state[tabId] ?? TabState.$default(tabId);
|
final current = state[tabId] ?? TabState.$default(tabId);
|
||||||
@@ -113,6 +134,9 @@ class TabStates extends _$TabStates {
|
|||||||
Future<void> _onThumbnailChange(ThumbnailEvent event) async {
|
Future<void> _onThumbnailChange(ThumbnailEvent event) async {
|
||||||
final ThumbnailEvent(:tabId, :bytes) = event;
|
final ThumbnailEvent(:tabId, :bytes) = event;
|
||||||
|
|
||||||
|
// Dispose old thumbnail before replacing
|
||||||
|
state[tabId]?.thumbnail?.dispose();
|
||||||
|
|
||||||
final image = await bytes.mapNotNull((bytes) => tryDecodeImage(bytes));
|
final image = await bytes.mapNotNull((bytes) => tryDecodeImage(bytes));
|
||||||
|
|
||||||
final current = state[tabId] ?? TabState.$default(tabId);
|
final current = state[tabId] ?? TabState.$default(tabId);
|
||||||
@@ -244,6 +268,12 @@ class TabStates extends _$TabStates {
|
|||||||
);
|
);
|
||||||
|
|
||||||
ref.onDispose(() async {
|
ref.onDispose(() async {
|
||||||
|
// Dispose all images from all tabs
|
||||||
|
for (final tab in state.values) {
|
||||||
|
_disposeTabImages(tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel all stream subscriptions
|
||||||
for (final sub in subscriptions) {
|
for (final sub in subscriptions) {
|
||||||
await sub.cancel();
|
await sub.cancel();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class TabStatesProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabStatesHash() => r'cd73ea6ea479f2708a2c9e5ca86054c9d2464116';
|
String _$tabStatesHash() => r'f2e6920017b3f46944d85254c09c54779eb89eb3';
|
||||||
|
|
||||||
abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
|
abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
|
||||||
Map<String, TabState> build();
|
Map<String, TabState> build();
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ class WebExtensionsState extends _$WebExtensionsState {
|
|||||||
} else {
|
} else {
|
||||||
if (state.containsKey(extensionId)) {
|
if (state.containsKey(extensionId)) {
|
||||||
state = {...state}..remove(extensionId);
|
state = {...state}..remove(extensionId);
|
||||||
|
// Dispose the cached image when extension is removed
|
||||||
|
_imageCache.remove(extensionId)?.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,13 +73,14 @@ class WebExtensionsState extends _$WebExtensionsState {
|
|||||||
final image = await tryDecodeImage(bytes);
|
final image = await tryDecodeImage(bytes);
|
||||||
|
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
if (_imageCache[extensionId] != image) {
|
// Dispose old image before replacing
|
||||||
_imageCache[extensionId] = image;
|
_imageCache[extensionId]?.dispose();
|
||||||
|
|
||||||
if (state.containsKey(extensionId)) {
|
_imageCache[extensionId] = image;
|
||||||
state = {...state}
|
|
||||||
..[extensionId] = state[extensionId]!.copyWith.icon(image);
|
if (state.containsKey(extensionId)) {
|
||||||
}
|
state = {...state}
|
||||||
|
..[extensionId] = state[extensionId]!.copyWith.icon(image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +109,13 @@ class WebExtensionsState extends _$WebExtensionsState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ref.onDispose(() async {
|
ref.onDispose(() async {
|
||||||
|
// Dispose all cached images
|
||||||
|
for (final image in _imageCache.values) {
|
||||||
|
image.dispose();
|
||||||
|
}
|
||||||
|
_imageCache.clear();
|
||||||
|
|
||||||
|
// Cancel all stream subscriptions
|
||||||
for (final sub in subscriptions) {
|
for (final sub in subscriptions) {
|
||||||
await sub.cancel();
|
await sub.cancel();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ final class WebExtensionsStateProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$webExtensionsStateHash() =>
|
String _$webExtensionsStateHash() =>
|
||||||
r'6fb44ab543250f2b2554e410ee749fdd035cd4e7';
|
r'd7147f419889386f8cf526aed3318839ac22d893';
|
||||||
|
|
||||||
final class WebExtensionsStateFamily extends $Family
|
final class WebExtensionsStateFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|||||||
+2
-2
@@ -205,7 +205,7 @@ class GridTabPreview extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
if (tabState.thumbnail != null)
|
if (tabState.thumbnail?.value != null)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
@@ -284,7 +284,7 @@ class ListTabPreview extends HookConsumerWidget {
|
|||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onLongPress,
|
onLongPress: onLongPress,
|
||||||
contentPadding: const EdgeInsets.only(left: 4),
|
contentPadding: const EdgeInsets.only(left: 4),
|
||||||
leading: (tabState.thumbnail != null)
|
leading: (tabState.thumbnail?.value != null)
|
||||||
? RepaintBoundary(
|
? RepaintBoundary(
|
||||||
child: RawImage(
|
child: RawImage(
|
||||||
image: tabState.thumbnail!.value,
|
image: tabState.thumbnail!.value,
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ import 'package:fast_equatable/hash.dart';
|
|||||||
import 'package:weblibre/domain/entities/equatable_image.dart';
|
import 'package:weblibre/domain/entities/equatable_image.dart';
|
||||||
import 'package:weblibre/utils/lru_cache.dart';
|
import 'package:weblibre/utils/lru_cache.dart';
|
||||||
|
|
||||||
final _cache = LRUCache<int, EquatableImage>(100);
|
final _cache = LRUCache<int, EquatableImage>(
|
||||||
|
100,
|
||||||
|
onEvict: (image) => image.dispose(),
|
||||||
|
);
|
||||||
|
|
||||||
Future<EquatableImage?> tryDecodeImage(
|
Future<EquatableImage?> tryDecodeImage(
|
||||||
Uint8List bytes, {
|
Uint8List bytes, {
|
||||||
@@ -35,7 +38,7 @@ Future<EquatableImage?> tryDecodeImage(
|
|||||||
final digest = secureHash(bytes);
|
final digest = secureHash(bytes);
|
||||||
|
|
||||||
final cached = _cache.get(digest);
|
final cached = _cache.get(digest);
|
||||||
if (cached != null) {
|
if (cached != null && !cached.isDisposed) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +53,7 @@ Future<EquatableImage?> tryDecodeImage(
|
|||||||
final frameInfo = await codec.getNextFrame();
|
final frameInfo = await codec.getNextFrame();
|
||||||
final image = EquatableImage(frameInfo.image, hash: digest);
|
final image = EquatableImage(frameInfo.image, hash: digest);
|
||||||
|
|
||||||
if (image.value.width > 0) {
|
if (image.value != null && image.value!.width > 0) {
|
||||||
_cache.set(digest, image);
|
_cache.set(digest, image);
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,13 +22,16 @@ import 'dart:collection';
|
|||||||
class LRUCache<K, V> {
|
class LRUCache<K, V> {
|
||||||
int _capacity;
|
int _capacity;
|
||||||
final LinkedHashMap<K, V> _cache;
|
final LinkedHashMap<K, V> _cache;
|
||||||
|
final void Function(V)? _onEvict;
|
||||||
|
|
||||||
LRUCache(
|
LRUCache(
|
||||||
this._capacity, {
|
this._capacity, {
|
||||||
bool Function(K, K)? equals,
|
bool Function(K, K)? equals,
|
||||||
int Function(K)? hashCode,
|
int Function(K)? hashCode,
|
||||||
bool Function(dynamic)? isValidKey,
|
bool Function(dynamic)? isValidKey,
|
||||||
}) : _cache = LinkedHashMap<K, V>(
|
void Function(V)? onEvict,
|
||||||
|
}) : _onEvict = onEvict,
|
||||||
|
_cache = LinkedHashMap<K, V>(
|
||||||
equals: equals,
|
equals: equals,
|
||||||
hashCode: hashCode,
|
hashCode: hashCode,
|
||||||
isValidKey: isValidKey,
|
isValidKey: isValidKey,
|
||||||
@@ -36,7 +39,12 @@ class LRUCache<K, V> {
|
|||||||
|
|
||||||
void resize(int capacity) {
|
void resize(int capacity) {
|
||||||
if (_capacity > capacity) {
|
if (_capacity > capacity) {
|
||||||
_cache.keys.take(_capacity - capacity).forEach(_cache.remove);
|
_cache.keys.take(_capacity - capacity).forEach((key) {
|
||||||
|
final evicted = _cache.remove(key);
|
||||||
|
if (evicted != null) {
|
||||||
|
_onEvict?.call(evicted);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_capacity = capacity;
|
_capacity = capacity;
|
||||||
@@ -58,14 +66,39 @@ class LRUCache<K, V> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
V set(K key, V value) {
|
V set(K key, V value) {
|
||||||
|
V? evicted;
|
||||||
|
|
||||||
if (_cache.containsKey(key)) {
|
if (_cache.containsKey(key)) {
|
||||||
_cache.remove(key); // Remove the existing item before updating.
|
evicted = _cache.remove(key); // Remove the existing item before updating.
|
||||||
} else if (_cache.length == _capacity) {
|
} else if (_cache.length == _capacity) {
|
||||||
_cache.remove(
|
evicted = _cache.remove(
|
||||||
_cache.keys.first,
|
_cache.keys.first,
|
||||||
); // Explicitly remove the least recently used item if at capacity.
|
); // Explicitly remove the least recently used item if at capacity.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (evicted != null) {
|
||||||
|
_onEvict?.call(evicted);
|
||||||
|
}
|
||||||
|
|
||||||
return _cache[key] = value; // Inserting or updating the item.
|
return _cache[key] = value; // Inserting or updating the item.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clears all entries from the cache, calling onEvict for each entry.
|
||||||
|
void clear() {
|
||||||
|
if (_onEvict != null) {
|
||||||
|
for (final value in _cache.values) {
|
||||||
|
_onEvict(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes an entry by key, calling onEvict if it existed.
|
||||||
|
V? remove(K key) {
|
||||||
|
final value = _cache.remove(key);
|
||||||
|
if (value != null) {
|
||||||
|
_onEvict?.call(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user