Skip to content

Commit 313b016

Browse files
authored
Reland "Speed up first asset load by using the binary-formatted asset manifest for image resolution" (#122505)
Reland "Speed up first asset load by using the binary-formatted asset manifest for image resolution"
1 parent e08d250 commit 313b016

File tree

7 files changed

+169
-215
lines changed

7 files changed

+169
-215
lines changed

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

Lines changed: 37 additions & 70 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 = manifest.getAssetVariants(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,24 @@ 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+
AssetMetadata _chooseVariant(String mainAssetKey, ImageConfiguration config, Iterable<AssetMetadata>? candidateVariants) {
330+
if (candidateVariants == null) {
331+
return AssetMetadata(key: mainAssetKey, targetDevicePixelRatio: null, main: true);
336332
}
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);
345-
}
346333

347-
String? _chooseVariant(String main, ImageConfiguration config, List<String>? candidates) {
348-
if (config.devicePixelRatio == null || candidates == null || candidates.isEmpty) {
349-
return main;
334+
if (config.devicePixelRatio == null) {
335+
return candidateVariants.firstWhere((AssetMetadata variant) => variant.main);
350336
}
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;
337+
338+
final SplayTreeMap<double, AssetMetadata> candidatesByDevicePixelRatio =
339+
SplayTreeMap<double, AssetMetadata>();
340+
for (final AssetMetadata candidate in candidateVariants) {
341+
candidatesByDevicePixelRatio[candidate.targetDevicePixelRatio ?? _naturalResolution] = candidate;
355342
}
356343
// TODO(ianh): implement support for config.locale, config.textDirection,
357344
// config.size, config.platform (then document this over in the Image.asset
358345
// docs)
359-
return _findBestVariant(mapping, config.devicePixelRatio!);
346+
return _findBestVariant(candidatesByDevicePixelRatio, config.devicePixelRatio!);
360347
}
361348

362349
// Returns the "best" asset variant amongst the available `candidates`.
@@ -371,48 +358,28 @@ class AssetImage extends AssetBundleImageProvider {
371358
// lowest key higher than `value`.
372359
// - If the screen has high device pixel ratio, choose the variant with the
373360
// key nearest to `value`.
374-
String? _findBestVariant(SplayTreeMap<double, String> candidates, double value) {
375-
if (candidates.containsKey(value)) {
376-
return candidates[value]!;
361+
AssetMetadata _findBestVariant(SplayTreeMap<double, AssetMetadata> candidatesByDpr, double value) {
362+
if (candidatesByDpr.containsKey(value)) {
363+
return candidatesByDpr[value]!;
377364
}
378-
final double? lower = candidates.lastKeyBefore(value);
379-
final double? upper = candidates.firstKeyAfter(value);
365+
final double? lower = candidatesByDpr.lastKeyBefore(value);
366+
final double? upper = candidatesByDpr.firstKeyAfter(value);
380367
if (lower == null) {
381-
return candidates[upper];
368+
return candidatesByDpr[upper]!;
382369
}
383370
if (upper == null) {
384-
return candidates[lower];
371+
return candidatesByDpr[lower]!;
385372
}
386373

387374
// On screens with low device-pixel ratios the artifacts from upscaling
388375
// images are more visible than on screens with a higher device-pixel
389376
// ratios because the physical pixels are larger. Choose the higher
390377
// resolution image in that case instead of the nearest one.
391378
if (value < _kLowDprLimit || value > (lower + upper) / 2) {
392-
return candidates[upper];
379+
return candidatesByDpr[upper]!;
393380
} 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)!);
381+
return candidatesByDpr[lower]!;
414382
}
415-
return _naturalResolution; // i.e. default to 1.0x
416383
}
417384

418385
@override

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ abstract class CachingAssetBundle extends AssetBundle {
266266
.then<T>(parser)
267267
.then<void>((T value) {
268268
result = SynchronousFuture<T>(value);
269+
_structuredBinaryDataCache[key] = result!;
269270
if (completer != null) {
270271
// The load and parse operation ran asynchronously. We already returned
271272
// from the loadStructuredBinaryData function and therefore the caller
@@ -278,7 +279,6 @@ abstract class CachingAssetBundle extends AssetBundle {
278279

279280
if (result != null) {
280281
// The above code ran synchronously. We can synchronously return the result.
281-
_structuredBinaryDataCache[key] = result!;
282282
return result!;
283283
}
284284

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,12 @@ abstract class AssetManifest {
3030
/// information.
3131
List<String> listAssets();
3232

33-
/// Retrieves metadata about an asset and its variants.
33+
/// Retrieves metadata about an asset and its variants. Returns null if the
34+
/// key was not found in the asset manifest.
3435
///
3536
/// This method considers a main asset to be a variant of itself and
3637
/// includes it in the returned list.
37-
///
38-
/// Throws an [ArgumentError] if [key] cannot be found within the manifest. To
39-
/// avoid this, use a key obtained from the [listAssets] method.
40-
List<AssetMetadata> getAssetVariants(String key);
38+
List<AssetMetadata>? getAssetVariants(String key);
4139
}
4240

4341
// Lazily parses the binary asset manifest into a data structure that's easier to work
@@ -64,14 +62,14 @@ class _AssetManifestBin implements AssetManifest {
6462
final Map<String, List<AssetMetadata>> _typeCastedData = <String, List<AssetMetadata>>{};
6563

6664
@override
67-
List<AssetMetadata> getAssetVariants(String key) {
65+
List<AssetMetadata>? getAssetVariants(String key) {
6866
// We lazily delay typecasting to prevent a performance hiccup when parsing
6967
// large asset manifests. This is important to keep an app's first asset
7068
// load fast.
7169
if (!_typeCastedData.containsKey(key)) {
7270
final Object? variantData = _data[key];
7371
if (variantData == null) {
74-
throw ArgumentError('Asset key $key was not found within the asset manifest.');
72+
return null;
7573
}
7674
_typeCastedData[key] = ((_data[key] ?? <Object?>[]) as Iterable<Object?>)
7775
.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<Object?, Object?>>> assetBundleMap =
44+
<String, List<Map<Object?, Object?>>>{};
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<Object?, Object?>>> assetBundleMap =
93+
<String, List<Map<Object?, Object?>>>{};
10094

95+
final Map<Object?, Object?> mainAssetVariantManifestEntry = <Object?, Object?>{};
96+
mainAssetVariantManifestEntry['asset'] = variantPath;
97+
mainAssetVariantManifestEntry['dpr'] = 3.0;
98+
assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[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<Object?, Object?>>> assetBundleMap =
125+
<String, List<Map<Object?, Object?>>>{};
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<Object?, Object?>>> assetBundleMap =
164+
<String, List<Map<Object?, Object?>>>{};
167165

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

170171
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
171172

0 commit comments

Comments
 (0)