Skip to content

Commit 8d4fd89

Browse files
author
Jonah Williams
authored
Revert "Reland "Speed up first asset load by using the binary-formatted asset manifest for image resolution (#121322)" (#122449)
Revert "Reland "Speed up first asset load by using the binary-formatted asset manifest for image resolution"
1 parent aaa9cea commit 8d4fd89

File tree

5 files changed

+156
-93
lines changed

5 files changed

+156
-93
lines changed

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

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

55
import 'dart:async';
66
import 'dart:collection';
7+
import 'dart:convert';
78

89
import 'package:flutter/foundation.dart';
910
import 'package:flutter/services.dart';
1011

1112
import 'image_provider.dart';
1213

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

284-
AssetManifest.loadFromAssetBundle(chosenBundle)
285-
.then((AssetManifest manifest) {
286-
final Iterable<AssetMetadata>? candidateVariants = manifest.getAssetVariants(keyName);
287-
final AssetMetadata chosenVariant = _chooseVariant(
287+
chosenBundle.loadStructuredData<Map<String, List<String>>?>(_kAssetManifestFileName, manifestParser).then<void>(
288+
(Map<String, List<String>>? manifest) {
289+
final String chosenName = _chooseVariant(
288290
keyName,
289291
configuration,
290-
candidateVariants,
291-
);
292+
manifest == null ? null : manifest[keyName],
293+
)!;
294+
final double chosenScale = _parseScale(chosenName);
292295
final AssetBundleImageKey key = AssetBundleImageKey(
293296
bundle: chosenBundle,
294-
name: chosenVariant.key,
295-
scale: chosenVariant.targetDevicePixelRatio ?? _naturalResolution,
297+
name: chosenName,
298+
scale: chosenScale,
296299
);
297300
if (completer != null) {
298301
// We already returned from this function, which means we are in the
@@ -306,15 +309,14 @@ class AssetImage extends AssetBundleImageProvider {
306309
// ourselves.
307310
result = SynchronousFuture<AssetBundleImageKey>(key);
308311
}
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-
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+
});
318320
if (result != null) {
319321
// The code above ran synchronously, and came up with an answer.
320322
// Return the SynchronousFuture that we created above.
@@ -326,24 +328,35 @@ class AssetImage extends AssetBundleImageProvider {
326328
return completer.future;
327329
}
328330

329-
AssetMetadata _chooseVariant(String mainAssetKey, ImageConfiguration config, Iterable<AssetMetadata>? candidateVariants) {
330-
if (candidateVariants == null) {
331-
return AssetMetadata(key: mainAssetKey, targetDevicePixelRatio: null, main: true);
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);
332336
}
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+
}
333346

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

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

374387
// On screens with low device-pixel ratios the artifacts from upscaling
375388
// images are more visible than on screens with a higher device-pixel
376389
// ratios because the physical pixels are larger. Choose the higher
377390
// resolution image in that case instead of the nearest one.
378391
if (value < _kLowDprLimit || value > (lower + upper) / 2) {
379-
return candidatesByDpr[upper]!;
392+
return candidates[upper];
380393
} else {
381-
return candidatesByDpr[lower]!;
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)!);
382414
}
415+
return _naturalResolution; // i.e. default to 1.0x
383416
}
384417

385418
@override

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

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

33-
/// Retrieves metadata about an asset and its variants. Returns null if the
34-
/// key was not found in the asset manifest.
33+
/// Retrieves metadata about an asset and its variants.
3534
///
3635
/// This method considers a main asset to be a variant of itself and
3736
/// includes it in the returned list.
38-
List<AssetMetadata>? getAssetVariants(String key);
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);
3941
}
4042

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

6466
@override
65-
List<AssetMetadata>? getAssetVariants(String key) {
67+
List<AssetMetadata> getAssetVariants(String key) {
6668
// We lazily delay typecasting to prevent a performance hiccup when parsing
6769
// large asset manifests. This is important to keep an app's first asset
6870
// load fast.
6971
if (!_typeCastedData.containsKey(key)) {
7072
final Object? variantData = _data[key];
7173
if (variantData == null) {
72-
return null;
74+
throw ArgumentError('Asset key $key was not found within the asset manifest.');
7375
}
7476
_typeCastedData[key] = ((_data[key] ?? <Object?>[]) as Iterable<Object?>)
7577
.cast<Map<Object?, Object?>>()

packages/flutter/test/painting/image_resolution_test.dart

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
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';
56
import 'dart:ui' as ui;
67

78
import 'package:flutter/foundation.dart';
@@ -12,14 +13,18 @@ import 'package:flutter_test/flutter_test.dart';
1213
class TestAssetBundle extends CachingAssetBundle {
1314
TestAssetBundle(this._assetBundleMap);
1415

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

1718
Map<String, int> loadCallCount = <String, int>{};
1819

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

2530
loadCallCount[key] = loadCallCount[key] ?? 0 + 1;
@@ -40,10 +45,9 @@ class TestAssetBundle extends CachingAssetBundle {
4045
void main() {
4146
group('1.0 scale device tests', () {
4247
void buildAndTestWithOneAsset(String mainAssetPath) {
43-
final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
44-
<String, List<Map<Object?, Object?>>>{};
48+
final Map<String, List<String>> assetBundleMap = <String, List<String>>{};
4549

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

4852
final AssetImage assetImage = AssetImage(
4953
mainAssetPath,
@@ -89,13 +93,11 @@ void main() {
8993
const String mainAssetPath = 'assets/normalFolder/normalFile.png';
9094
const String variantPath = 'assets/normalFolder/3.0x/normalFile.png';
9195

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

95-
final Map<Object?, Object?> mainAssetVariantManifestEntry = <Object?, Object?>{};
96-
mainAssetVariantManifestEntry['asset'] = variantPath;
97-
mainAssetVariantManifestEntry['dpr'] = 3.0;
98-
assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[mainAssetVariantManifestEntry];
99101
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
100102

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

124-
final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
125-
<String, List<Map<Object?, Object?>>>{};
126+
final Map<String, List<String>> assetBundleMap =
127+
<String, List<String>>{};
126128

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

129131
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
130132

@@ -160,13 +162,10 @@ void main() {
160162
double chosenAssetRatio,
161163
String expectedAssetPath,
162164
) {
163-
final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
164-
<String, List<Map<Object?, Object?>>>{};
165+
final Map<String, List<String>> assetBundleMap =
166+
<String, List<String>>{};
165167

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

171170
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
172171

packages/flutter/test/services/asset_manifest_test.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ void main() {
4141

4242
expect(manifest.listAssets(), unorderedEquals(<String>['assets/foo.png', 'assets/bar.png']));
4343

44-
final List<AssetMetadata> fooVariants = manifest.getAssetVariants('assets/foo.png')!;
44+
final List<AssetMetadata> fooVariants = manifest.getAssetVariants('assets/foo.png');
4545
expect(fooVariants.length, 2);
4646
final AssetMetadata firstFooVariant = fooVariants[0];
4747
expect(firstFooVariant.key, 'assets/foo.png');
@@ -52,16 +52,17 @@ void main() {
5252
expect(secondFooVariant.targetDevicePixelRatio, 2.0);
5353
expect(secondFooVariant.main, false);
5454

55-
final List<AssetMetadata> barVariants = manifest.getAssetVariants('assets/bar.png')!;
55+
final List<AssetMetadata> barVariants = manifest.getAssetVariants('assets/bar.png');
5656
expect(barVariants.length, 1);
5757
final AssetMetadata firstBarVariant = barVariants[0];
5858
expect(firstBarVariant.key, 'assets/bar.png');
5959
expect(firstBarVariant.targetDevicePixelRatio, null);
6060
expect(firstBarVariant.main, true);
6161
});
6262

63-
test('getAssetVariants returns null if the key not contained in the asset manifest', () async {
63+
test('getAssetVariants throws if given a key not contained in the asset manifest', () async {
6464
final AssetManifest manifest = await AssetManifest.loadFromAssetBundle(TestAssetBundle());
65-
expect(manifest.getAssetVariants('invalid asset key'), isNull);
65+
66+
expect(() => manifest.getAssetVariants('invalid asset key'), throwsArgumentError);
6667
});
6768
}

0 commit comments

Comments
 (0)