Изолят представляет собой независимый Dart-воркер со своей памятью и своим event loop, поэтому он выполняется по-настоящему параллельно с UI-изолятом. Выносить в него стоит работу, которая нагружает CPU и на главном изоляте заняла бы дольше кадра (около 16 мс).
Нет общего состояния: изоляты не видят объекты друг друга, поэтому блокировки не нужны; общаются они только сообщениями, которые копируются (неизменяемые значения вроде строк могут передаваться без копии)
**Isolate.run()**: одна функция на входе, один результат на выходе; выбор по умолчанию. Функция compute() из Flutter делает то же самое.
**Isolate.spawn() с ReceivePort: долгоживущий воркер, который обрабатывает много задач
Подходит: декодирование большого JSON, обработка изображений, хеширование, сжатие
Не подходит: ожидание сети или диска (это и так асинхронно и ничего не блокирует), мелкие задачи, где копирование дороже самой работы, всё, что трогает виджеты
Web**: в вебе изолятов нет; compute() выполняет функцию в том же потоке
FlutterMiddle
Продвинутый Dart
Обобщения, расширения, примеси, изоляты, продвинутый dart:core
Закрыто вопросов
0 / 15
- 0Повторить
- 3Часто спрашивают
Вопросы
late позволяет присвоить non-nullable переменную после объявления. Компилятор перестаёт проверять, что она задана, а чтение до первого присваивания бросает LateInitializationError во время выполнения.
Отложенная инициализация: late String _draft; присваивается в initState, и поле State остаётся non-nullable без значения-заглушки
Ленивая инициализация: late final _anim = AnimationController(vsync: this); выполняет инициализатор при первом чтении, поэтому в нём и можно использовать this
И так ленивые: top-level и static переменные инициализируются при первом обращении; late там ничего не добавляет
Неуместен, когда значения может не быть: это T?; late лишь превращает проверку на null в краш
Неуместен для асинхронных результатов: промежуток до прихода данных является реальным состоянием; моделируйте загрузку и ошибку явно
Ничего не совпало. firstWhere должен вернуть элемент типа E, поэтому, когда ни один элемент не проходит проверку, он бросает StateError («No element»), а не выдумывает значение.
То же семейство: first, last, single и reduce на пустой коллекции; singleWhere к тому же бросает "Too many elements" при двух совпадениях
**orElse: () => fallback: когда настоящее значение по умолчанию существует; оно должно быть того же типа, что и элементы
where(test).firstOrNull**: null вместо исключения; firstOrNull есть в dart:core начиная с Dart 3.0
**firstWhereOrNull**: то же одним вызовом, из package:collection
**fold вместо reduce**: fold принимает начальное значение, поэтому пустой список не вызывает ошибки
Дженерик-класс или функция принимает параметр типа, который подставляет вызывающий код: Page<T> превращается в Page<User>. Ограничение вроде T extends Comparable<T> сужает набор допустимых типов и позволяет телу вызывать методы этого типа.
Типобезопасность без копипасты: один Page<T>, Result<T> или виджет AsyncList<T> обслуживает любую модель, а компилятор всё равно проверяет item.name в каждом месте вызова
Классы и функции: у дженерик-класса T фиксируется для экземпляра (Page<User>), у дженерик-функции фиксируется для вызова (T first<T>(List<T> xs))
Только верхние границы: есть extends, но нет super; без ограничения T ведёт себя как Object?
Вывод типов: maxOf(['a', 'b']) выводит T = String; если выводить не из чего, T становится своим ограничением или dynamic, поэтому указывайте тип явно
**В сравнении с Object или dynamic**: они тоже компилируются, но вы теряете проверки на этапе компиляции и вынуждены приводить типы при каждом использовании
Expando привязывает данные к объекту, и ключ должен быть настоящим объектом. photo.id имеет тип int, а числа, строки, булевы значения, records и null отвергаются, поэтому уже первое чтение _meta[photo.id] бросает ArgumentError.
Почему падает: Expando держит ключи слабо, поэтому идентичность ключа должна что-то значить; у int и строк собственной идентичности нет, а равные значения могут быть как одним экземпляром, так и разными
Это не Map: Expando<Meta> похож на Map<int, Meta>, но им не является: у него нет keys и length, а запись исчезает, когда объект-ключ собран сборщиком мусора
Исправление: использовать сам объект как ключ, _meta[photo], тогда данные живут ровно столько же, сколько Photo; если данные нужно искать по id и хранить дольше объекта, берите Map<int, Meta> и удаляйте записи сами
Как поймать: падает при первом вызове, поэтому хватит одного юнит-теста; на ревью считайте багом любой индекс Expando, который не является обычной ссылкой на объект
Она выведет animal, woof, meow, а затем бросит NoSuchMethodError. Extension-члены выбираются на этапе компиляции по статическому типу получателя; реальный тип объекта в выборе не участвует.
**a.speak()**: a объявлена как Animal, поэтому выполняется AnimalX, хотя объект имеет тип Dog; extension не работает как override
**Dog().speak()**: подходят оба расширения, и побеждает то, у которого on-тип конкретнее, то есть DogX
**Cat().speak()**: настоящий метод экземпляра всегда важнее расширения, поэтому CatX.speak оказывается мёртвым кодом
**d.speak()**: у dynamic-получателя нет статического типа для сопоставления, поэтому вызов ищется у объекта во время выполнения и падает
Вывод: extension представляет собой статический хелпер с удобным синтаксисом; поведение, которое должно зависеть от реального типа, принадлежит методу класса или sealed-иерархии.
map, where, expand, take и skip возвращают ленивый Iterable, а не список: ваш колбэк выполняется только тогда, когда результат действительно обходят.
Обычно это плюс: items.where(match).take(3) останавливается на третьем совпадении, а не сканирует весь список.
Побочные эффекты не выполняются: items.map(save) выглядит как цикл и ничего не делает; используйте for
Пересчёт при каждом обходе: каждый .length, .first или цикл по результату where(...) снова вызывает предикат
Источник читается вживую: измените список до обхода, и представление увидит изменение, а изменение во время обхода бросит ConcurrentModificationError
Правило: вызывайте toList(), как только результат используется больше одного раза, и возвращайте List из своих API, если ленивость не является целью
Генератор выдаёт последовательность по одному значению через yield. Функция sync* возвращает Iterable, значения которого вычисляются по запросу; функция async* возвращает Stream и может делать await между значениями.
**yield и yield*: отдать одно значение или все значения другого iterable или стрима
sync*** нужен для последовательностей, которые вычисляются пошагово: обход дерева, перебор комбинаций, скользящее окно; вызывающий код может остановиться раньше через take или first
**async*** подходит, когда каждому значению нужен await: цикл по страницам API, чтение с устройства или файла по частям
При вызове ничего не выполняется: тело стартует только когда кто-то начинает обход или подписывается
Отмена: когда слушатель отписывается, тело async* завершается на следующем yield, поэтому блоки finally выполняются; именно там стоит освобождать ресурсы
Когда не нужны: если данные уже в памяти, верните List; для одного значения верните Future
Она выведет created, затем start, yield 1, 1, а потом ещё раз start, yield 1, 1. Тело sync* не выполняется при вызове функции; оно работает, только пока кто-то обходит результат, и каждый новый обход запускает его с начала.
**count() ничего не выполняет**: сразу возвращает Iterable, поэтому первым печатается created
**.first создаёт новый итератор**: тело выполняется до первого yield, отдаёт 1 и итератор на этом забрасывается
Дважды: второй .first создаёт ещё один итератор, поэтому start печатается снова; ничего не кешируется
**end не печатается никогда: второй элемент никто не запросил, поэтому цикл не доходит до конца
Исправление, если работа дорогая или с побочными эффектами**: обойдите один раз через toList() и переиспользуйте список
Uint8List, Int32List, Float64List и остальные классы dart:typed_data представляют собой списки фиксированной длины, хранящиеся одним плотным блоком «сырых» чисел. Используйте их для бинарных данных (файлы, байты изображений, сеть, буферы FFI), где List<int> тратил бы память и требовал копий.
Память: Uint8List хранит один байт на элемент; List<int> тратит на элемент слот в 4 или 8 байт, поэтому изображение на 10 МБ занимает 10 МБ, а не 40–80 МБ
Фиксированная длина и молчаливое усечение: add нет, а запись 300 в Uint8List сохраняет младшие 8 бит, 44, без всякой ошибки
Представления, а не копии: Uint8List.sublistView и buffer.asUint8List(offset, length) работают с теми же байтами; sublist копирует
**ByteData**: читает поля разной ширины с явным порядком байт, например getUint32(0, Endian.little) для бинарного заголовка
Где встречаются: response.bodyBytes, File.readAsBytes, Image.memory, rootBundle.load, TransferableTypedData между изолятами, Pointer.asTypedList в FFI
map ленив: rows не список, а рецепт, который вызывает _toRow заново при каждом проходе. rows.first создаёт Row(0), затем цикл for создаёт ещё три строки с id 1, 2 и 3, поэтому id 0 в byId нет.
Лениво значит заново: Iterable.map, where и генераторы sync* делают работу во время обхода, и каждый новый обход начинает её сначала; результат не кешируется
Побочные эффекты делают это заметным: _nextId++ создаёт побочный эффект, поэтому второй проход даёт другие объекты; то же касается дорогого парсинга, сетевых вызовов или DateTime.now() внутри map
Исправление: материализовать один раз через .toList() прямо там, где создаются строки, и дальше брать first, обходить и искать по этому единственному списку
Как поймать: держите функции для map и where чистыми; если результат используется больше одного раза или хранится в поле, сразу вызывайте toList()
Фронтенд компилятора переводит Dart в kernel (.dill); в release gen_snapshot превращает всю программу в машинный код (libapp.so на Android, App.framework на iOS) после того, как tree shaking по всей программе выбросил всё недостижимое.
От исходников до бинарника
Фронтенд: разбирает и проверяет типы вашего Dart и записывает kernel, платформонезависимое промежуточное представление
Tree shaking: анализ всей программы выбрасывает каждый класс и метод, до которого ничто не может дойти, а type-flow-анализ превращает многие виртуальные вызовы в прямые
**gen_snapshot: компилирует оставшийся kernel в код ARM64 или x64; компилятора на устройстве нет
Что попадает в сборку
Урезанный рантайм: приложение всё равно несёт рантайм Dart (GC, изоляты, event loop), только без JIT
Обфускация**: --obfuscate --split-debug-info переименовывает символы и выносит отладочную информацию из бинарника; сохраняйте файлы символов, чтобы читать стеки крашей
Веб: тот же код проходит через dart2js или dart2wasm вместо VM
В Dart поколенческий сборщик мусора. Новые объекты попадают в небольшое молодое пространство, которое scavenger чистит, копируя выживших; объекты, прожившие достаточно долго, переводятся в старое пространство, которое в основном конкурентный mark-sweep собирает и иногда уплотняет.
Молодое поколение
Выделение памяти сводится к сдвигу указателя, поэтому создать объект дёшево
Scavenge копирует только живые объекты и освобождает остальное разом, поэтому его стоимость зависит от выживших, а не от мусора; именно под тысячи короткоживущих виджетов за кадр он и сделан
Scavenge ненадолго приостанавливает Dart-код
Старое поколение
Маркировка идёт параллельно с вашим кодом, очистка выполняется во вспомогательных потоках, а уплотнение выполняется только тогда, когда куча фрагментирована
Всё долгоживущее (кеши, большие списки, хранимые декодированные данные) переводится в старое поколение и потом стоит работы там
Модель памяти вкратце
Все объекты живут в куче; на стеке лежат ссылки и небольшие целые числа, хранящиеся прямо в слоте как тегированные значения
Изоляты, запущенные через Isolate.spawn, образуют одну группу изолятов с общей кучей и общим сборщиком, но по-прежнему не видят объекты друг друга
Что это значит для вашего кода
Короткоживущие объекты считаются нормой; не делайте пулы виджетов и других мелких объектов
Настоящая проблема кроется в долгоживущем росте: неограниченные кеши и объекты, удерживаемые слушателями или статикой, и именно их стоит искать во вкладке Memory в DevTools
Нативная память (пиксели изображений, FFI) лежит вне этой кучи, поэтому небольшая куча Dart может соседствовать с большим размером процесса
Рефлексия в Dart есть (dart:mirrors), но только в standalone VM. Flutter её отключает, потому что рефлексия позволяет добраться до любого класса или члена по имени, а это ломает tree shaking: компилятору пришлось бы оставлять в каждом приложении всю программу вместе с фреймворком.
Tree shaking нужен замкнутый мир: AOT оставляет только статически достижимое; вызов reflectClass или строка с именем метода могут указывать куда угодно, и удалить нельзя было бы ничего
Размер и скорость: mirrors требуют метаданных для каждого члена во время выполнения, а это раздувает бинарник и мешает оптимизациям вроде инлайнинга
Вместо этого: кодогенерация: json_serializable, freezed, injectable и auto_route работают через build_runner и пишут тот код, который сделала бы рефлексия, поэтому он проверяется типами и проходит tree shaking, как всё остальное; эксперимент с макросами отменили в начале 2025 года
Что работает во время выполнения: runtimeType, проверки is и реифицированные дженерики; но runtimeType.toString() меняется при --obfuscate, поэтому никогда не используйте имена типов как ключи или в сохранённых данных
Обычный Map хранит сильные ссылки, поэтому всё в нём живёт столько же, сколько сам кеш. WeakReference и Expando ссылаются на объект, не удерживая его; Finalizer лишь сообщает постфактум, что объект собран, и может не сработать вообще.
**WeakReference<T>**: .target возвращает объект или null, если GC его уже забрал; тогда удалите запись, потому что Map<String, WeakReference<V>> всё равно держит ключи. Числа, строки, булевы значения, records и null не могут быть целью.
Слабая ссылка не заменяет политику кеширования: GC может очистить запись сразу после добавления, поэтому сочетайте слабую мапу с небольшим сильным LRU для горячих элементов
**Expando<T>: привязывает данные к объекту, который вам не принадлежит, не удерживая этот объект
Finalizer<T>**: attach(object, token, detach: object) может вызвать колбэк после того, как object стал недостижим. Токен не должен ссылаться на объект, а сам Finalizer должен оставаться достижимым, поэтому держите его в static final.
**NativeFinalizer**: из dart:ffi, для нативной памяти: вызывает C-функцию и гарантированно срабатывает, самое позднее при завершении группы изолятов; но не при падении процесса или exit
Правило: настоящая очистка живёт в dispose, close и cancel; Finalizer служит страховкой или детектором утечек, а не деструктором