Move UTF-8 decoding off the main thread. (#14206)

This reduces the jank of bringing up the license screen further.
The remaining lost frame or two are caused by Dart itself, see:

   https://github.com/dart-lang/sdk/issues/31954
   https://github.com/dart-lang/sdk/issues/31959
   https://github.com/dart-lang/sdk/issues/31960

Fixes https://github.com/flutter/flutter/issues/5187
diff --git a/packages/flutter/lib/src/services/asset_bundle.dart b/packages/flutter/lib/src/services/asset_bundle.dart
index 5e38f9b..29f0178 100644
--- a/packages/flutter/lib/src/services/asset_bundle.dart
+++ b/packages/flutter/lib/src/services/asset_bundle.dart
@@ -160,6 +160,15 @@
     final ByteData data = await load(key);
     if (data == null)
       throw new FlutterError('Unable to load asset: $key');
+    if (data.lengthInBytes < 10 * 1024) {
+      // 10KB takes about 3ms to parse on a Pixel 2 XL.
+      // See: https://github.com/dart-lang/sdk/issues/31954
+      return UTF8.decode(data.buffer.asUint8List());
+    }
+    return compute(_utf8decode, data, debugLabel: 'UTF8 decode for "$key"');
+  }
+
+  static String _utf8decode(ByteData data) {
     return UTF8.decode(data.buffer.asUint8List());
   }
 
diff --git a/packages/flutter/lib/src/services/binding.dart b/packages/flutter/lib/src/services/binding.dart
index f4e1919..676ac38 100644
--- a/packages/flutter/lib/src/services/binding.dart
+++ b/packages/flutter/lib/src/services/binding.dart
@@ -40,9 +40,32 @@
   }
 
   Stream<LicenseEntry> _addLicenses() async* {
-    final String rawLicenses = await rootBundle.loadString('LICENSE', cache: false);
-    final List<LicenseEntry> licenses = await compute(_parseLicenses, rawLicenses, debugLabel: 'parseLicenses');
-    yield* new Stream<LicenseEntry>.fromIterable(licenses);
+    // We use timers here (rather than scheduleTask from the scheduler binding)
+    // because the services layer can't use the scheduler binding (the scheduler
+    // binding uses the services layer to manage its lifecycle events). Timers
+    // are what scheduleTask uses under the hood anyway. The only difference is
+    // that these will just run next, instead of being prioritized relative to
+    // the other tasks that might be running. Using _something_ here to break
+    // this into two parts is important because isolates take a while to copy
+    // data at the moment, and if we receive the data in the same event loop
+    // iteration as we send the data to the next isolate, we are definitely
+    // going to miss frames. Another solution would be to have the work all
+    // happen in one isolate, and we may go there eventually, but first we are
+    // going to see if isolate communication can be made cheaper.
+    // See: https://github.com/dart-lang/sdk/issues/31959
+    //      https://github.com/dart-lang/sdk/issues/31960
+    // TODO(ianh): Remove this complexity once these bugs are fixed.
+    final Completer<String> rawLicenses = new Completer<String>();
+    Timer.run(() async {
+      rawLicenses.complete(rootBundle.loadString('LICENSE', cache: false));
+    });
+    await rawLicenses.future;
+    final Completer<List<LicenseEntry>> parsedLicenses = new Completer<List<LicenseEntry>>();
+    Timer.run(() async {
+      parsedLicenses.complete(compute(_parseLicenses, await rawLicenses.future, debugLabel: 'parseLicenses'));
+    });
+    await parsedLicenses.future;
+    yield* new Stream<LicenseEntry>.fromIterable(await parsedLicenses.future);
   }
 
   // This is run in another isolate created by _addLicenses above.