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:typed_data' ;
6+
57import 'package:meta/meta.dart' ;
68import 'package:package_config/package_config.dart' ;
9+ import 'package:standard_message_codec/standard_message_codec.dart' ;
710
811import 'base/context.dart' ;
912import 'base/deferred_component.dart' ;
@@ -162,7 +165,11 @@ class ManifestAssetBundle implements AssetBundle {
162165
163166 DateTime ? _lastBuildTimestamp;
164167
165- static const String _kAssetManifestJson = 'AssetManifest.json' ;
168+ // We assume the main asset is designed for a device pixel ratio of 1.0.
169+ static const double _defaultResolution = 1.0 ;
170+ static const String _kAssetManifestJsonFilename = 'AssetManifest.json' ;
171+ static const String _kAssetManifestBinFilename = 'AssetManifest.bin' ;
172+
166173 static const String _kNoticeFile = 'NOTICES' ;
167174 // Comically, this can't be name with the more common .gz file extension
168175 // because when it's part of an AAR and brought into another APK via gradle,
@@ -230,8 +237,15 @@ class ManifestAssetBundle implements AssetBundle {
230237 // device.
231238 _lastBuildTimestamp = DateTime .now ();
232239 if (flutterManifest.isEmpty) {
233- entries[_kAssetManifestJson] = DevFSStringContent ('{}' );
234- entryKinds[_kAssetManifestJson] = AssetKind .regular;
240+ entries[_kAssetManifestJsonFilename] = DevFSStringContent ('{}' );
241+ entryKinds[_kAssetManifestJsonFilename] = AssetKind .regular;
242+ entries[_kAssetManifestJsonFilename] = DevFSStringContent ('{}' );
243+ entryKinds[_kAssetManifestJsonFilename] = AssetKind .regular;
244+ final ByteData emptyAssetManifest =
245+ const StandardMessageCodec ().encodeMessage (< dynamic , dynamic > {})! ;
246+ entries[_kAssetManifestBinFilename] =
247+ DevFSByteContent (emptyAssetManifest.buffer.asUint8List (0 , emptyAssetManifest.lengthInBytes));
248+ entryKinds[_kAssetManifestBinFilename] = AssetKind .regular;
235249 return 0 ;
236250 }
237251
@@ -428,7 +442,10 @@ class ManifestAssetBundle implements AssetBundle {
428442 _wildcardDirectories[uri] ?? = _fileSystem.directory (uri);
429443 }
430444
431- final DevFSStringContent assetManifest = _createAssetManifest (assetVariants, deferredComponentsAssetVariants);
445+ final Map <String , List <String >> assetManifest =
446+ _createAssetManifest (assetVariants, deferredComponentsAssetVariants);
447+ final DevFSStringContent assetManifestJson = DevFSStringContent (json.encode (assetManifest));
448+ final DevFSByteContent assetManifestBinary = _createAssetManifestBinary (assetManifest);
432449 final DevFSStringContent fontManifest = DevFSStringContent (json.encode (fonts));
433450 final LicenseResult licenseResult = _licenseCollector.obtainLicenses (packageConfig, additionalLicenseFiles);
434451 if (licenseResult.errorMessages.isNotEmpty) {
@@ -452,26 +469,40 @@ class ManifestAssetBundle implements AssetBundle {
452469 _fileSystem.file ('DOES_NOT_EXIST_RERUN_FOR_WILDCARD$suffix ' ).absolute);
453470 }
454471
455- _setIfChanged (_kAssetManifestJson, assetManifest, AssetKind .regular);
472+ _setIfChanged (_kAssetManifestJsonFilename, assetManifestJson, AssetKind .regular);
473+ _setIfChanged (_kAssetManifestBinFilename, assetManifestBinary, AssetKind .regular);
456474 _setIfChanged (kFontManifestJson, fontManifest, AssetKind .regular);
457475 _setLicenseIfChanged (licenseResult.combinedLicenses, targetPlatform);
458476 return 0 ;
459477 }
460478
461479 @override
462480 List <File > additionalDependencies = < File > [];
463-
464- void _setIfChanged (String key, DevFSStringContent content, AssetKind assetKind) {
465- if (! entries.containsKey (key)) {
466- entries[key] = content;
467- entryKinds[key] = assetKind;
481+ void _setIfChanged (String key, DevFSContent content, AssetKind assetKind) {
482+ final DevFSContent ? oldContent = entries[key];
483+ // In the case that the content is unchanged, we want to avoid an overwrite
484+ // as the isModified property may be reset to true,
485+ if (oldContent is DevFSByteContent && content is DevFSByteContent &&
486+ _compareIntLists (oldContent.bytes, content.bytes)) {
468487 return ;
469488 }
470- final DevFSStringContent ? oldContent = entries[key] as DevFSStringContent ? ;
471- if (oldContent? .string != content.string) {
472- entries[key] = content;
473- entryKinds[key] = assetKind;
489+
490+ entries[key] = content;
491+ entryKinds[key] = assetKind;
492+ }
493+
494+ static bool _compareIntLists (List <int > o1, List <int > o2) {
495+ if (o1.length != o2.length) {
496+ return false ;
474497 }
498+
499+ for (int index = 0 ; index < o1.length; index++ ) {
500+ if (o1[index] != o2[index]) {
501+ return false ;
502+ }
503+ }
504+
505+ return true ;
475506 }
476507
477508 void _setLicenseIfChanged (
@@ -623,39 +654,84 @@ class ManifestAssetBundle implements AssetBundle {
623654 return deferredComponentsAssetVariants;
624655 }
625656
626- DevFSStringContent _createAssetManifest (
657+ Map < String , List < String >> _createAssetManifest (
627658 Map <_Asset , List <_Asset >> assetVariants,
628659 Map <String , Map <_Asset , List <_Asset >>> deferredComponentsAssetVariants
629660 ) {
630- final Map <String , List <String >> jsonObject = < String , List <String >> {};
631- final Map <_Asset , List <String >> jsonEntries = < _Asset , List <String >> {};
661+ final Map <String , List <String >> manifest = < String , List <String >> {};
662+ final Map <_Asset , List <String >> entries = < _Asset , List <String >> {};
632663 assetVariants.forEach ((_Asset main, List <_Asset > variants) {
633- jsonEntries [main] = < String > [
664+ entries [main] = < String > [
634665 for (final _Asset variant in variants)
635666 variant.entryUri.path,
636667 ];
637668 });
638669 if (deferredComponentsAssetVariants != null ) {
639670 for (final Map <_Asset , List <_Asset >> componentAssets in deferredComponentsAssetVariants.values) {
640671 componentAssets.forEach ((_Asset main, List <_Asset > variants) {
641- jsonEntries [main] = < String > [
672+ entries [main] = < String > [
642673 for (final _Asset variant in variants)
643674 variant.entryUri.path,
644675 ];
645676 });
646677 }
647678 }
648- final List <_Asset > sortedKeys = jsonEntries .keys.toList ()
679+ final List <_Asset > sortedKeys = entries .keys.toList ()
649680 ..sort ((_Asset left, _Asset right) => left.entryUri.path.compareTo (right.entryUri.path));
650681 for (final _Asset main in sortedKeys) {
651682 final String decodedEntryPath = Uri .decodeFull (main.entryUri.path);
652- final List <String > rawEntryVariantsPaths = jsonEntries [main]! ;
683+ final List <String > rawEntryVariantsPaths = entries [main]! ;
653684 final List <String > decodedEntryVariantPaths = rawEntryVariantsPaths
654685 .map ((String value) => Uri .decodeFull (value))
655686 .toList ();
656- jsonObject[decodedEntryPath] = decodedEntryVariantPaths;
687+ manifest[decodedEntryPath] = decodedEntryVariantPaths;
688+ }
689+ return manifest;
690+ }
691+
692+ // Matches path-like strings ending in a number followed by an 'x'.
693+ // Example matches include "assets/animals/2.0x", "plants/3x", and "2.7x".
694+ static final RegExp _extractPixelRatioFromKeyRegExp = RegExp (r'/?(\d+(\.\d*)?)x$' );
695+
696+ DevFSByteContent _createAssetManifestBinary (
697+ Map <String , List <String >> assetManifest
698+ ) {
699+ double parseScale (String key) {
700+ final Uri assetUri = Uri .parse (key);
701+ String directoryPath = '' ;
702+ if (assetUri.pathSegments.length > 1 ) {
703+ directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2 ];
704+ }
705+
706+ final Match ? match = _extractPixelRatioFromKeyRegExp.firstMatch (directoryPath);
707+ if (match != null && match.groupCount > 0 ) {
708+ return double .parse (match.group (1 )! );
709+ }
710+ return _defaultResolution;
657711 }
658- return DevFSStringContent (json.encode (jsonObject));
712+
713+ final Map <String , dynamic > result = < String , dynamic > {};
714+
715+ for (final MapEntry <String , dynamic > manifestEntry in assetManifest.entries) {
716+ final List <dynamic > resultVariants = < dynamic > [];
717+ final List <String > entries = (manifestEntry.value as List <dynamic >).cast <String >();
718+ for (final String variant in entries) {
719+ if (variant == manifestEntry.key) {
720+ // With the newer binary format, don't include the main asset in it's
721+ // list of variants. This reduces parsing time at runtime.
722+ continue ;
723+ }
724+ final Map <String , dynamic > resultVariant = < String , dynamic > {};
725+ final double variantDevicePixelRatio = parseScale (variant);
726+ resultVariant['asset' ] = variant;
727+ resultVariant['dpr' ] = variantDevicePixelRatio;
728+ resultVariants.add (resultVariant);
729+ }
730+ result[manifestEntry.key] = resultVariants;
731+ }
732+
733+ final ByteData message = const StandardMessageCodec ().encodeMessage (result)! ;
734+ return DevFSByteContent (message.buffer.asUint8List (0 , message.lengthInBytes));
659735 }
660736
661737 /// Prefixes family names and asset paths of fonts included from packages with
0 commit comments