.NET 8 Вдосконалення продуктивності .NET 8 в .NET MAUI
Основна увага в .NET MAUI у релізі .NET 8 приділяється якості. Тому ми зосередилися на виправленні помилок замість того, щоб гнатися за високими показниками продуктивності. У .NET 8 ми об'єднали 1 559 запитів, які закрили 596 проблем. Вони включають зміни від команди .NET MAUI, а також від спільноти .NET MAUI. Ми оптимістично налаштовані, що це повинно призвести до значного підвищення якості в .NET 8.
Проте! У нас є ще багато змін у продуктивності, які ми можемо продемонструвати. Спираючись на фундаментальні покращення продуктивності в .NET 8 ми постійно знаходимо легку здобич, і на GitHub були проблеми з продуктивністю, які ми намагалися вирішити. Наша мета — продовжувати робити .NET MAUI швидшим з кожним релізом, читайте далі, щоб дізнатися подробиці!
Огляд покращень продуктивності в попередніх релізах можна знайти в наших статтях для .NET 6 і 7. Це також дасть вам уявлення про покращення, які ви побачите при переході з Xamarin.Forms на .NET MAUI:
.NET 7 Покращення продуктивності в .NET MAUI
.NET 6 Покращення продуктивності в .NET MAUI
Зміст
Нові функції
- AndroidStripILAfterAOT
- AndroidEnableMarshalMethods
- NativeAOT на iOS
Побудова та продуктивність внутрішнього циклу
- Відфільтруйте вихідні дані Android ps -A за допомогою grep
- Перенесення використання vcmeta.dll з WindowsAppSDK на C#
- Покращення віддалених збірок iOS на Windows
- Покращення внутрішнього циклу Android
- Компіляція XAML більше не використовує LoadInSeparateAppDomain
Покращення продуктивності або розміру програми
- Структури та IEquatable в .NET MAUI
- Виправлено проблему продуктивності в {AppThemeBinding}
- Зверніться до CA1307 та CA1309 для отримання інформації про продуктивність
- Зверніться до CA1311 для отримання інформації
- Видалення невикористаної події ViewAttachedToWindow на Android
- Видаліть непотрібні System.Reflection для {Binding}
- Використовуйте StringComparer.Ordinal для словника та хеш-набору
- Зменшення взаємодії з Java у MauiDrawable на Android
- Покращення продуктивності верстки Label на Android
- Зменшення викликів взаємодії Java для елементів керування в .NET MAUI
- Покращення продуктивності Entry.MaxLength на Android
- Зменшення використання пам'яті CollectionView на Windows
- Використовуйте атрибут UnmanagedCallersOnlyAttribute на платформах Apple
- Швидша взаємодія з Java для рядків на Android
- Швидша взаємодія з Java для подій C# на Android
- Використання функціональних покажчиків для JNI
- Видалено Xamarin.AndroidX.Legacy.Support.V4
- Дедуплікація генериків на iOS та macOS
- Виправлено реалізацію System.Linq.Expressions на iOS-подібних платформах
- Встановіть DynamicCodeSupport=false для iOS та Catalyst
Витоки пам'яті
- Витоки пам'яті та якість
- Діагностика витоків в .NET MAUI
- Патерни, що призводять до витоків: Події C#
- Кільцеві посилання на платформах Apple
- Аналізатор Roslyn для платформ Apple
Інструменти та документація
- Спрощені dotnet-trace та dotnet-dsrouter
- Підтримка мобільних пристроїв dotnet-gcdump
Нові функції
AndroidStripILAfterAOT
Одного разу у нас виникла геніальна думка: якщо AOT попередньо компілює методи C#, то чи потрібен нам керований метод? Видалення тіла методу C# дозволило б зменшити розмір збірок. Додатки .NET iOS вже роблять це, так чому б не зробити це і для Android?
Хоча ідея проста, реалізація не була такою: iOS використовує "повний" AOT, який перетворює всі методи AOT у форму, що не вимагає використання JIT під час виконання. Це дозволило iOS запустити cil-strip видаляючи всі тіла методів з усіх керованих типів.
На той час Xamarin.Android підтримував лише "звичайний" AOT, а звичайний AOT вимагає JIT для певних конструкцій, таких як узагальнені типи та узагальнені методи. Це означало, що спроба запустити cil-strip призведе до помилок під час виконання, якщо буде видалено тіло методу, яке насправді потрібно під час виконання. Це було особливо погано, оскільки cil-strip могла видалити лише всі тіла методів!
Ми повторно впроваджуємо IL-розбірку для .NET 8. Додаємо нову властивість $(AndroidStripILAfterAOT) для MSBuild. При значенні true завдання буде відстежувати, які тіла методів були фактично AOT'ені, зберігаючи цю інформацію у %(_MonoAOTCompiledAssemblies.MethodTokenFile), а нове завдання буде оновлювати вхідні збірки, видаляючи всі тіла методів, які можна видалити.
За замовчуванням увімкнення $(AndroidStripILAfterAOT) замінить стандартне налаштування $(AndroidEnableProfiledAot), дозволяючи вилучити всі обрізані AOT'івські методи. Цей вибір було зроблено тому, що $(AndroidStripILAfterAOT) є найбільш корисним при AOT-компіляції всього вашого додатку. Профільоване вилучення AOT та IL можна використовувати разом, явно встановивши обидва параметри у файлі .csproj, але єдиною перевагою буде невелике зменшення розміру .apk:

.apk для нового додатку dotnet для Android:

Зауважте, що AndroidStripILAfterAOT=false та AndroidEnableProfiledAot=true є типовим середовищем конфігурації релізу, для 7.7MB.
Проєкт, який встановлює лише AndroidStripILAfterAOT=true, неявно встановлює AndroidEnableProfiledAot=false, призводить до отримання програми розміром 8.1 МБ.
Дивіться xamarin-android#8172 та dotnetdotnet/runtime#86722, щоб дізнатися більше про цю можливість.
AndroidEnableMarshalMethods
.NET 8 вводить нове експериментальне налаштування для конфігурацій Release:

Ми сподіваємося зробити цю функцію доступною за замовчуванням у .NET 9, але наразі ми надаємо цю можливість як експериментальну. Програми, які орієнтовані лише на одну архітектуру, наприклад RuntimeIdentifier=android-arm64, ймовірно, зможуть увімкнути цю функцію без проблем.
Загальні відомості про методи Маршала
Метод Маршала JNI - це вказівник на функцію, що викликається з JNI, який надається в JNIEnv::RegisterNatives(). Наразі маршальські методи JNI надаються через взаємодію між кодом, який ми генеруємо, та JNINativeWrapper.CreateDelegate():
- Наш генератор коду видає "справжній" JNI метод, який можна викликати.
- JNINativeWrapper.CreateDelegate() використовує System.Reflection.Emit для обгортки методу для обробки виключень.
Маршальські методи JNI потрібні для всіх переходів з Java на C#.
Розглянемо віртуальний метод Activity.OnCreate():



Activity.n_OnCreate_Landroid_os_Bundle_() - це метод маршала JNI, який відповідає за перетворення параметрів зі значень JNI у типи C#, перенаправлення виклику методу до Activity.OnCreate() та (за необхідності) перетворення значення, що повертається, назад до JNI.
Activity.GetOnCreate_Landroid_os_Bundle_Handler() є частиною інфраструктури реєстрації типів, надаючи екземпляр Delegate до RegisterNativeMembers .RegisterNativeMembers(), який згодом передається до JNIEnv::RegisterNatives().
Хоча це і працює, результат не є неймовірно продуктивним: якщо не використовувати один з оптимізованих типів делегатів, доданих в xamarin-android#6657, System.Reflection.Emit використовується для створення обгортки навколо методу marshal, чого ми намагалися уникати роками.
Отже, ідея: оскільки ми вже збираємо нативний інструментарій і використовуємо LLVM-IR для створення libxamarin-app.so, що, якби ми видали нативні імена методів Java і пропустили все, що було зроблено в рамках Runtime.register() і JNIEnv.RegisterJniNatives()?
Дано:

Під час збирання, libxamarin-app.so міститиме цю функцію:

Під час виконання програми, виклик Runtime.register(), присутній в Java викликається Java-обгортці, буде або пропущено, або не буде виконуватися, і Android/JNI натомість перетворить MyActivity.n_onCreate() у Java_crc..._MyActivity_n_1onCreate().
Ми називаємо цю роботу "LLVM Marshal Methods", яка наразі є експериментальною в .NET 8. Багато особливостей все ще досліджуються, і ця функція буде поширюватися на різні області.
Дивіться xamarin-android#7351, щоб дізнатися більше про цю експериментальну функцію.
NativeAOT на iOS
У .NET 7 ми почали експеримент, щоб побачити, що потрібно для підтримки NativeAOT на iOS. Перехід від прототипу до початкової реалізації: .NET 8 Preview 6 включав NativeAOT як експериментальну функцію для iOS.
Щоб вибрати NativeAOT у проєкті MAUI iOS, скористайтеся наступними налаштуваннями у файлі проєкту:

Потім створити додаток для iOS-пристрою:

Зауваження. Ми можемо розглянути можливість уніфікації та покращення назв властивостей MSBuild для цієї функції у майбутніх релізах .NET. Щоб виконати одноразову збірку у командному рядку, вам також може знадобитися вказати -p:PublishAotUsingRuntimePack=true на додаток до -p:PublishAot=true.
Однією з головних проблем першого релізу було те, як робоче навантаження iOS підтримує інтероперабельність Objective-C. Проблема була пов'язана в основному з системою реєстрації типів, яка є ключовим компонентом для ефективної підтримки iOS-подібних платформ (дивіться документацію для деталей). У своїй реалізації система реєстрації типів залежить від токенів метаданих типів, які недоступні у NativeAOT. Тому, щоб скористатися перевагами високоефективного середовища виконання NativeAOT, нам довелося адаптуватися. dotnetdotnet/runtime#80912 містить обговорення того, як розв'язати цю проблему, і, нарешті, в xamarin-macios#18268 ми реалізували новий керований статичний реєстратор, який працює з NativeAOT. Новий керований статичний реєстратор не тільки сумісний з NativeAOT, але і набагато швидший за стандартний, і доступний для всіх підтримуваних середовищ виконання (див. у документації for докладніше).
На цьому шляху нам дуже допомогла наша GH-спільнота, і їхній внесок (огляди коду, PR) був дуже важливим, щоб допомогти нам швидко рухатися вперед і випустити цю функцію вчасно. Ось деякі з багатьох PR, які допомогли нам і розблокували наш шлях:
- dotnet/runtime#77956
- dotnet/runtime#78280
- dotnet/runtime#82317
- dotnet/runtime#85996
і цей список можна продовжувати...
З появою .NET 8 Preview 6 ми нарешті змогли випустити нашу першу версію NativeAOT на iOS, яка також підтримує MAUI. Дивіться в пост в блозі про .NET 8 Preview 6, щоб дізнатися більше про те, чого нам вдалося досягти в початковій версії.
У наступних випусках .NET 8 результати значно покращилися, оскільки ми виявляли та вирішували проблеми на цьому шляху. На графіку нижче показано порівняння розміру шаблонних додатків .NET MAUI для iOS у попередніх версіях:

Ми досягли стабільного прогресу і повідомили про очікувану економію розміру, завдяки вирішенню наступних проблем:
- dotnet/runtime#87924 - виправлено серйозну проблему з розміром NativeAOT з AOT-несумісними шляхами коду у System.Linq.Expressions, а також зроблено повністю сумісними з NativeAOT при націлюванні на iOS
- xamarin-macios#18332 - зменшено розмір розділу __LINKEDIT Export Info у вилучених двійкових файлах
Крім того, в останньому випуску RC 1 розмір додатків ще більше зменшився, досягнувши на 50% меншого розміру для шаблонних додатків .NET MAUI для iOS порівняно з Mono. Найвпливовіші проблеми/ПР, які сприяли цьому:
- xamarin-macios#18734 - Зробити Full режимом з'єднання за замовчуванням для NativeAOT
- xamarin-macios#18584 - Зробити обрізку кодової бази сумісним за допомогою серії PR.
Попри те, що розмір програми був основною метрикою, на якій ми зосередилися, для випуску RC 1 ми також виміряли час запуску для шаблонного додатка .NET MAUI для iOS, порівнюючи NativeAOT і Mono, де NativeAOT показав майже вдвічі швидший час запуску.

Основні висновки
Для сценаріїв NativeAOT на iOS зміна режиму зв'язування за замовчуванням на Full (xamarin-macios#18734), ймовірно, є найбільшим покращенням для розміру програми. Але водночас, ця зміна може призвести до поломки додатків, які не є повністю сумісними з AOT та обрізкою. У режимі Full, тример може обрізати несумісні з AOT шляхи коду (згадайте про використання рефлексії), доступ до яких здійснюється динамічно під час виконання. Режим зв'язування Full не є типовою конфігурацією у разі використання середовища виконання Mono, тому деякі програми можуть бути не повністю сумісними з AOT.
Підтримка NativeAOT на iOS є експериментальною функцією і все ще перебуває в процесі розробки, і ми плануємо вирішувати потенційні проблеми з режимом повного посилання поступово:
- Як перший крок, ми увімкнули попередження про обрізання, AOT та однофайлові попередження за замовчуванням у xamarin-macios#18571. Увімкнені попередження мають попередити наших клієнтів про те, що використання певного фреймворку, бібліотеки або деяких конструкцій C# у їхньому коді несумісне з NativeAOT - і може призвести до аварійного завершення роботи під час виконання. Ця інформація має допомогти нашим клієнтам писати код сумісний з AOT, а також допомогти нам покращити наші фреймворки та бібліотеки з тією ж метою — повністю використати переваги компіляції AOT.
- Другим кроком було усунення всіх попереджень від збірок Microsoft.iOS та System.Private.CoreLib, про які повідомлялося для шаблонного додатку iOS з: xamarin-macios#18629 та dotnetdotnet/runtime#91520.
- У наступних випусках ми плануємо вирішити проблеми, пов'язані з фреймворком MAUI, та покращити загальний користувацький досвід. Наша мета — створити повністю сумісний з AOT та обрзкою фреймворк.
NET 8 буде підтримувати таргетинг платформ iOS з NativeAOT як опціональну функцію і демонструє великий потенціал, генеруючи на 50% менший і на 50% швидший запуск у порівнянні з Mono. Враховуючи високу продуктивність, яку обіцяє NativeAOT, будь ласка, допоможіть нам на цьому шляху і випробуйте свої додатки з NativeAOT та повідомляйте про будь-які потенційні проблеми. Водночас давайте нам знати, коли NativeAOT працює без проблем одразу.
Щоб стежити за подальшим прогресом, див. dotnetdotnet/runtime#80905. І останнє, але не менш важливе, ми хотіли б подякувати нашим учасникам GH, які допомагають нам зробити NativeAOT на iOS можливим.
Побудова та продуктивність внутрішнього циклу
Відфільтруйте вихідні дані Android ps -A за допомогою grep
При перегляді внутрішнього циклу Android для проєкту .NET MAUI з допомогою PerfView ми виявили, що близько 1,2% процесорного часу витрачається лише на отримання ідентифікатора процесу запущеного Android-додатку.
Змінивши багатослівність виводу на Diagnostics - Tools > Options > Xamarin > Xamarin Diagnostics output , ви зможете побачити:

Розширення Xamarin/.NET MAUI у Visual Studio щосекунди опитує, чи не завершено роботу програми. Це корисно для зміни стану кнопки відтворення/зупинки, якщо ви примусово закриваєте програму тощо.
Тестуючи на Pixel 5, ми побачили, що команда насправді виводить 762 рядки!

Натомість ми могли б зробити щось на кшталт:

Де ми передаємо вивід ps -A до команди grep на пристрої Android. Так, Android має підмножину unix-команд! Ми фільтруємо або за рядком, що містить PID, або за назвою пакунка вашої програми.
В результаті тепер IDE розбирає лише 4 рядки:

Це не тільки оптимізує пам'ять, яка використовується для розділення та аналізу цієї інформації в C#, але adb також передає набагато менше байт через ваш USB-кабель або віртуально з емулятора.
Ця можливість з'явилася в останніх версіях Visual Studio 2022, що покращує цей сценарій для всіх користувачів Xamarin і .NET MAUI.
Перенесення використання vcmeta.dll з WindowsAppSDK на C#
Ми виявили, що кожна інкрементна збірка проєкту .NET MAUI, що працює під Windows, витрачає час:

Це компілятор XAML для WindowsAppSDK, який компілює XAML для WinUI3 (не .NET MAUI XAML). У проєктах .NET MAUI дуже мало XAML цього типу, фактично, єдиним файлом є файл Platforms/Windows/App.xaml у шаблоні проєкту.
Цікаво, що якщо в інсталяторі Visual Studio встановити Desktop development with C++ workload, то цей час просто зникне!

Компілятор XAML WindowsAppSDK звертається до власної бібліотеки з робочого навантаження C++, vcmeta.dll, щоб обчислити хеш для файлів збірки .NET. Це використовується для прискорення інкрементальних збірок — якщо хеш змінюється, компілюйте XAML заново. Якщо vcmeta.dll не було знайдено на диску, компілятор XAML фактично "перекомпілював все" при кожній інкрементальній збірці.
Для початкового виправлення ми просто включили невелику частину навантаження C++ як залежність .NET MAUI у Visual Studio. Дещо більший розмір інсталяції був гарним компромісом для економії до 4 секунд часу інкрементальної збірки.
Далі ми реалізували функціонал хешування vcmeta.dll звичайною мовою C# за допомогою System.Reflection.Metadata, щоб обчислювати ідентичні хеш-значення, як і раніше. Ця реалізація була не тільки кращою, оскільки ми могли позбутися залежності від робочого навантаження C++, але й швидшою! Час обчислення одного хешу:

Деякі з причин, чому це було швидше:
- Не залучені p/invoke або COM-інтерфейси.
- System.Reflection.Metadata має швидкий API на основі структур, що ідеально підходить для ітерацій над типами в .NET збірці та обчислення хеш-значення.
В результаті CompileXaml може бути навіть швидшим за 9 мс в інкрементних збірках.
Цю можливість було включено до WindowsAppSDK 1.3, який тепер використовується .NET MAUI у .NET 8. Докладні відомості про це вдосконалення див. у WindowsAppSDK#3128.
Покращення віддалених збірок iOS на Windows
Порівнюючи продуктивність внутрішнього циклу для iOS, було виявлено значний розрив між "віддаленою iOS" розробкою на Windows і локальною розробкою на macOS. Було зроблено багато невеликих удосконалень, заснованих на порівнянні файлів .binlog внутрішнього циклу, записаних на macOS, з файлами, записаними всередині Visual Studio на Windows.
Деякі приклади включають:
- maui#12747: не копіювати явно файли на сервер збірки
- xamarin-macios#16752: не копіювати файли на сервер збірки для операції видалення
- xamarin-macios#16929: пакетне видалення файлів через DeleteFilesAsync
- xamarin-macios#17033: кешувати шлях до компілятора AOT
- Розширення Xamarin/MAUI Visual Studio: під час запуску dotnet-install.sh на віддалених хостах збірки встановіть явний прапорець процесора для комп'ютерів M1 Mac.
Ми також зробили деякі покращення для всіх проектів iOS та MacCatalyst, такі як:
- xamarin-macios#16416: не обробляйте збірки знову і знову
Покращення внутрішнього циклу Android
Ми також зробили багато невеликих поліпшень у "внутрішній цикл" на Android — більшість з них були зосереджені на конкретній області.
Раніше проєкти Xamarin.Forms мали розкіш бути організованими в декілька проєктів, таких як:
- YourApp.Android.csproj: Проєкт програми Xamarin.Android
- YourApp.iOS.csproj: Проєкт додатку Xamarin.iOS
- YourApp.csproj: бібліотека класів netstandard2.0
Де майже вся логіка програми Xamarin.Forms містилася в проекті netstandard2.0. Майже всі інкрементні збірки були б змінами до XAML або C# у бібліотеці класів. Така структура дозволила Xamarin.Android MSBuild-цілям повністю пропустити багато специфічних для Android кроків MSBuild. У .NET MAUI функція "єдиного проекту" означає, що кожна інкрементальна збірка повинна виконувати ці специфічні для Android кроки збірки.
Зосередившись саме на покращенні цієї сфери, ми внесли багато невеликих змін, таких як:
- java-interop#1061: уникнення string.Format()
- java-interop#1064: покращено ToJniNameFromAttributesForAndroid
- java-interop#1065: уникнення перевірки File.Exists()
- java-interop#1069: виправлено більше місць використання TypeDefinitionCache
- java-interop#1072: використання меншої кількості System.Linq для користувацьких атрибутів
- java-interop#1103: використання MemoryMappedFile при використанні Mono.Cecil
- xamarin-android#7621: уникнення перевірки File.Exists()
- xamarin-android#7626: покращено роботу LlvmIrGenerator.
- xamarin-android#7652: швидкий шлях для
- xamarin-android#7653: затримка ToJniName під час генерації AndroidManifest.xml
- xamarin-android#7686: ліниве заповнення пошуку ресурсів
Ці зміни мають покращити інкрементні збірки у всіх типах проектів .NET 8 для Android.
Компіляція XAML більше не використовує LoadInSeparateAppDomain
Переглядаючи звіт JITStats в PerfView (для MSBuild.exe):

Схоже, що Microsoft.Maui.Controls.Build.Tasks.dll проводив багато часу в JIT. Що було незрозуміло, так це те, що це була інкрементна збірка, де все вже повинно бути завантажено. Робота JIT вже повинна бути виконана?
Причиною є використання атрибута [LoadInSeparateAppDomain], визначеного за допомогою у .NET MAUI. Це функція MSBuild, яка дозволяє завданням MSBuild запускатися в ізольованому AppDomain - з очевидним недоліком продуктивності. Однак, ми не могли просто видалити його, оскільки це призвело б до ускладнень...
[LoadInSeparateAppDomain] також зручно скидає весь статичний стан при повторному запуску >. Це означає, що майбутні інкрементні збірки потенційно використовуватимуть старі (сміттєві) значення. Існує декілька місць, які кешують об'єкти Mono.Cecil з міркувань продуктивності. Якщо цього не зробити, можуть виникнути дуже дивні помилки.
Для того, щоб зробити цю зміну, ми переробили весь статичний стан у компіляторі XAML, щоб він зберігався у полях та властивостях екземпляра. Це загальне покращення дизайну програмного забезпечення, на додаток до можливості безпечно видалити [LoadInSeparateAppDomain].
Результати цієї зміни для інкрементальної збірки на ПК з Windows:

Це дозволило заощадити близько 587 мс на інкрементних збірках на всіх платформах, що становить покращення на 82%. Це ще більше допоможе у великих рішеннях з декількома проєктами .NET MAUI, де виконується декілька разів.
Дивіться maui#11982 для більш детальної інформації про це покращення.
Покращення продуктивності або розміру програми
Структури та IEquatable в .NET MAUI
За допомогою профілю Visual Studio .NET Object Allocation Tracking на прикладі клієнтського додатку .NET MAUI ми побачили:


Це здається непомірним обсягом пам'яті для запуску прикладу програми!
Заглибимось, щоб побачити, де ці структури були створені:

Основна проблема полягала в тому, що ця структура не реалізовувала IEquatable і використовувалась як ключ для словника. Правило аналізу коду CA1815 було розроблено для виявлення цієї проблеми. Це правило не вмикається за замовчуванням, тому проєкти повинні вибрати його.
Щоб вирішити це:
- Підписка є внутрішньою для .NET MAUI, і її використання дозволило зробити структуру доступною лише для читання. Це стало просто додатковим покращенням.
· Ми зробили CA1815 помилкою збірки у всьому репозиторії dotnet/maui.
· Ми реалізували IEquatable для всіх типів struct.
Після цих змін ми більше не змогли знайти Microsoft.Maui.WeakEventManager+Subscription у знімках пам'яті взагалі. Що дозволило заощадити ~21 МБ розподіленої пам'яті у цьому прикладі програми. Якщо у ваших проєктах використовується struct, то варто зробити CA1815 помилкою збірки.
Меншу цільову версію цієї зміни було перенесено до MAUI у .NET 7. Докладні відомості про це покращення наведено у maui#13232.
Виправлено проблему продуктивності в {AppThemeBinding}
Аналізуючи зразок додатка .NET MAUI від клієнта, ми помітили багато часу, який витрачається на {AppThemeBinding} та WeakEventManager під час скролінгу:

У цьому додатку відбувалося наступне:
- Стандартний шаблон проєкту .NET MAUI має багато {AppThemeBinding} у файлі Styles.xaml за замовчуванням. Це підтримка світлих та темних тем.
- {AppThemeBinding} підписується на Application.RequestedThemeChanged
- Отже, кожен перегляд MAUI підписується на цю подію - можливо, кілька разів.
- Передплатники — це Dictionary>>, де відбувається пошук за словником, за яким слідує O(N) пошук операцій відписки.
Тут є потенційна можливість створити узагальнений шаблон "слабкої події" для .NET. Реалізація в .NET MAUI прийшла з Xamarin.Forms, але узагальнений шаблон може бути корисним для .NET розробників, які використовують інші фреймворки інтерфейсу користувача.
Щоб зробити цей сценарій швидким, поки що в .NET 8:
Раніше:
- Для будь-якого {AppThemeBinding} він викликає обидва:
- RequestedThemeChanged -= OnRequestedThemeChanged O(N) times
- RequestedThemeChanged += OnRequestedThemeChanged постійний час
- Де -= помітно повільніше, через, можливо, сотні підписників.
Після:
- Створіть булеву функцію _attached, щоб ми знали "стан", чи вона приєднана, чи ні.
- Нові прив'язки викликають лише +=, тоді як -= тепер викликатиметься {AppThemeBinding} лише у рідкісних випадках.
- Більшість програм .NET MAUI не "скасовують" прив'язки, але -= буде використано лише у цьому випадку.
Повну інформацію про це виправлення див. в maui#14625. Див. dotnetdotnet/runtime#61517 про те, як ми можемо реалізувати "слабкі події" у .NET у майбутньому.
Зверніться до CA1307 та CA1309 для отримання інформації про продуктивність
Переглядаючи зразок додатка .NET MAUI від клієнта, ми помітили час, що витрачається на "культурно-орієнтовані" операції з рядками:

Цю ситуацію можна покращити, просто викликавши натомість ToLowerInvariant(). У деяких випадках ви можете навіть розглянути можливість використання string.Equals() з StringComparer.Ordinal. У цьому випадку наш код було додатково переглянуто та оптимізовано у статтях Зменшення Java взаємодії з у MauiDrawable на Android.
У .NET 7 ми додали правила аналізу коду CA1307 і CA1309 для виявлення подібних випадків, але, схоже, ми пропустили деякі з них у Microsoft.Maui.Graphics.dll. Ці правила можуть бути корисними для використання у ваших власних додатках .NET MAUI, оскільки уникнення всіх культурно-залежних операцій з рядками може мати значний вплив на мобільні пристрої.
Дивіться maui#14627, щоб дізнатися більше про це покращення.
Зверніться до CA1311 для отримання інформації
Після розгляду правил аналізу коду CA1307 і CA1309 ми пішли далі і розглянули CA1311.
Як згадувалося в турецькому прикладі, подібні дії:

Можуть спричинити неочікувану поведінку в турецьких регістрах, оскільки в турецькій мові символ I (Юнікод 0049) вважається верхнім регістром іншого символу ý (Юнікод 0131), а i (Юнікод 0069) вважається нижнім регістром ще одного символу Ý (Юнікод 0130).
ToLowerInvariant() і ToUpperInvariant() також краще для продуктивності, оскільки інваріантна операція ToLower / ToUpper працює трохи швидше. Це також дозволяє уникнути завантаження поточної культури, що покращує продуктивність запуску.
Існують випадки, коли вам потрібно використовувати поточну культуру, наприклад, у випадку типу CaseConverter у .NET MAUI. Для цього вам просто потрібно чітко вказати, яку культуру ви хочете використовувати:

Мета цього CaseConverter - показати користувачеві текст у верхньому або нижньому регістрі. Тому має сенс використовувати для цього CurrentCulture.
Дивіться maui#14773, щоб дізнатися більше про це покращення.
Видалення невикористаної події ViewAttachedToWindow на Android
Кожен Label в .NET MAUI був підписаний:

Це залишилося після рефакторингу, але з'явилося у виводі дотнет-траси як:

Де перше — це підписка, а друге — подія, що викликається з Java на C# - тільки для запуску порожнього керованого методу.
Просто видаливши підписку на цю подію та порожній метод, ми залишили лише кілька елементів управління для підписки на цю подію за потреби:

Дивіться maui#14833, щоб дізнатися більше про це покращення.
Видаліть непотрібні System.Reflection для {Binding}
Всі прив'язки в .NET MAUI зазвичай потрапляють до шляху коду:

Де ~53% часу, витраченого на застосування прив'язки, з'явилося в dotnet-трасі в методі MethodInfo.GetParameters():

Вищенаведений код C# просто знаходить тип властивості. Він використовує обхідний спосіб використання першого параметра задавача властивості, який можна спростити:

Ми могли побачити результати цієї зміни в бенчмарку BenchmarkDotNet:

Де ++ позначає нові зміни.
Дивіться maui#14830 для більш детальної інформації про це покращення.
Використання StringComparer.Ordinal для словника та хеш-набору
Проаналізувавши зразок додатку .NET MAUI від клієнта, ми помітили, що 4% часу під час прокрутки витрачається на пошук у словнику:

Спостерігаючи за стеком викликів, деякі з них надходили з культурно-орієнтованого пошуку рядків у .NET MAUI:
- microsoft.maui!Microsoft.Maui.PropertyMapper.GetProperty(string)
-microsoft.maui!Microsoft.Maui.WeakEventManager.AddEventHandler(System.EventHandler,string)
- microsoft.maui!Microsoft.Maui.CommandMapper.GetCommand(string)
Які відображаються в dotnet-trace у вигляді суміші рядкових порівнянь:

У випадку Dictionary або HashSet ми можемо використовувати StringComparer.Ordinal у багатьох випадках, щоб пришвидшити пошук у словнику. Це має дещо покращити продуктивність обробників та всіх елементів управління .NET MAUI на всіх платформах.
Дивіться maui#14900, щоб дізнатися більше про це покращення.
Зменшення взаємодії з Java у MauiDrawable на Android
Профілюючи зразок .NET MAUI клієнта під час скролінгу на Pixel 5, ми побачили, як цікаво він проводив час:

У цьому прикладі знаходиться всередині >, тому ви можете бачити, як це відбувається під час прокрутки.
Зокрема, ми розглянули код в .NET MAUI, такий як:

Це п'ять викликів з C# на Java. Створення нового методу в PlatformInterop.java дозволило нам скоротити його до одного разу.
Ми також покращили наступний метод, який виконує багато викликів з C# на Java:


Щоб бути більш лаконічно реалізованим на Java як:



Що зводить нашу нову реалізацію на стороні C# до одного виклику Java та створення структури Android.Graphics.Color:

Після цих змін ми побачили такий вивід dotnet-trace:

Це збільшує продуктивність будь-якого (та інших фігур) на Android і зменшує використання процесора на ~1% під час прокрутки у цьому прикладі.
Дивіться maui#14933 для більш детальної інформації про це покращення.
Покращення продуктивності верстки Label на Android
Тестуючи різні зразки додатків .NET MAUI на Android, ми помітили, що близько 5,1% часу витрачається на PrepareForTextViewArrange():

Більшість часу витрачається на виклик Android.Views.View.Context, щоб потім мати змогу викликати метод розширення:

Виклик властивості Context може бути дорогим через взаємодію між C# та Java. Java повертає хендл на екземпляр, після чого нам доводиться шукати всі наявні керовані об'єкти C# для Context. Якщо всієї цієї роботи можна просто уникнути, це може значно підвищити продуктивність.
У .NET 7 ми зробили перевантаження для ToPixels(), що дозволяє отримати те саме значення за допомогою Android.Views.View
Тож замість цього ми можемо зробити так:

Ця зміна не тільки покращила результати dotnet-trace, але й помітно вплинула на наші LOLs в секунду у нашому тестовому додатку порівняно з минулим роком:

Дивіться maui#14980, щоб дізнатися більше про це покращення.
Зменшення викликів взаємодії Java для елементів керування в .NET MAUI
Огляд чудового прикладу .NET MAUI "Surfing App" від @jsuarezruiz:

Ми помітили, що багато часу витрачається на взаємодію з Java під час прокрутки:

Ці методи були глибоко вкладені, виконуючи взаємодію з Java -> C# -> Java на багатьох рівнях. У цьому випадку, переміщення деякого коду з C# на Java може призвести до того, що він буде виконувати менше операцій, а в деяких випадках взагалі не буде виконувати жодної операції!
Так, наприклад, раніше DispatchDraw() перевизначався у C#, щоб реалізувати поведінку відсікання:

Створивши PlatformContentViewGroup.java, ми можемо зробити щось на зразок


setHasClip() викликається, коли відсікання увімкнено/вимкнено у будь-якому елементі управління .NET MAUI. Це дозволило загальному шляху взагалі не взаємодіяти з C#, а лише з тими елементами управління, які ввімкнули відсікання. Це дуже добре, тому що dispatchDraw() викликається досить часто під час верстки Android, скролінгу тощо.
Таку саму обробку було також зроблено з кількома іншими внутрішніми типами .NET MAUI, такими як WrapperView: покращено загальну ситуацію, щоб взаємодія відбувалася лише тоді, коли перегляди вибрали відсікання або відкидання тіней.
Для тестування впливу цих змін ми використовували Google's FrameMetricsAggregator від Google який можна налаштувати в Platforms/Android/MainActivity.cs будь-якого додатка .NET MAUI:




API FrameMetricsAggregator є дещо дивним, але дані, які ми отримуємо, є досить корисними. Результатом є таблиця пошуку, де ключ — це тривалість в мілісекундах, а значення — кількість "кадрів", які зайняли цю тривалість. Ідея полягає в тому, що будь-який кадр, який триває довше 16 мс, вважається "повільним" або "смиканим", як іноді кажуть в документації до Android.
Приклад .NET MAUI "Surfing App", що працює на Pixel 5:




Після внесення змін до ContentViewGroup та WrapperView ми отримали дуже гарне покращення! Навіть у додатку, що інтенсивно використовує обрізки та тіні:



Перегляньте maui#14275 для більш детальної інформації про ці зміни.
Збільшення продуктивності Entry.MaxLength на Android
Досліджуємо зразок клієнта .NET MAUI:
- Навігація з випадаючого вікна Shell.
- На нову сторінку з кількома елементами керування введенням.
- Була помітна затримка у виконанні.
Під час профілювання на Pixel 5 одним з “гарячих шляхів” був Entry.MaxLength:

- Виклики EditTextExtensions.UpdateMaxLength()
- Getter та setter EditText.Text
- Виклики EditTextExtensions.SetLengthFilter()
- EditText.Get/SetFilters()
В результаті ми пересилаємо рядки та IInputFilter[] між C# та Java для кожного елемента управління Entry. Всі елементи управління Entry проходять через цей шлях коду (навіть ті, що мають значення MaxLength за замовчуванням), тому є сенс перенести частину цього коду з C# на Java.
Наш код на C# раніше:




Натомість перейшли на Java (з ідентичною поведінкою):





Це дозволяє уникнути маршування (копіювання!) значень рядків і масивів з C# на Java і навпаки. Завдяки цим змінам виклики EditTextExtensions.UpdateMaxLength() стали настільки швидкими, що вони повністю відсутні у виводі dotnet-trace, заощаджуючи ~19 мс при переході на сторінку у клієнтському прикладі.
Дивіться maui#15614, щоб дізнатися більше про це покращення.
Зменшення використання пам'яті CollectionView на Windows
Ми розглянули зразок клієнта .NET MAUI з CollectionView на 150 000 прив'язаних до даних рядків. Налагоджуючи те, що відбувається під час виконання, .NET MAUI ефективно виконував:

А потім кожен елемент створюється в міру прокручування:

Це був не найкращий підхід, але для покращення ситуації:
- використовуйте натомість Dictionary>, просто дозвольте йому динамічно змінювати розмір.
- використовуйте TryGetValue(..., out var context), щоб кожен виклик звертався до індексатора на один раз менше, ніж раніше.
- використовуйте або розмір пов'язаної колекції, або 64 (залежно від того, що менше) як приблизну оцінку того, скільки зображень може поміститися на екрані за один раз
Наш код змінюється на:

Після внесення цих змін, знімок пам'яті програми після запуску наступний:

Що економить близько 1 МБ пам'яті при запуску. У цьому випадку краще просто дозволити Словнику самому визначати свій розмір з оцінкою того, якою буде його ємність.
Дивіться maui#16838, щоб дізнатися більше про це покращення.
Використовуйте UnmanagedCallersOnlyAttribute на платформах Apple
Коли некерований код викликає керований код, наприклад, виклик зворотного виклику з Objective-C, раніше в Xamarin.iOS, Xamarin.Mac і .NET 6+ для цієї мети використовувався атрибут [MonoPInvokeCallbackAttribute]. Атрибут [UnmanagedCallersOnlyAttribute] з'явився як сучасна заміна цієї функції Mono, яка реалізована з урахуванням продуктивності.
На жаль, є кілька обмежень при використанні цього нового атрибуту:
· Метод повинен бути позначений як статичний.
· Не можна викликати з керованого коду.
· Повинні бути тільки аргументи, що розбиваються на частини.
· Не повинен мати параметрів узагальненого типу або міститися в узагальненому класі.
Нам довелося не тільки рефакторити "генератор коду", який створює багато прив'язок для API Apple для AppKit, UIKit і т.д., але й багато ручних прив'язок, які потребували б такої ж обробки.
В результаті більшість функцій зворотного виклику з Objective-C на C# повинні працювати швидше в .NET 8, ніж раніше. Дивіться xamarin-macios#10470 та xamarin-macios#15783 для отримання детальної інформації про ці покращення.
Швидша взаємодія з Java для рядків на Android
При зв'язуванні членів, що мають типи параметрів або типів повернення java.lang.CharSequence, член "перевантажується" для заміни CharSequence на System.String, а "оригінальний" член має суфікс Formatted.
Наприклад, розглянемо android.widget.TextView який має функцію getText() та setText() методи, які мають типи параметрів та повертають типи java.lang.CharSequence:

У зв'язаному вигляді це призводить до двох властивостей:

"Неформатоване перевантаження" працює шляхом створення тимчасового об'єкта String для виклику форматованого перевантаження, ось так виглядає фактична реалізація:

TextView.Text набагато легше зрозуміти і простіше використовувати для .NET розробників, ніж TextView.TextFormatted.
Проблемою такого підходу є продуктивність: створення нового екземпляру Java.Lang.String вимагає:
1. Створення керованого однорангового об'єкту (екземпляру Java.Lang.String),
2. Створення нативного однорангового екземпляру (екземпляр java.lang.String),
3. І реєстрація співвідношення між (1) і (2)
А потім одразу цим і користуйтесь ..
Це особливо помітно у додатках .NET MAUI. Розглянемо приклад клієнта, який використовує XAML для встановлення прив'язаних до даних значень Text у CollectionView, які зрештою потрапляють до TextView.Text. Профілювання показує:

6.3% часу скролінгу витрачається на встановлення властивості TextView.Text!
Цей випадок можна частково оптимізувати: якщо член *Formatted є (1) властивістю і (2) не є віртуальним, то ми можемо безпосередньо викликати метод встановлювача Java.Це дозволяє уникнути необхідності створення керованого однорангового пристрою та реєстрації зв'язку між одноранговими пристроями:


В результаті чого:

Час виклику встановлювача властивості TextView.Text зменшено до 20% від попереднього середнього часу виклику.
Зауважте, що віртуальний випадок є проблематичним з інших причин, але, на щастя, TextView.setText() не є віртуальним і, ймовірно, є одним з найбільш поширених API Android.
Дивіться java-interop#1101 для більш детальної інформації про це покращення.
Швидша взаємодія з Java для подій C# на Android
Проаналізувавши вибірку клієнтів .NET MAUI під час прокрутки на Pixel 5, ми побачили ~2,2% часу, витраченого на конструктор IOnFocusChangeListenerImplementor, через підписку на подію View.FocusChange:

MAUI підписується на Android.Views.View.FocusChange для кожного подання, розміщеного на екрані, що відбувається під час прокрутки у цьому прикладі.
Переглядаючи згенерований код конструктора IOnFocusChangeListenerImplementor, ми бачимо, що він все ще використовує застарілі API JNIEnv:

Які ми можемо змінити, щоб використовувати новіші/швидші API Java.Interop:

Вони кращі, оскільки еквівалентний виклик JNIEnv.FindClass() кешується, серед іншого. Це був лише один з випадків, який ми випадково пропустили, коли впроваджували нові API Java.Interop в Xamarin. Нам просто потрібно було оновити наш генератор коду, щоб він видавав кращу прив'язку C# для цього випадку.
Після цих змін ми побачили натомість результати в dotnet-trace:

Це повинно покращити продуктивність усіх подій C#, які обгортають слухачів Java, шаблон дизайну, який зазвичай використовується у програмах на Java та Android. Сюди входить подія FocusedChanged, яка використовується всіма представленнями .NET MAUI на Android.
Дивіться java-interop#1105 для більш детальної інформації про це покращення.
Використання функціональних покажчиків для JNI
Існують різні механізми та згенерований код, які роблять можливим взаємодію Java з C#. Візьмемо, наприклад, наступний метод екземпляру foo() в Java:

Метод C# з назвою CallObjectMethod відповідає за виклик власного інтерфейсу Java (JNI), який викликає JVM для виклику методу Java:


У Xamarin.Android, .NET 6 та .NET 7 всі виклики на Java проходять через java_interop_jnienv_call_object_method_a p/invoke, сигнатура якого має такий вигляд:

Що реалізовано на мові C наступним чином:

C# 9 вводить функції вказівники що дозволило нам дещо спростити роботу — і, як наслідок, пришвидшити її.
Отже, замість того, щоб використовувати p/invoke у .NET 8, ми могли б викликати новий небезпечний метод з назвою CallObjectMethodA:

Який викликає покажчик на функцію C# напряму:

Цей покажчик на функцію оголошено з використанням нового синтаксису, введеного в C# 9:

Порівняння двох реалізацій з ручним бенчмарком:

У Release-збірці середній час виклику JIFunctionPointersTiming займає 97% від часу JIPinvokeTiming, тобто виходить на 3% швидше. Крім того, використання вказівників на функції C# 9 означає, що ми можемо позбутися всіх функцій C java_interop_jnienv_*(), що зменшує libmonodroid.so на ~55 КБ для кожної архітектури.
Дивіться xamarin-android#8234 та java-interop#938 для більш детальної інформації про це покращення.
Видалено Xamarin.AndroidX.Legacy.Support.V4
Переглядаючи залежності .NET MAUI для Android, ми помітили підозрілий пакет:

Якщо ви знайомі з Бібліотеками Підтримки Android, це набір пакунків, які Google надає для "переливання" API у попередні версії Android. Це дає їм можливість додавати нові API до старих версій ОС, оскільки екосистема Android (OEM-виробники тощо) оновлюється набагато повільніше, ніж, наприклад, iOS. Цей конкретний пакет, Legacy.Support.V4, насправді підтримує Android аж до Android API 4! Мінімальна підтримувана версія Android в .NET - це Android API 21, яка була випущена в 2017 році.
Виявляється, ця залежність була перенесена з Xamarin.Forms і насправді не була потрібна. Як і очікувалося, ця зміна призвела до того, що з додатків .NET MAUI було видалено багато Java-коду. Настільки багато, що додатки .NET 8 MAUI тепер підпадають під ліміт мультидексів - весь байт-код Dalvik може зафіксувати в одному файлі classes.dex.
Детальна розбивка змін розміру за допомогою apkdiff:



Дивіться dotnetdotnet/maui#12232, щоб дізнатися більше про це покращення.
Дедуплікація генериків на iOS та macOS
У .NET 7 додатки для iOS збільшилися у розмірі через використання узагальнень C# у декількох збірках .NET. Коли моно AOT-компілятор .NET 7 зустрічає узагальнений екземпляр, який не обробляється спільним використанням, він видає код цього екземпляра. Якщо той самий екземпляр зустрічається під час AOT-компіляції у декількох збірках, код буде видано кілька разів, що збільшить розмір коду.
У .NET 8 нові опції командного рядка dedup-skip і dedup-include передаються компілятору Mono AOT. Створено нову збірку aot-instances.dll для спільного використання цієї інформації в одному місці у всій програмі.
Зміна була протестована в додатку MySingleView і тестах Monotouch в кодовій базі xamarin/xamarin-macios:

Дивіться xamarin-macios#17766 для більш детальної інформації про це покращення.
Виправлено реалізацію System.Linq.Expressions на iOS-подібних платформах
У .NET 7 кодові шляхи в System.Linq.Expressions контролювалися різними прапорцями, такими як:
- CanCompileToil
- CanEmitObjectArrayDelegate
- CanCreateArbitraryDelegates
Ці прапорці керують кодовими шляхами, які є "дружніми до AOT", а які ні. Для десктопних платформ NativeAOT визначає наступну конфігурацію для AOT-сумісного коду:

Що стосується iOS-подібних платформ, то бібліотека System.Linq.Expressions була зібрана з увімкненим константним розмноженням та вилученими керуючими змінними. Це призвело до того, що перелічені вище перемикачі функцій NativeAOT не мали жодного ефекту (не обрізалися під час збирання програми), що потенційно могло призвести до того, що компіляція AOT йшла непідтримуваними шляхами коду на цих платформах.
У .NET 8 ми уніфікували збірку System.Linq.Expressions.dll, щоб забезпечити однакову збірку для всіх підтримуваних платформ і середовищ виконання, а також спростили ці перемикачі для врахування IsDynamicCodeSupported, щоб тример .NET міг видаляти відповідні IL в System.Linq.Expressions.dll під час збірки додатку.
Дивіться dotnetdotnet/runtime#87924 та dotnetdotnet/runtime#89308 для отримання детальної інформації про це покращення.
Встановіть DynamicCodeSupport=false для iOS та Catalyst
У .NET 8 перемикач функцій $(DynamicCodeSupport) встановлено у значення false для платформ:
- Де неможливо публікувати без компілятора AOT.
- Коли перекладач не ввімкнений.
Це стосується додатків, що працюють на iOS, tvOS, MacCatalyst тощо.
DynamicCodeSupport=false дозволяє тримеру .NET видаляти шляхи коду в залежності від RuntimeFeature.IsDynamicCodeSupported, наприклад цей приклад у System.Linq.Expressions.
Орієнтовна економія становить:

У поєднанні з System.Linq.Expressions на iOS-подібних платформах це дало гарне загальне покращення розміру програми:

Дивіться xamarin-macios#18555 для більш детальної інформації про це покращення.
Витоки пам'яті
Витоки пам'яті та якість
Враховуючи, що основною темою .NET MAUI в .NET 8 є якість, проблеми, пов'язані з пам'яттю, стали основною темою цього випуску. Деякі з виявлених проблем існували навіть у кодовій базі Xamarin.Forms, тому ми раді працювати над створенням фреймворку, на який розробники можуть покластися при створенні крос-платформних .NET додатків.
Більш детальну інформацію про роботу, виконану в .NET 8, можна знайти в різних прес-релізах та випусках, пов'язаних з проблемами пам'яті, за посиланням:
- Запити
- Проблеми
Ви можете бачити, що в .NET 8 був досягнутий значний прогрес в цій області.
Якщо порівняти .NET 7 MAUI з .NET 8 MAUI у прикладі додатку, що працює на Windows, виводячи результати виконання GC.GetTotalMemory() на екран:
Порівняння на Windows: .NET 7 проти .NET 8, зображення
Потім порівняйте приклад програми, що працює на macOS, але з набагато більшою кількістю сторінок у стеку навігації:
Порівняння на Mac: .NET 7 проти .NET 8, зображення
Дивіться приклад коду для цього проекту на GitHub для більш детальної інформації.
Діагностика витоків в .NET MAUI
Симптомом витоку пам'яті в .NET MAUI додатку може бути щось на кшталт цього:
- Перейдіть з цільової сторінки на підсторінку.
- Поверніться.
- Знову перейдіть на підсторінку.
- Повторіть.
- Пам'ять постійно зростає, поки операційна система не закриє програму через нестачу пам'яті.
У випадку Android ви можете побачити такі повідомлення журналу, як

У цьому прикладі маса розміром 116 МБ є досить великою для мобільного додатку, а також понад 46 000 об'єктів обгортки C# <-> Java!
Щоб точно визначити, чи підсторінка втрачає, ми можемо зробити кілька модифікацій у додатку .NET MAUI:
1. Додати реєстрацію у фіналізаторі. Наприклад:

Під час навігації по вашому додатку ви можете виявити, що цілі сторінки живуть вічно, якщо повідомлення журналу ніколи не відображається. Це поширений симптом витоку, оскільки будь-яке подання містить .Parent.Parent.Parent і т.д. аж до об'єкта Page.
2. Викликати GC.Collect() десь у додатку, наприклад, у конструкторі підсторінки:

Це робить GC більш детермінованим, оскільки ми змушуємо його працювати частіше. Кожного разу, коли ми переходимо на підсторінку, ми з більшою ймовірністю спричиняємо зникнення старих підсторінок. Якщо все працює належним чином, ми повинні побачити повідомлення журналу від фіналізатора.
Зауважте, що GC.Collect() призначено лише для налагодження. Після завершення дослідження вам не знадобиться ця функція у вашому додатку, тому обов'язково видаліть її після завершення.
3. Після внесення цих змін протестуйте релізну збірку вашого додатка.
На iOS, Android, macOS тощо ви можете переглядати консольний вивід вашого додатку, щоб визначити, що насправді відбувається під час виконання. adb logcat наприклад, є способом перегляду цих логів на Android.
Якщо ви працюєте під Windows, ви також можете скористатися Debug > Windows > Diagnostic Tools у Visual Studio для створення знімків пам'яті у Visual Studio. У майбутньому ми хотіли б, щоб засоби діагностики Visual Studio підтримували програми .NET MAUI, що працюють на інших платформах.
Дивіться нашу вікі-сторінку про пам'яті витоки wiki, для отримання додаткової інформації про витоки пам'яті у додатках .NET MAUI.
Патерни, що призводять до витоків: Події C#
Події C#, такі як поля, властивості тощо, можуть створювати сильні зв'язки між об'єктами. Розгляньмо ситуацію, коли все може піти не так.
Візьмемо, наприклад, кросплатформну властивість Grid.ColumnDefinitions:


- Grid має сильне відношення на колекцію ColumnDefinitionCollection через BindableProperty.
- ColumnDefinitionCollection має сильне відношення на Grid через подію ItemSizeChanged.
Якщо ви поставите точку зупинки на рядку з ItemSizeChanged +=, ви побачите, що подія має об'єкт EventHandler, де Target є сильним посиланням назад на Grid.
У деяких випадках такі циклічні посилання цілком прийнятні. Збирачі сміття середовища виконання .NET знають, як збирати цикли об'єктів, які вказують один на одного. Якщо немає "кореневого" об'єкта, який утримує обидва об'єкти, вони обидва можуть зникнути.
Проблема виникає з часом життя об'єктів: що станеться, якщо ColumnDefinitionCollection існуватиме протягом усього життя програми?
Розглянемо наступний Style у Application.Resources або Resources/Styles/Styles.xaml:

Якщо ви застосували цей стиль до сітки на довільній сторінці:
- Основний словник ресурсів програми містить стиль.
- Стиль містить ColumnDefinitionCollection.
- ColumnDefinitionCollection містить сітку.
- На жаль, Grid утримує сторінку через .Parent.Parent.Parent і т.д.
Ця ситуація може призвести до того, що цілі сторінки будуть жити вічно!
Зауваження Проблему з Grid виправлено у maui#16145, але вона є чудовим прикладом ілюстрації того, як події C# можуть піти не так, як треба.
Кільцеві посилання на платформах Apple
Ще з перших днів існування Xamarin.iOS існувала проблема з "циклічними посиланнями", навіть у середовищі виконання, що збирає сміття, як .NET. Об'єкти C# співіснують зі світом посилань на платформах Apple, і тому об'єкт C#, який має підкласи NSObject, може потрапити в ситуацію, коли він випадково може жити вічно — витік пам'яті. Це не є специфічною проблемою .NET, оскільки ви можете так само легко створити таку ж ситуацію в Objective-C або Swift. Зверніть увагу, що цього не відбувається на платформах Android або Windows.
Візьмемо, наприклад, наступне кільцеве посилання:


У цьому випадку:
- parent -> view через Subviews
- view -> parent через властивість Parent
- Кількість посилань на обидва об'єкти відмінна від нуля.
- Обидва об'єкти живуть вічно.
Ця проблема не обмежується полем або властивістю, ви можете створювати подібні ситуації з подіями C#:


У цьому випадку:
- MyView -> UIDatePicker через Subviews
- UIDatePicker -> MyView через ValueChanged та EventHandler.Target
- Обидва об'єкти живуть вічно.
Рішення для цього прикладу — зробити метод OnValueChanged static, що призведе до значення null Target в екземплярі EventHandler.
Іншим рішенням може бути розміщення OnValueChanged у підкласі, який не є NSObject:


Це шаблон, який ми використовуємо у більшості обробників .NET MAUI та інших підкласах UIView.
Подивіться на MemoryLeaksOniOS якщо ви хочете ізольовано погратися з деякими з цих сценаріїв у додатку для iOS без .NET MAUI.
Аналізатор Roslyn для платформ Apple
Також у нас є експериментальний аналізатор Roslyn, який може виявляти такі ситуації під час збірки. Щоб додати його до проєктів net7.0-ios, net8.0-ios тощо, ви можете просто встановити пакет NuGet:

Деякі приклади попередження можуть бути наступними:

Зауважте, що аналізатор може попередити про можливу проблему, тому його ввімкнення у великій наявній кодовій базі може бути досить шумним. Перевірка пам'яті під час виконання — найкращий спосіб визначити, чи дійсно є витік пам'яті.
Інструменти та документація
Спрощені dotnet-trace та dotnet-dsrouter
У .NET 7 профілювання мобільних додатків було дещо складним завданням. Вам потрібно було запустити dotnet-dsrouter і dotnet-trace разом і виконати всі налаштування правильно, щоб мати змогу отримати .nettrace або speedscope для дослідження продуктивності. Також не було вбудованої підтримки dotnet-gcdump для підключення до dotnet-dsrouter для отримання знімків пам'яті запущеної програми .NET MAUI.
У .NET 8 ми спростили цей сценарій, створивши нові команди для dotnet-dsrouter, які спрощують робочий процес.
Щоб переконатися, що у вас встановлені найновіші діагностичні засоби, ви можете встановити їх за посиланням:


Переконайтеся, що у вас встановлені версії цих інструментів не нижче 8.x:

Щоб профілювати Android-додаток на емуляторі Android, спочатку створіть і встановіть свій додаток у режимі Release, наприклад, так:

Далі відкрийте термінал для запуску dotnet-dsrouter

Потім у другому вікні терміналу ми можемо встановити системну властивість debug.mono.profile Android, як замінник $DOTNET_DiagnosticPorts:

Зауважте, що Android не має належної підтримки змінних оточення, таких як $DOTNET_DiagnosticPorts. Ви можете створити текстовий файл AndroidEnvironment для встановлення змінних середовища, але системні властивості Android можуть бути простішими, оскільки для їх встановлення не потрібно перезбирати програму.
Після запуску програми для Android вона повинна мати можливість підключитися до dotnet-dsrouter -> dotnet-trace і записати інформацію про профілювання продуктивності для дослідження. Аргумент --format є необов'язковим і за замовчуванням має значення .nettrace. Однак файли .nettrace можна переглянути лише за допомогою Perfview у Windows, тоді як JSON-файли speedscope можна переглянути "на" macOS або Linux, завантаживши їх на https://speedscope.app.
Зауваження Якщо ви надаєте ідентифікатор процесу програмі dotnet-trace, вона знає, як визначити, чи є ідентифікатор процесу dotnet-dsrouter, і з'єднатися через нього належним чином.
dotnet-dsrouter має наступні нові команди для спрощення робочого процесу:
- dotnet-dsrouter android: Пристрої Android
- dotnet-dsrouter android-emu: Емулятори Android
- dotnet-dsrouter ios: пристрої iOS
- dotnet-dsrouter ios-sim: симулятори iOS
Дивіться вікі .NET MAUI для отримання додаткової інформації про профілювання додатків .NET MAUI на кожній платформі.
Підтримка мобільних пристроїв dotnet-gcdump
У .NET 7 у нас був дещо складний метод (див. вікі) для отримання знімка пам'яті програми у середовищі виконання Mono (наприклад, iOS або Android). Вам потрібно було використовувати спеціальний для Mono провайдер подій, наприклад:

А потім ми поклалися на Філіпа Навару (Filip Navara) та його інструмент mono-gcdump (дякуємо Філіпу!), щоб перетворити файл .nettrace у .gcdump, який можна відкрити у Visual Studio або PerfView.
У .NET 8 ми тепер маємо dotnet-gcdump для мобільних сценаріїв. Якщо ви хочете отримати знімок пам'яті запущеної програми, ви можете використовувати dotnet-gcdump так само як і dotnet-trace:

Зауваження Для цього потрібні такі самі налаштування, як і для dotnet-trace, наприклад, -p:AndroidEnableProfiler=true, dotnet-dsrouter, команди adb тощо.
Це значно спрощує наш робочий процес дослідження витоків пам'яті в додатках .NET MAUI. Дивіться нашу wiki вікі-сторінку про витоки пам'яті для отримання додаткової інформації.
Source