.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
Тож замість цього ми можемо зробити так: