Skip to content

Commit e3db048

Browse files
authored
Speed up first asset load by using the binary-formatted asset manifest for image resolution (flutter#118782)
* add asset manifest bin loading and asset manifest api * use new api for image resolution * remove upfront smc data casting * fix typecasting issue * remove unused import * fix tests * lints * lints * fix import * fix outdated type name * restore AssetManifest docstrings * update test * update other test * make error message for invalid keys more useful
1 parent 7175de4 commit e3db048

File tree

6 files changed

+98
-149
lines changed

6 files changed

+98
-149
lines changed

dev/integration_tests/ui/test/asset_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ void main() {
1414

1515
// If this asset couldn't be loaded, the exception message would be
1616
// "asset failed to load"
17-
expect(tester.takeException().toString(), contains('Invalid image data'));
17+
expect(tester.takeException().toString(), contains('The key was not found in the asset manifest'));
1818
});
1919
}

packages/flutter/lib/src/painting/image_resolution.dart

Lines changed: 46 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,12 @@
44

55
import 'dart:async';
66
import 'dart:collection';
7-
import 'dart:convert';
87

98
import 'package:flutter/foundation.dart';
109
import 'package:flutter/services.dart';
1110

1211
import 'image_provider.dart';
1312

14-
const String _kAssetManifestFileName = 'AssetManifest.json';
15-
1613
/// A screen with a device-pixel ratio strictly less than this value is
1714
/// considered a low-resolution screen (typically entry-level to mid-range
1815
/// laptops, desktop screens up to QHD, low-end tablets such as Kindle Fire).
@@ -284,18 +281,18 @@ class AssetImage extends AssetBundleImageProvider {
284281
Completer<AssetBundleImageKey>? completer;
285282
Future<AssetBundleImageKey>? result;
286283

287-
chosenBundle.loadStructuredData<Map<String, List<String>>?>(_kAssetManifestFileName, manifestParser).then<void>(
288-
(Map<String, List<String>>? manifest) {
289-
final String chosenName = _chooseVariant(
284+
AssetManifest.loadFromAssetBundle(chosenBundle)
285+
.then((AssetManifest manifest) {
286+
final Iterable<AssetMetadata> candidateVariants = _getVariants(manifest, keyName);
287+
final AssetMetadata chosenVariant = _chooseVariant(
290288
keyName,
291289
configuration,
292-
manifest == null ? null : manifest[keyName],
293-
)!;
294-
final double chosenScale = _parseScale(chosenName);
290+
candidateVariants,
291+
);
295292
final AssetBundleImageKey key = AssetBundleImageKey(
296293
bundle: chosenBundle,
297-
name: chosenName,
298-
scale: chosenScale,
294+
name: chosenVariant.key,
295+
scale: chosenVariant.targetDevicePixelRatio ?? _naturalResolution,
299296
);
300297
if (completer != null) {
301298
// We already returned from this function, which means we are in the
@@ -309,14 +306,15 @@ class AssetImage extends AssetBundleImageProvider {
309306
// ourselves.
310307
result = SynchronousFuture<AssetBundleImageKey>(key);
311308
}
312-
},
313-
).catchError((Object error, StackTrace stack) {
314-
// We had an error. (This guarantees we weren't called synchronously.)
315-
// Forward the error to the caller.
316-
assert(completer != null);
317-
assert(result == null);
318-
completer!.completeError(error, stack);
319-
});
309+
})
310+
.onError((Object error, StackTrace stack) {
311+
// We had an error. (This guarantees we weren't called synchronously.)
312+
// Forward the error to the caller.
313+
assert(completer != null);
314+
assert(result == null);
315+
completer!.completeError(error, stack);
316+
});
317+
320318
if (result != null) {
321319
// The code above ran synchronously, and came up with an answer.
322320
// Return the SynchronousFuture that we created above.
@@ -328,35 +326,34 @@ class AssetImage extends AssetBundleImageProvider {
328326
return completer.future;
329327
}
330328

331-
/// Parses the asset manifest string into a strongly-typed map.
332-
@visibleForTesting
333-
static Future<Map<String, List<String>>?> manifestParser(String? jsonData) {
334-
if (jsonData == null) {
335-
return SynchronousFuture<Map<String, List<String>>?>(null);
329+
Iterable<AssetMetadata> _getVariants(AssetManifest manifest, String key) {
330+
try {
331+
return manifest.getAssetVariants(key);
332+
} catch (e) {
333+
throw FlutterError.fromParts(<DiagnosticsNode>[
334+
ErrorSummary('Unable to load asset with key "$key".'),
335+
ErrorDescription(
336+
'''
337+
The key was not found in the asset manifest.
338+
Make sure the key is correct and the appropriate file or folder is specified in pubspec.yaml.
339+
'''),
340+
]);
336341
}
337-
// TODO(ianh): JSON decoding really shouldn't be on the main thread.
338-
final Map<String, dynamic> parsedJson = json.decode(jsonData) as Map<String, dynamic>;
339-
final Iterable<String> keys = parsedJson.keys;
340-
final Map<String, List<String>> parsedManifest = <String, List<String>> {
341-
for (final String key in keys) key: List<String>.from(parsedJson[key] as List<dynamic>),
342-
};
343-
// TODO(ianh): convert that data structure to the right types.
344-
return SynchronousFuture<Map<String, List<String>>?>(parsedManifest);
345342
}
346343

347-
String? _chooseVariant(String main, ImageConfiguration config, List<String>? candidates) {
348-
if (config.devicePixelRatio == null || candidates == null || candidates.isEmpty) {
349-
return main;
344+
AssetMetadata _chooseVariant(String mainAssetKey, ImageConfiguration config, Iterable<AssetMetadata> candidateVariants) {
345+
if (config.devicePixelRatio == null || candidateVariants.isEmpty) {
346+
return candidateVariants.firstWhere((AssetMetadata variant) => variant.main);
350347
}
351-
// TODO(ianh): Consider moving this parsing logic into _manifestParser.
352-
final SplayTreeMap<double, String> mapping = SplayTreeMap<double, String>();
353-
for (final String candidate in candidates) {
354-
mapping[_parseScale(candidate)] = candidate;
348+
final SplayTreeMap<double, AssetMetadata> candidatesByDevicePixelRatio =
349+
SplayTreeMap<double, AssetMetadata>();
350+
for (final AssetMetadata candidate in candidateVariants) {
351+
candidatesByDevicePixelRatio[candidate.targetDevicePixelRatio ?? _naturalResolution] = candidate;
355352
}
356353
// TODO(ianh): implement support for config.locale, config.textDirection,
357354
// config.size, config.platform (then document this over in the Image.asset
358355
// docs)
359-
return _findBestVariant(mapping, config.devicePixelRatio!);
356+
return _findBestVariant(candidatesByDevicePixelRatio, config.devicePixelRatio!);
360357
}
361358

362359
// Returns the "best" asset variant amongst the available `candidates`.
@@ -371,48 +368,28 @@ class AssetImage extends AssetBundleImageProvider {
371368
// lowest key higher than `value`.
372369
// - If the screen has high device pixel ratio, choose the variant with the
373370
// key nearest to `value`.
374-
String? _findBestVariant(SplayTreeMap<double, String> candidates, double value) {
375-
if (candidates.containsKey(value)) {
376-
return candidates[value]!;
371+
AssetMetadata _findBestVariant(SplayTreeMap<double, AssetMetadata> candidatesByDpr, double value) {
372+
if (candidatesByDpr.containsKey(value)) {
373+
return candidatesByDpr[value]!;
377374
}
378-
final double? lower = candidates.lastKeyBefore(value);
379-
final double? upper = candidates.firstKeyAfter(value);
375+
final double? lower = candidatesByDpr.lastKeyBefore(value);
376+
final double? upper = candidatesByDpr.firstKeyAfter(value);
380377
if (lower == null) {
381-
return candidates[upper];
378+
return candidatesByDpr[upper]!;
382379
}
383380
if (upper == null) {
384-
return candidates[lower];
381+
return candidatesByDpr[lower]!;
385382
}
386383

387384
// On screens with low device-pixel ratios the artifacts from upscaling
388385
// images are more visible than on screens with a higher device-pixel
389386
// ratios because the physical pixels are larger. Choose the higher
390387
// resolution image in that case instead of the nearest one.
391388
if (value < _kLowDprLimit || value > (lower + upper) / 2) {
392-
return candidates[upper];
389+
return candidatesByDpr[upper]!;
393390
} else {
394-
return candidates[lower];
395-
}
396-
}
397-
398-
static final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$');
399-
400-
double _parseScale(String key) {
401-
if (key == assetName) {
402-
return _naturalResolution;
403-
}
404-
405-
final Uri assetUri = Uri.parse(key);
406-
String directoryPath = '';
407-
if (assetUri.pathSegments.length > 1) {
408-
directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2];
409-
}
410-
411-
final Match? match = _extractRatioRegExp.firstMatch(directoryPath);
412-
if (match != null && match.groupCount > 0) {
413-
return double.parse(match.group(1)!);
391+
return candidatesByDpr[lower]!;
414392
}
415-
return _naturalResolution; // i.e. default to 1.0x
416393
}
417394

418395
@override

packages/flutter/lib/src/services/asset_manifest.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class _AssetManifestBin implements AssetManifest {
7171
if (!_typeCastedData.containsKey(key)) {
7272
final Object? variantData = _data[key];
7373
if (variantData == null) {
74-
throw ArgumentError('Asset key $key was not found within the asset manifest.');
74+
throw ArgumentError('Asset key "$key" was not found.');
7575
}
7676
_typeCastedData[key] = ((_data[key] ?? <Object?>[]) as Iterable<Object?>)
7777
.cast<Map<Object?, Object?>>()

packages/flutter/test/painting/image_resolution_test.dart

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'dart:convert';
65
import 'dart:ui' as ui;
76

87
import 'package:flutter/foundation.dart';
@@ -13,18 +12,14 @@ import 'package:flutter_test/flutter_test.dart';
1312
class TestAssetBundle extends CachingAssetBundle {
1413
TestAssetBundle(this._assetBundleMap);
1514

16-
final Map<String, List<String>> _assetBundleMap;
15+
final Map<String, List<Map<Object?, Object?>>> _assetBundleMap;
1716

1817
Map<String, int> loadCallCount = <String, int>{};
1918

20-
String get _assetBundleContents {
21-
return json.encode(_assetBundleMap);
22-
}
23-
2419
@override
2520
Future<ByteData> load(String key) async {
26-
if (key == 'AssetManifest.json') {
27-
return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert(_assetBundleContents)).buffer);
21+
if (key == 'AssetManifest.bin') {
22+
return const StandardMessageCodec().encodeMessage(_assetBundleMap)!;
2823
}
2924

3025
loadCallCount[key] = loadCallCount[key] ?? 0 + 1;
@@ -45,9 +40,10 @@ class TestAssetBundle extends CachingAssetBundle {
4540
void main() {
4641
group('1.0 scale device tests', () {
4742
void buildAndTestWithOneAsset(String mainAssetPath) {
48-
final Map<String, List<String>> assetBundleMap = <String, List<String>>{};
43+
final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
44+
<String, List<Map<dynamic, dynamic>>>{};
4945

50-
assetBundleMap[mainAssetPath] = <String>[];
46+
assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[];
5147

5248
final AssetImage assetImage = AssetImage(
5349
mainAssetPath,
@@ -93,11 +89,13 @@ void main() {
9389
const String mainAssetPath = 'assets/normalFolder/normalFile.png';
9490
const String variantPath = 'assets/normalFolder/3.0x/normalFile.png';
9591

96-
final Map<String, List<String>> assetBundleMap =
97-
<String, List<String>>{};
98-
99-
assetBundleMap[mainAssetPath] = <String>[mainAssetPath, variantPath];
92+
final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
93+
<String, List<Map<dynamic, dynamic>>>{};
10094

95+
final Map<dynamic, dynamic> mainAssetVariantManifestEntry = <dynamic, dynamic>{};
96+
mainAssetVariantManifestEntry['asset'] = variantPath;
97+
mainAssetVariantManifestEntry['dpr'] = 3.0;
98+
assetBundleMap[mainAssetPath] = <Map<dynamic, dynamic>>[mainAssetVariantManifestEntry];
10199
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
102100

103101
final AssetImage assetImage = AssetImage(
@@ -123,10 +121,10 @@ void main() {
123121
test('When high-res device and high-res asset not present in bundle then return main variant', () {
124122
const String mainAssetPath = 'assets/normalFolder/normalFile.png';
125123

126-
final Map<String, List<String>> assetBundleMap =
127-
<String, List<String>>{};
124+
final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
125+
<String, List<Map<dynamic, dynamic>>>{};
128126

129-
assetBundleMap[mainAssetPath] = <String>[mainAssetPath];
127+
assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[];
130128

131129
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
132130

@@ -162,10 +160,13 @@ void main() {
162160
double chosenAssetRatio,
163161
String expectedAssetPath,
164162
) {
165-
final Map<String, List<String>> assetBundleMap =
166-
<String, List<String>>{};
163+
final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
164+
<String, List<Map<dynamic, dynamic>>>{};
167165

168-
assetBundleMap[mainAssetPath] = <String>[mainAssetPath, variantPath];
166+
final Map<dynamic, dynamic> mainAssetVariantManifestEntry = <dynamic, dynamic>{};
167+
mainAssetVariantManifestEntry['asset'] = variantPath;
168+
mainAssetVariantManifestEntry['dpr'] = 3.0;
169+
assetBundleMap[mainAssetPath] = <Map<dynamic, dynamic>>[mainAssetVariantManifestEntry];
169170

170171
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
171172

0 commit comments

Comments
 (0)