diff --git a/lib/src/font/font_asset_resolver.dart b/lib/src/font/font_asset_resolver.dart index 1f1988f..7ea3c31 100644 --- a/lib/src/font/font_asset_resolver.dart +++ b/lib/src/font/font_asset_resolver.dart @@ -8,6 +8,8 @@ import 'binary_reader.dart'; import 'font_selection.dart'; import 'open_type_font.dart'; +const _maximumCachedFonts = 32; +const _maximumRetainedFontBytes = 32 << 20; const _maximumCachedGlyphs = 256; const _maximumRetainedGlyphBytes = 8 << 20; typedef _GlyphCacheKey = (IconData, String, MorphFontSelection); @@ -18,8 +20,11 @@ final class FontAssetResolver { final AssetBundle bundle; Future>? _manifest; - final Map> _fonts = - >{}; + final SizedLruCache> _fonts = + SizedLruCache>( + maximumSize: _maximumCachedFonts, + maximumSizeBytes: _maximumRetainedFontBytes, + ); final SizedLruCache<_GlyphCacheKey, Future> _glyphs = SizedLruCache<_GlyphCacheKey, Future>( maximumSize: _maximumCachedGlyphs, @@ -166,17 +171,17 @@ final class FontAssetResolver { } Future _loadFont(String assetKey) async { - var future = _fonts[assetKey]; + var future = _fonts.get(assetKey); if (future == null) { future = _readFont(assetKey); - _fonts[assetKey] = future; + _fonts.put(assetKey, future); } try { - return await future; + final font = await future; + _fonts.updateSizeIfSame(assetKey, future, font.bytes.lengthInBytes); + return font; } catch (_) { - if (identical(_fonts[assetKey], future)) { - _fonts.remove(assetKey); - } + _fonts.removeIfSame(assetKey, future); rethrow; } } diff --git a/test/src/font/font_asset_resolver_test.dart b/test/src/font/font_asset_resolver_test.dart index 22cf73d..b0062a1 100644 --- a/test/src/font/font_asset_resolver_test.dart +++ b/test/src/font/font_asset_resolver_test.dart @@ -95,6 +95,66 @@ void main() { expect(bundle.loadCount('assets/icons.ttf'), 1); }); + test('parsed font assets are evicted by the bounded cache', () async { + const retainedFontCount = 32; + const icons = [ + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family0'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family1'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family2'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family3'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family4'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family5'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family6'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family7'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family8'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family9'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family10'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family11'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family12'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family13'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family14'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family15'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family16'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family17'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family18'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family19'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family20'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family21'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family22'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family23'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family24'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family25'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family26'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family27'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family28'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family29'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family30'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family31'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family32'), + ]; + final font = TestFontBuilder.trueType(); + final manifest = >{}; + final assets = {}; + for (var index = 0; index <= retainedFontCount; index++) { + final asset = 'assets/font_$index.ttf'; + manifest['Family$index'] = [asset]; + assets[asset] = font; + } + final bundle = TestAssetBundle.fonts(manifest: manifest, assets: assets); + final resolver = FontAssetResolver(bundle); + + for (final icon in icons) { + await resolver.resolve(icon); + } + expect(bundle.loadCount('assets/font_0.ttf'), 1); + + await resolver.resolve( + const IconData(TestFontBuilder.compositeCodePoint, fontFamily: 'Family0'), + ); + + expect(bundle.loadCount('assets/font_0.ttf'), 2); + }); + test('unique glyph streams evict old decoded outlines', () async { const glyphCount = 300; final resolver = FontAssetResolver(aliasedFixtureBundle(glyphCount));