Revert "Speed up first asset load by using the binary-formatted asset manifest for image resolution (#118782)" (#121268)
This reverts commit e3db0488adaf1ca1330c800bef9aa06749d30a7a.
diff --git a/dev/integration_tests/ui/test/asset_test.dart b/dev/integration_tests/ui/test/asset_test.dart
index 6819fda..2f3e1c4 100644
--- a/dev/integration_tests/ui/test/asset_test.dart
+++ b/dev/integration_tests/ui/test/asset_test.dart
@@ -14,6 +14,6 @@
// If this asset couldn't be loaded, the exception message would be
// "asset failed to load"
- expect(tester.takeException().toString(), contains('The key was not found in the asset manifest'));
+ expect(tester.takeException().toString(), contains('Invalid image data'));
});
}
diff --git a/packages/flutter/lib/src/painting/image_resolution.dart b/packages/flutter/lib/src/painting/image_resolution.dart
index bc5fed8..3c20277 100644
--- a/packages/flutter/lib/src/painting/image_resolution.dart
+++ b/packages/flutter/lib/src/painting/image_resolution.dart
@@ -4,12 +4,15 @@
import 'dart:async';
import 'dart:collection';
+import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'image_provider.dart';
+const String _kAssetManifestFileName = 'AssetManifest.json';
+
/// A screen with a device-pixel ratio strictly less than this value is
/// considered a low-resolution screen (typically entry-level to mid-range
/// laptops, desktop screens up to QHD, low-end tablets such as Kindle Fire).
@@ -281,18 +284,18 @@
Completer<AssetBundleImageKey>? completer;
Future<AssetBundleImageKey>? result;
- AssetManifest.loadFromAssetBundle(chosenBundle)
- .then((AssetManifest manifest) {
- final Iterable<AssetMetadata> candidateVariants = _getVariants(manifest, keyName);
- final AssetMetadata chosenVariant = _chooseVariant(
+ chosenBundle.loadStructuredData<Map<String, List<String>>?>(_kAssetManifestFileName, manifestParser).then<void>(
+ (Map<String, List<String>>? manifest) {
+ final String chosenName = _chooseVariant(
keyName,
configuration,
- candidateVariants,
- );
+ manifest == null ? null : manifest[keyName],
+ )!;
+ final double chosenScale = _parseScale(chosenName);
final AssetBundleImageKey key = AssetBundleImageKey(
bundle: chosenBundle,
- name: chosenVariant.key,
- scale: chosenVariant.targetDevicePixelRatio ?? _naturalResolution,
+ name: chosenName,
+ scale: chosenScale,
);
if (completer != null) {
// We already returned from this function, which means we are in the
@@ -306,15 +309,14 @@
// ourselves.
result = SynchronousFuture<AssetBundleImageKey>(key);
}
- })
- .onError((Object error, StackTrace stack) {
- // We had an error. (This guarantees we weren't called synchronously.)
- // Forward the error to the caller.
- assert(completer != null);
- assert(result == null);
- completer!.completeError(error, stack);
- });
-
+ },
+ ).catchError((Object error, StackTrace stack) {
+ // We had an error. (This guarantees we weren't called synchronously.)
+ // Forward the error to the caller.
+ assert(completer != null);
+ assert(result == null);
+ completer!.completeError(error, stack);
+ });
if (result != null) {
// The code above ran synchronously, and came up with an answer.
// Return the SynchronousFuture that we created above.
@@ -326,34 +328,35 @@
return completer.future;
}
- Iterable<AssetMetadata> _getVariants(AssetManifest manifest, String key) {
- try {
- return manifest.getAssetVariants(key);
- } catch (e) {
- throw FlutterError.fromParts(<DiagnosticsNode>[
- ErrorSummary('Unable to load asset with key "$key".'),
- ErrorDescription(
-'''
-The key was not found in the asset manifest.
-Make sure the key is correct and the appropriate file or folder is specified in pubspec.yaml.
-'''),
- ]);
+ /// Parses the asset manifest string into a strongly-typed map.
+ @visibleForTesting
+ static Future<Map<String, List<String>>?> manifestParser(String? jsonData) {
+ if (jsonData == null) {
+ return SynchronousFuture<Map<String, List<String>>?>(null);
}
+ // TODO(ianh): JSON decoding really shouldn't be on the main thread.
+ final Map<String, dynamic> parsedJson = json.decode(jsonData) as Map<String, dynamic>;
+ final Iterable<String> keys = parsedJson.keys;
+ final Map<String, List<String>> parsedManifest = <String, List<String>> {
+ for (final String key in keys) key: List<String>.from(parsedJson[key] as List<dynamic>),
+ };
+ // TODO(ianh): convert that data structure to the right types.
+ return SynchronousFuture<Map<String, List<String>>?>(parsedManifest);
}
- AssetMetadata _chooseVariant(String mainAssetKey, ImageConfiguration config, Iterable<AssetMetadata> candidateVariants) {
- if (config.devicePixelRatio == null || candidateVariants.isEmpty) {
- return candidateVariants.firstWhere((AssetMetadata variant) => variant.main);
+ String? _chooseVariant(String main, ImageConfiguration config, List<String>? candidates) {
+ if (config.devicePixelRatio == null || candidates == null || candidates.isEmpty) {
+ return main;
}
- final SplayTreeMap<double, AssetMetadata> candidatesByDevicePixelRatio =
- SplayTreeMap<double, AssetMetadata>();
- for (final AssetMetadata candidate in candidateVariants) {
- candidatesByDevicePixelRatio[candidate.targetDevicePixelRatio ?? _naturalResolution] = candidate;
+ // TODO(ianh): Consider moving this parsing logic into _manifestParser.
+ final SplayTreeMap<double, String> mapping = SplayTreeMap<double, String>();
+ for (final String candidate in candidates) {
+ mapping[_parseScale(candidate)] = candidate;
}
// TODO(ianh): implement support for config.locale, config.textDirection,
// config.size, config.platform (then document this over in the Image.asset
// docs)
- return _findBestVariant(candidatesByDevicePixelRatio, config.devicePixelRatio!);
+ return _findBestVariant(mapping, config.devicePixelRatio!);
}
// Returns the "best" asset variant amongst the available `candidates`.
@@ -368,17 +371,17 @@
// lowest key higher than `value`.
// - If the screen has high device pixel ratio, choose the variant with the
// key nearest to `value`.
- AssetMetadata _findBestVariant(SplayTreeMap<double, AssetMetadata> candidatesByDpr, double value) {
- if (candidatesByDpr.containsKey(value)) {
- return candidatesByDpr[value]!;
+ String? _findBestVariant(SplayTreeMap<double, String> candidates, double value) {
+ if (candidates.containsKey(value)) {
+ return candidates[value]!;
}
- final double? lower = candidatesByDpr.lastKeyBefore(value);
- final double? upper = candidatesByDpr.firstKeyAfter(value);
+ final double? lower = candidates.lastKeyBefore(value);
+ final double? upper = candidates.firstKeyAfter(value);
if (lower == null) {
- return candidatesByDpr[upper]!;
+ return candidates[upper];
}
if (upper == null) {
- return candidatesByDpr[lower]!;
+ return candidates[lower];
}
// On screens with low device-pixel ratios the artifacts from upscaling
@@ -386,12 +389,32 @@
// ratios because the physical pixels are larger. Choose the higher
// resolution image in that case instead of the nearest one.
if (value < _kLowDprLimit || value > (lower + upper) / 2) {
- return candidatesByDpr[upper]!;
+ return candidates[upper];
} else {
- return candidatesByDpr[lower]!;
+ return candidates[lower];
}
}
+ static final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$');
+
+ double _parseScale(String key) {
+ if (key == assetName) {
+ return _naturalResolution;
+ }
+
+ final Uri assetUri = Uri.parse(key);
+ String directoryPath = '';
+ if (assetUri.pathSegments.length > 1) {
+ directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2];
+ }
+
+ final Match? match = _extractRatioRegExp.firstMatch(directoryPath);
+ if (match != null && match.groupCount > 0) {
+ return double.parse(match.group(1)!);
+ }
+ return _naturalResolution; // i.e. default to 1.0x
+ }
+
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
diff --git a/packages/flutter/lib/src/services/asset_manifest.dart b/packages/flutter/lib/src/services/asset_manifest.dart
index fe41abd..4de568a 100644
--- a/packages/flutter/lib/src/services/asset_manifest.dart
+++ b/packages/flutter/lib/src/services/asset_manifest.dart
@@ -71,7 +71,7 @@
if (!_typeCastedData.containsKey(key)) {
final Object? variantData = _data[key];
if (variantData == null) {
- throw ArgumentError('Asset key "$key" was not found.');
+ throw ArgumentError('Asset key $key was not found within the asset manifest.');
}
_typeCastedData[key] = ((_data[key] ?? <Object?>[]) as Iterable<Object?>)
.cast<Map<Object?, Object?>>()
diff --git a/packages/flutter/test/painting/image_resolution_test.dart b/packages/flutter/test/painting/image_resolution_test.dart
index 84eaad3..8e04f2a 100644
--- a/packages/flutter/test/painting/image_resolution_test.dart
+++ b/packages/flutter/test/painting/image_resolution_test.dart
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:convert';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
@@ -12,14 +13,18 @@
class TestAssetBundle extends CachingAssetBundle {
TestAssetBundle(this._assetBundleMap);
- final Map<String, List<Map<Object?, Object?>>> _assetBundleMap;
+ final Map<String, List<String>> _assetBundleMap;
Map<String, int> loadCallCount = <String, int>{};
+ String get _assetBundleContents {
+ return json.encode(_assetBundleMap);
+ }
+
@override
Future<ByteData> load(String key) async {
- if (key == 'AssetManifest.bin') {
- return const StandardMessageCodec().encodeMessage(_assetBundleMap)!;
+ if (key == 'AssetManifest.json') {
+ return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert(_assetBundleContents)).buffer);
}
loadCallCount[key] = loadCallCount[key] ?? 0 + 1;
@@ -40,10 +45,9 @@
void main() {
group('1.0 scale device tests', () {
void buildAndTestWithOneAsset(String mainAssetPath) {
- final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
- <String, List<Map<dynamic, dynamic>>>{};
+ final Map<String, List<String>> assetBundleMap = <String, List<String>>{};
- assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[];
+ assetBundleMap[mainAssetPath] = <String>[];
final AssetImage assetImage = AssetImage(
mainAssetPath,
@@ -89,13 +93,11 @@
const String mainAssetPath = 'assets/normalFolder/normalFile.png';
const String variantPath = 'assets/normalFolder/3.0x/normalFile.png';
- final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
- <String, List<Map<dynamic, dynamic>>>{};
+ final Map<String, List<String>> assetBundleMap =
+ <String, List<String>>{};
- final Map<dynamic, dynamic> mainAssetVariantManifestEntry = <dynamic, dynamic>{};
- mainAssetVariantManifestEntry['asset'] = variantPath;
- mainAssetVariantManifestEntry['dpr'] = 3.0;
- assetBundleMap[mainAssetPath] = <Map<dynamic, dynamic>>[mainAssetVariantManifestEntry];
+ assetBundleMap[mainAssetPath] = <String>[mainAssetPath, variantPath];
+
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
final AssetImage assetImage = AssetImage(
@@ -121,10 +123,10 @@
test('When high-res device and high-res asset not present in bundle then return main variant', () {
const String mainAssetPath = 'assets/normalFolder/normalFile.png';
- final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
- <String, List<Map<dynamic, dynamic>>>{};
+ final Map<String, List<String>> assetBundleMap =
+ <String, List<String>>{};
- assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[];
+ assetBundleMap[mainAssetPath] = <String>[mainAssetPath];
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
@@ -160,13 +162,10 @@
double chosenAssetRatio,
String expectedAssetPath,
) {
- final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
- <String, List<Map<dynamic, dynamic>>>{};
+ final Map<String, List<String>> assetBundleMap =
+ <String, List<String>>{};
- final Map<dynamic, dynamic> mainAssetVariantManifestEntry = <dynamic, dynamic>{};
- mainAssetVariantManifestEntry['asset'] = variantPath;
- mainAssetVariantManifestEntry['dpr'] = 3.0;
- assetBundleMap[mainAssetPath] = <Map<dynamic, dynamic>>[mainAssetVariantManifestEntry];
+ assetBundleMap[mainAssetPath] = <String>[mainAssetPath, variantPath];
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
diff --git a/packages/flutter/test/widgets/image_resolution_test.dart b/packages/flutter/test/widgets/image_resolution_test.dart
index 7312537..fe836c1 100644
--- a/packages/flutter/test/widgets/image_resolution_test.dart
+++ b/packages/flutter/test/widgets/image_resolution_test.dart
@@ -5,7 +5,6 @@
@TestOn('!chrome')
library;
-import 'dart:convert';
import 'dart:ui' as ui show Image;
import 'package:flutter/foundation.dart';
@@ -19,31 +18,27 @@
ByteData testByteData(double scale) => ByteData(8)..setFloat64(0, scale);
double scaleOf(ByteData data) => data.getFloat64(0);
-final Map<dynamic, dynamic> testManifest = json.decode('''
+const String testManifest = '''
{
"assets/image.png" : [
- {"asset": "assets/1.5x/image.png", "dpr": 1.5},
- {"asset": "assets/2.0x/image.png", "dpr": 2.0},
- {"asset": "assets/3.0x/image.png", "dpr": 3.0},
- {"asset": "assets/4.0x/image.png", "dpr": 4.0}
+ "assets/image.png",
+ "assets/1.5x/image.png",
+ "assets/2.0x/image.png",
+ "assets/3.0x/image.png",
+ "assets/4.0x/image.png"
]
}
-''') as Map<Object?, Object?>;
+''';
class TestAssetBundle extends CachingAssetBundle {
- TestAssetBundle({ required Map<dynamic, dynamic> manifest }) {
- this.manifest = const StandardMessageCodec().encodeMessage(manifest)!;
- }
+ TestAssetBundle({ this.manifest = testManifest });
- late final ByteData manifest;
+ final String manifest;
@override
Future<ByteData> load(String key) {
late ByteData data;
switch (key) {
- case 'AssetManifest.bin':
- data = manifest;
- break;
case 'assets/image.png':
data = testByteData(1.0);
break;
@@ -67,6 +62,14 @@
}
@override
+ Future<String> loadString(String key, { bool cache = true }) {
+ if (key == 'AssetManifest.json') {
+ return SynchronousFuture<String>(manifest);
+ }
+ return SynchronousFuture<String>('');
+ }
+
+ @override
String toString() => '${describeIdentity(this)}()';
}
@@ -104,7 +107,7 @@
devicePixelRatio: ratio,
),
child: DefaultAssetBundle(
- bundle: bundle ?? TestAssetBundle(manifest: testManifest),
+ bundle: bundle ?? TestAssetBundle(),
child: Center(
child: inferSize ?
Image(
@@ -257,21 +260,46 @@
expect(getRenderImage(tester, key).scale, 4.0);
});
+ testWidgets('Image for device pixel ratio 1.0, with no main asset', (WidgetTester tester) async {
+ const String manifest = '''
+ {
+ "assets/image.png" : [
+ "assets/1.5x/image.png",
+ "assets/2.0x/image.png",
+ "assets/3.0x/image.png",
+ "assets/4.0x/image.png"
+ ]
+ }
+ ''';
+ final AssetBundle bundle = TestAssetBundle(manifest: manifest);
+
+ const double ratio = 1.0;
+ Key key = GlobalKey();
+ await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images, bundle));
+ expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
+ expect(getRenderImage(tester, key).scale, 1.5);
+ key = GlobalKey();
+ await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images, bundle));
+ expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
+ expect(getRenderImage(tester, key).scale, 1.5);
+ });
+
testWidgets('Image for device pixel ratio 1.0, with a main asset and a 1.0x asset', (WidgetTester tester) async {
// If both a main asset and a 1.0x asset are specified, then prefer
// the 1.0x asset.
- final Map<Object?, Object?> manifest = json.decode('''
+ const String manifest = '''
{
"assets/image.png" : [
- {"asset": "assets/1.0x/image.png", "dpr": 1.0},
- {"asset": "assets/1.5x/image.png", "dpr": 1.5},
- {"asset": "assets/2.0x/image.png", "dpr": 2.0},
- {"asset": "assets/3.0x/image.png", "dpr": 3.0},
- {"asset": "assets/4.0x/image.png", "dpr": 4.0}
+ "assets/image.png",
+ "assets/1.0x/image.png",
+ "assets/1.5x/image.png",
+ "assets/2.0x/image.png",
+ "assets/3.0x/image.png",
+ "assets/4.0x/image.png"
]
}
- ''') as Map<Object?, Object?>;
+ ''';
final AssetBundle bundle = TestAssetBundle(manifest: manifest);
const double ratio = 1.0;
@@ -310,14 +338,14 @@
// if higher resolution assets are not available we will pick the best
// available.
testWidgets('Low-resolution assets', (WidgetTester tester) async {
- final Map<Object?, Object?> manifest = json.decode('''
+ final AssetBundle bundle = TestAssetBundle(manifest: '''
{
"assets/image.png" : [
- {"asset": "assets/1.5x/image.png", "dpr": 1.5}
+ "assets/image.png",
+ "assets/1.5x/image.png"
]
}
- ''') as Map<Object?, Object?>;
- final AssetBundle bundle = TestAssetBundle(manifest: manifest);
+ ''');
Future<void> testRatio({required double ratio, required double expectedScale}) async {
Key key = GlobalKey();
diff --git a/packages/flutter/test/widgets/image_test.dart b/packages/flutter/test/widgets/image_test.dart
index 52eb2b4..232a45b 100644
--- a/packages/flutter/test/widgets/image_test.dart
+++ b/packages/flutter/test/widgets/image_test.dart
@@ -2000,9 +2000,10 @@
);
expect(
tester.takeException().toString(),
- equals('Unable to load asset with key "missing-asset".\n'
- 'The key was not found in the asset manifest.\n'
- 'Make sure the key is correct and the appropriate file or folder is specified in pubspec.yaml.'),
+ equals(
+ 'Unable to load asset: "missing-asset".\n'
+ 'The asset does not exist or has empty data.',
+ ),
);
await tester.pump();
await expectLater(