(Время чтения статьи: 20 минут)
Пользовательский интерфейс многоплатформенного приложения .NET (MAUI) объединяет API-интерфейсы Android, iOS, macOS и Windows в единый API, поэтому вы можете написать приложение, которое будет работать на многих платформах. Мы фокусируемся на повышении вашей повседневной продуктивности и производительности ваших приложений. Мы считаем, что повышение продуктивности разработчиков не должно происходить за счет производительности приложений.
То же самое касается размера приложения: какие накладные расходы возникают в пустом приложении .NET MAUI. Когда мы начали оптимизировать .NET MAUI, стало ясно, что iOS необходимо проделать некоторую работу по увеличению размера приложения, в то время как Android не хватает производительности при загрузке.
Приложение iOS для нового проекта Maui в DotNet изначально будет иметь размер около 18 МБ. Аналогично, время запуска .NET MAUI на Android в предыдущих предварительных версиях не было идеальным:
приложение | рамка | Время запуска (мс) |
---|---|---|
Xamarin.Android | Xamarin | 306.5 |
Xamarin.Forms | Xamarin | 498.6 |
Xamarin.Forms (Shell) | Xamarin | 817.7 |
dotnet new android | .NET 6 (ранняя предварительная версия) | 210.5 |
dotnet new maui | .NET 6 (ранняя предварительная версия) | 683.9 |
.NET Podcast | .NET 6 (ранняя предварительная версия) | 1299.9 |
Это в среднем 10 запусков на устройстве Pixel 5. См. нашу документацию по профилированию maui, чтобы узнать, как получить эти числа.
Наша цель — сделать .NET MAUI быстрее, чем его предшественник Xamarin. Понятно, что нам также есть над чем поработать и над самим .NET MAUI. Новые шаблоны dotnet для Android выпускаются быстрее, чем Xamarin.Android, главным образом из-за новой среды выполнения BCL и Mono в .NET 6.
Новые шаблоны .NET maui пока не используют режим навигации Shell, но планируется сделать его режимом навигации по умолчанию для .NET maui. Когда мы приняли это изменение, мы знали, что шаблоны повлияют на производительность.
Чтобы достичь того, что мы имеем сегодня, потребовалось сотрудничество нескольких разных команд. Мы улучшили Microsoft.Extensions, использование внедрения зависимостей, компиляцию AOT, взаимодействие с Java, XAML, код .NET MAUI и многое другое.
приложение | рамка | Время запуска (мс) |
---|---|---|
Xamarin.Android | Xamarin | 306.5 |
Xamarin.Forms | Xamarin | 498.6 |
Xamarin.Forms (Shell) | Xamarin | 817.7 |
dotnet new android | .NET 6 (MAUI GA) | 182.8 |
dotnet new maui (No Shell**) | .NET 6 (MAUI GA) | 464.2 |
dotnet new maui (Shell) | .NET 6 (MAUI GA) | 568.1 |
.NET Podcast App (Shell) | .NET 6 (MAUI GA) | 814.2 |
**Это исходный шаблон dotnet new maui без использования оболочки.
Контент очень богатый, проверьте его, чтобы узнать, есть ли какие-либо обновления, которых вы с нетерпением ждете!
Основное содержание
❖ Улучшение производительности при запуске
Улучшение производительности при запуске
▌Выполнение анализа на мобильных устройствах
Я должен упомянуть инструменты диагностики .NET, доступные на мобильных платформах, потому что это наш шаг 0 в ускорении работы .NET MAUI.
Анализ .NET 6 Приложение Android требует использования инструмента под названием dotnet-dsrouter. Этот инструмент позволяет отслеживать соединение Dotnet с работающим мобильным приложением в Android, iOS и т. д. Вероятно, это то, что мы используем. Анализ .NET Самый эффективный инструмент MAUI.
Чтобы начать использовать dotnettrace и dsrouter, сначала настройте некоторые параметры через adb и запустите dsrouter:
adb reverse tcp:9000 tcp:9001
adb shell setprop debug.mono.profile '127.0.0.1:9000,suspend'
dotnet-dsrouter client-server -tcps 127.0.0.1:9001 -ipcc /tmp/maui-app --verbose debug
Следующий шаг — запустить трассировку dotnet, например:
dotnet-trace collect --diagnostic-port /tmp/maui-app --format speedscope
При запуске программы используйте -c Releaseи-p:androidEnableProfiler=true После сборки приложения Android, когда dotnet Когда трассировка выводится, вы заметите соединение:
Press <Enter> or <Ctrl+C> to exit...812 (KB)
После того, как ваше приложение полностью запустится,Просто нажмите клавишу ввода, чтобы сохранить *.speedscope в текущем каталоге. Вы можете открыть этот файл по адресу https://speedscope.app.,Узнайте подробнее, сколько времени потратил каждый метод во время запуска приложения:
Более подробная информация об использовании отслеживания dotnet в приложении для Android,Пожалуйста, ознакомьтесь с нашей документацией. Рекомендую проанализировать версию Release на устройстве Android,Чтобы получить максимальную производительность приложения в реальном мире.
▌Измерение с течением времени
Наши друзья из команды .NET Foundation создали конвейер для отслеживания сценариев производительности .NET MAUI, таких как:
Это позволяет нам видеть влияние улучшений или регрессов с течением времени, просматривая цифры для каждой фиксации в репозитории dotnet/maui. Мы также можем определить, вызвана ли эта разница изменениями в xamarin-android, xamarin-macios или dotnet/runtime.
Например, график времени запуска (в миллисекундах) для нового шаблона dotnet maui, работающего на физическом устройстве Pixel 4a:
Обратите внимание, что Pixel 4a намного медленнее Pixel 5.
Мы можем выявить регрессы и улучшения, произошедшие в dotnet/maui. Это очень полезно для отслеживания наших целей.
Аналогично, мы можем видеть прогресс приложения .NET Podcast с течением времени на том же устройстве Pixel 4a:
На этой диаграмме мы действительно уделяем особое внимание, потому что это «настоящее приложение» и оно близко к тому, что разработчики видят в своих собственных мобильных приложениях.
Что касается размера приложения,Это более стабильное число – когда дела идут хуже или лучше.,Обнулить легко:
Подробные сведения об этих улучшениях см. в dotnet-podcasts#58, Android x#520 и dotnet/maui#6419.
▌Чужой АОТ
В наших первоначальных тестах производительности .NET MAUI мы видели, как работает код, скомпилированный JIT (точно в срок) и AOT (заранее):
приложение | Время JIT (мс) | Время АОТ (мс) |
---|---|---|
DotNet Нью-Мауи | 1078.0ms | 683.9ms |
JIT-обработка происходит каждый раз при вызове метода C#, что неявно влияет на производительность запуска мобильного приложения.
Другая проблема — увеличение размера приложения из-за AOT. Каждая сборка .NET добавляет в последнее приложение собственную библиотеку Android. Чтобы лучше использовать оба мира,Запуск отслеживания или профилирования AOT — это текущая функция Xamarin.Android. Это механизм пути запуска приложения AOT.,Это значительно сокращает время запуска,И лишь скромное увеличение в размерах.
В .NET В версии 6 это опция по умолчанию, которая имеет смысл. Раньше для выполнения любого вида AOT с помощью Xamarin.Android требовался Android. NDK (загрузка нескольких ГБ). У нас не установлен андроид Приложение NDK, построенное на базе AOT,сделать это возможным。
мы за dotnet new android, maui, иmaui — встроенные файлы конфигурации для шаблонов блазор, которые приносят пользу большинству приложений. Если ты хочешь В .NET Чтобы записать собственный профиль в 6, вы можете попробовать наш экспериментальный Mono.Profiler. пакет Андроид. Мы работаем над полной поддержкой регистрации пользовательских профилей в будущей версии .NET.
Подробные сведения об этом улучшении см. в разделе xamarin-Android#6547 и dotnet/maui#4859.
▌Хранилище в одном файле сборки
Раньше, если вы просматривали содержимое Release android .apk в своей любимой утилите для работы с zip-файлами, вы могли видеть, что сборки .NET расположены по адресу:
assemblies/Java.Interop.dll
assemblies/Mono.android.dll
assemblies/System.Runtime.dll
assemblies/arm64-v8a/System.Private.CoreLib.dll
assemblies/armeabi-v7a/System.Private.CoreLib.dll
assemblies/x86/System.Private.CoreLib.dll
assemblies/x86_64/System.Private.CoreLib.dll
Эти файлы загружаются индивидуально с помощью системного вызова mmap.,Это стоимость каждой .NET сборки в приложении. Это реализовано на C/C++ в рабочей нагрузке Android.,Используйте обратные вызовы, предоставляемые средой выполнения Mono, для загрузки сборки. Приложение MAUI имеет много сборок,Итак, мы представили новую функцию $(androidUseAssemblyStore),Эта функция включена по умолчанию в версии Release.
После этого изменения вы получите:
assemblies/assemblies.manifest
assemblies/assemblies.blob
assemblies/assemblies.arm64_v8a.blob
assemblies/assemblies.armeabi_v7a.blob
assemblies/assemblies.x86.blob
assemblies/assemblies.x86_64.blob
Теперь при запуске Android требуется вызвать mmap только дважды: один раз для assemblies.blob и второй раз для BLOB-объекта, зависящего от архитектуры. Эта пара имеет многое. netсборкаприложениеоказал значительное влияние。
Если вам нужно проверить IL этих сборок в скомпилированном приложении Android, мы создали инструмент чтения хранилища сборок для «распаковки» этих файлов.
Другой вариант — отключить эти настройки при сборке приложения:
dotnet build -c Release -p:AndroidUseAssemblyStore=false -p:Android EnableAssemblyCompression=false
Таким образом, вы можете распаковать полученный файл .apk с помощью вашего любимого инструмента сжатия и проверить сборку .NET с помощью такого инструмента, как ILSpy. Это отличный способ диагностировать проблемы триммера/линкера.
Дополнительные сведения об этом улучшении см. в разделе xamarin-android#6311.
▌Spanify RegisterNativeMembers
Когда объект C# создается в Java, вызывается небольшая оболочка Java, например:
public class MainActivity extends Android.app.Activity
{
public static final String methods;
static {
methods = "n_onCreate:(LAndroid/os/Bundle;)V:GetOnCreate_Landroid_os_Bundle_Handler\n";
mono.Android.Runtime.register ("foo.MainActivity, foo", MainActivity.class, methods);
}
Список методов представляет собой разделенный \n и : список сигнатур собственного интерфейса Java (JNI), которые переопределяются в управляемом коде C#. Для каждого метода Java, который вы переопределяете в C#, вы получаете такой метод.
Когда фактический метод Java onCreate() вызывается как действие Android:
public void onCreate (Android.os.Bundle p0)
{
n_onCreate (p0);
}
private native void n_onCreate (Android.os.Bundle p0);
С помощью различных магических действий и жестов n_onCreate вызывается в среду выполнения Mono и вызывает метод OnCreate() в C#.
Код для разделения списков методов, разделенных \nи:-delimited, был написан на заре Xamarin с использованием string.Split(). Можно сказать,Span<T>не существовало в то время,Но мы можем использовать его сейчас! Это повышает стоимость любого класса C#, который наследует класс Java!,Поэтому это лучший выбор, чем .NET. Более широкое улучшение MAUI.
Вы можете спросить: «Зачем вообще использовать строки?» Использование массивов Java, похоже, оказывает большее влияние на производительность, чем разделение строк. В наших тестах вызов JNI для получения элементов массива Java работал хуже, чем строки. Новое использование Split и Span. У нас есть некоторые идеи о том, как перестроить это в будущих версиях .NET.
Помимо .NET 6, это изменение также включено в последнюю версию Android для текущих клиентов Xamarin.
Дополнительные сведения об этом улучшении см. в разделе xamarin-android#6708.
▌System.Reflection.Emit и конструктор
На заре использования Xamarin у нас был довольно сложный способ вызова конструкторов C# из Java.
Во-первых, у нас есть несколько вызовов отражения, которые происходят при запуске:
static MethodInfo newobject = typeof (System.Runtime.CompilerServices.RuntimeHelpers).GetMethod ("GetUninitializedObject", BindingFlags.Public | BindingFlags.Static)!;
static MethodInfo gettype = typeof (System.Type).GetMethod ("GetTypeFromHandle", BindingFlags.Public | BindingFlags.Static)!;
static FieldInfo handle = typeof (Java.Lang.Object).GetField ("handle", BindingFlags.NonPublic | BindingFlags.Instance)!;
Похоже, это перенесено из более ранних версий Mono и продолжается по сей день. Например, RuntimeHelpers.GetUninitializedObject() можно вызвать напрямую.
Затем некоторое сложное использование System.Reflection.Emit, и в
Передайте экземпляр cinfo в System.Reflection.ConstructorInfo:
DynamicMethod method = new DynamicMethod (DynamicMethodNameCounter.GetUniqueName (), typeof (void), new Type [] {typeof (IntPtr), typeof (object []) }, typeof (DynamicMethodNameCounter), true);
ILGenerator il = method.GetILGenerator ();
il.DeclareLocal (typeof (object));
il.Emit (OpCodes.Ldtoken, type);
il.Emit (OpCodes.Call, gettype);
il.Emit (OpCodes.Call, newobject);
il.Emit (OpCodes.Stloc_0);
il.Emit (OpCodes.Ldloc_0);
il.Emit (OpCodes.Ldarg_0);
il.Emit (OpCodes.Stfld, handle);
il.Emit (OpCodes.Ldloc_0);
var len = cinfo.GetParameters ().Length;
for (int i = 0; i < len; i++) {
il.Emit (OpCodes.Ldarg, 1);
il.Emit (OpCodes.Ldc_I4, i);
il.Emit (OpCodes.Ldelem_Ref);
}
il.Emit (OpCodes.Call, cinfo);
il.Emit (OpCodes.Ret);
return (Action<IntPtr, object?[]?>) method.CreateDelegate (typeof (Action <IntPtr, object []>));
Возвращаемый делегат вызывается так, что IntPtr является дескриптором подкласса Java.Lang.Object, а Object[] — любым аргументом этого конкретного конструктора C#. Emit требует значительных затрат при первом использовании при запуске и при каждом последующем вызове.
После тщательного анализа мы можем сделать поле дескриптора внутренним и упростить этот код:
var newobj = RuntimeHelpers.GetUninitializedObject (cinfo.DeclaringType);
if (newobj is Java.Lang.Object o) {
o.handle = jobject;
} else if (newobj is Java.Lang.Throwable throwable) {
throwable.handle = jobject;
} else {
throw new InvalidOperationException ($"Unsupported type: '{newobj}'");
}
cinfo.Invoke (newobj, parms);
Этот код создает объект без вызова конструктора, устанавливает поля дескрипторов и затем вызывает конструктор. Это сделано для того, чтобы при запуске конструктора C# Handle был действителен для любого Java.Lang.Object. Handle требуется для любого взаимодействия Java внутри конструктора (например, для вызова других методов Java в классе), а также для вызова любого базового конструктора Java.
Новый код значительно улучшает любой конструктор C#, вызываемый из Java, поэтому это конкретное изменение улучшает не только .NET MAUI. Помимо .NET 6, это изменение также включено в последнюю версию Xamarin.android для текущих клиентов.
Дополнительные сведения об этом улучшении см. в разделе xamarin-android#6766.
▌System.Reflection.Emit и методы
Когда вы переопределяете метод Java в C#, например:
public class MainActivity : Activity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
//...
}
}
В процессе преобразования Java в C# мы должны инкапсулировать методы C# для обработки исключений, таких как:
try
{
// Call the actual C# method here
}
catch (Exception e) when (_unhandled_exception (e))
{
androidEnvironment.UnhandledException (e);
if (Debugger.IsAttached || !JNIEnv.PropagateExceptions)
throw;
}
Например, если управляемое исключение не обрабатывается в OnCreate(), это фактически вызовет собственный сбой (и отсутствие трассировки управляемого стека C#). Нам нужно убедиться, что отладчик может прервать работу при присоединении исключения, иначе трассировка стека C# будет записана в журнал.
Начиная с Xamarin, приведенный выше код генерируется с помощью System.Reflection.Emit:
var dynamic = new DynamicMethod (DynamicMethodNameCounter.GetUniqueName (), ret_type, param_types, typeof (DynamicMethodNameCounter), true);
var ig = dynamic.GetILGenerator ();
LocalBuilder? retval = null;
if (ret_type != typeof (void))
retval = ig.DeclareLocal (ret_type);
ig.Emit (OpCodes.Call, wait_for_bridge_processing_method!);
var label = ig.BeginExceptionBlock ();
for (int i = 0; i < param_types.Length; i++)
ig.Emit (OpCodes.Ldarg, i);
ig.Emit (OpCodes.Call, dlg.Method);
if (retval != null)
ig.Emit (OpCodes.Stloc, retval);
ig.Emit (OpCodes.Leave, label);
bool filter = Debugger.IsAttached || !JNIEnv.PropagateExceptions;
if (filter && JNIEnv.mono_unhandled_exception_method != null) {
ig.BeginExceptFilterBlock ();
ig.Emit (OpCodes.Call, JNIEnv.mono_unhandled_exception_method);
ig.Emit (OpCodes.Ldc_I4_1);
ig.BeginCatchBlock (null!);
} else {
ig.BeginCatchBlock (typeof (Exception));
}
ig.Emit (OpCodes.Dup);
ig.Emit (OpCodes.Call, exception_handler_method!);
if (filter)
ig.Emit (OpCodes.Throw);
ig.EndExceptionBlock ();
if (retval != null)
ig.Emit (OpCodes.Ldloc, retval);
ig.Emit (OpCodes.Ret);
Этот код вызывается дважды как dotnet new android приложение, но ~58 раз как дотнет new mauiприложение!
Мы поняли, что на самом деле можем написать строго типизированный «быстрый путь» для каждого универсального типа делегата вместо использования System.Reflection.Emit. Существует сгенерированный делегат, соответствующий каждой подписи:
void OnCreate(Bundle savedInstanceState);
// Maps to *JNIEnv, JavaClass, Bundle
// Internal to each assembly
internal delegate void _JniMarshal_PPL_V(IntPtr, IntPtr, IntPtr);
Таким образом, мы можем перечислить все используемые дотнеты. подпись приложения maui,например:
class JNINativeWrapper
{
static Delegate? CreateBuiltInDelegate (Delegate dlg, Type delegateType)
{
switch (delegateType.Name)
{
// Unsafe.As<T>() is used, because _JniMarshal_PPL_V is generated internal in each assembly
case nameof (_JniMarshal_PPL_V):
return new _JniMarshal_PPL_V (Unsafe.As<_JniMarshal_PPL_V> (dlg).Wrap_JniMarshal_PPL_V);
// etc.
}
return null;
}
// Static extension method is generated to avoid capturing variables in anonymous methods
internal static void Wrap_JniMarshal_PPL_V (this _JniMarshal_PPL_V callback, IntPtr jnienv, IntPtr klazz, IntPtr p0)
{
// ...
}
}
Недостаток этого подхода в том, что при использовании новых сигнатур нам приходится перечислять больше случаев. Мы не хотим перечислять все комбинации исчерпывающе, поскольку это приведет к увеличению размера IL. Мы изучаем, как улучшить это в будущих версиях .NET.
Дополнительные сведения об этом улучшении см. в разделах xamarin-android#6657 и xamarin-android#6707.
▌Обновленные API Java.Interop.
Исходный API Xamarin в Java.Interop.dll — это этот API:
«Новый метод», вызываемый в Java, занимает меньше памяти за вызов:
При создании привязок C# для методов Java во время сборки по умолчанию используется более новый/более быстрый метод — он уже некоторое время доступен в Xamarin.Android. Раньше в проектах привязки Java можно было установить для $(AndroidCodegenTarget) значение XAJavaInterop1, что кэшировало и повторно использовало экземпляр jmethodID при каждом вызове. См. документацию java.interop для истории этой функции.
Другими проблемными местами являются «ручные» привязки. Эти методы, как правило, тоже часто используются, поэтому стоит их исправить!
Несколько примеров улучшения этой ситуации:
▌Многомерный массив Java
При передаче массива C# туда и обратно в Java на промежуточном этапе необходимо скопировать массив, чтобы соответствующая среда выполнения могла получить к нему доступ. На самом деле это ситуация, связанная с опытом разработчиков, поскольку ожидается, что разработчики C# будут писать такие вещи:
var array = new int[] { 1, 2, 3, 4};
MyJavaMethod (array);
Что будет сделано в MyJavaMethod:
IntPtr native_items = JNIEnv.NewArray (items);
try
{
// p/invoke here, actually calls into Java
}
finally
{
if (items != null)
{
JNIEnv.CopyArray (native_items, items); // If the calling method mutates the array
JNIEnv.DeleteLocalRef (native_items); // Delete our Java local reference
}
}
JNIEnv.NewArray() обращается к «карте типов», чтобы узнать, какой класс Java необходимо использовать для элементов массива.
Существует проблема с конкретным API-интерфейсом Android, используемым новым проектом dotnet maui:
public ColorStateList (int[][]? states, int[]? colors)
Найден многомерный массив int[][] с доступом к «карте типов» для каждого элемента. Мы можем видеть это во многих случаях, когда включено дополнительное ведение журнала:
monodroid: typemap: failed to map managed type to Java type: System.Int32, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e (Module ID: 8e4cd939-3275-41c4-968d-d5a4376b35f5; Type token: 33554653)
monodroid-assembly: typemap: called from
monodroid-assembly: at android.Runtime.JNIEnv.TypemapManagedToJava(Type )
monodroid-assembly: at android.Runtime.JNIEnv.GetJniName(Type )
monodroid-assembly: at android.Runtime.JNIEnv.FindClass(Type )
monodroid-assembly: at android.Runtime.JNIEnv.NewArray(Array , Type )
monodroid-assembly: at android.Runtime.JNIEnv.NewArray[Int32[]](Int32[][] )
monodroid-assembly: at android.Content.Res.ColorStateList..ctor(Int32[][] , Int32[] )
monodroid-assembly: at Microsoft.Maui.Platform.ColorStateListExtensions.CreateButton(Int32 enabled, Int32 disabled, Int32 off, Int32 pressed)
В этом случае мы должны иметь возможность один раз вызвать JNIEnv.FindClass() и повторно использовать это значение для каждого элемента массива!
Мы изучаем, как улучшить это в будущих выпусках .NET. Одним из таких примеров является dotnet/maui#5654, где мы просто рассматриваем возможность создания массивов полностью на Java.
Дополнительные сведения об этом улучшении см. в разделе xamarin-android#6870.
▌Использование Glide для изображений Android
Glideсовременныйandroidприложение Рекомендуемая библиотека загрузки изображений。Google Docs даже рекомендует использовать его,Из-за встроенногоandroid Класс Bitmap может быть сложно использовать правильно. glidex.forms — это прототип использования Glide с Xamarin.Forms. но мы будем Glide Продвижение в будущее .NET MAUI «Способ» загрузки изображений в формате .
Чтобы уменьшить накладные расходы на взаимодействие JNI, реализация .NET MAUI Glide в основном написана на Java, например:
import com.bumptech.glide.Glide;
//...
public static void loadImageFromUri(ImageView imageView, String uri, Boolean cachingEnabled, ImageLoaderCallback callback) {
//...
RequestBuilder<Drawable> builder = Glide
.with(imageView)
.load(androidUri);
loadInto(builder, imageView, cachingEnabled, callback);
}
ImageLoaderCallback является подклассом C# для обработки завершения в управляемом коде. В результате производительность изображений из Интернета должна быть значительно улучшена по сравнению с тем, что ранее было доступно в Xamarin.Forms.
Дополнительные сведения см. в dotnet/maui#759 и dotnet/maui#5198.
▌Уменьшить количество вызовов взаимодействия Java
Допустим, у вас есть следующий Java API:
public void setFoo(int foo);
public void setBar(int bar);
Эти методы взаимодействуют следующим образом:
public unsafe static void SetFoo(int foo)
{
JniArgumentValue* __args = stackalloc JniArgumentValue[1];
__args[0] = new JniArgumentValue(foo);
return _members.StaticMethods.InvokeInt32Method("setFoo.(I)V", __args);
}
public unsafe static void SetBar(int bar)
{
JniArgumentValue* __args = stackalloc JniArgumentValue[1];
__args[0] = new JniArgumentValue(bar);
return _members.StaticMethods.InvokeInt32Method("setBar.(I)V", __args);
}
Таким образом, вызов этих двух методов приведет к вызову stackalloc дважды и p/invoke дважды. Было бы более эффективно создать небольшую оболочку Java, например:
public void setFooAndBar(int foo, int bar)
{
setFoo(foo);
setBar(bar);
}
Переведено как:
public unsafe static void SetFooAndBar(int foo, int bar)
{
JniArgumentValue* __args = stackalloc JniArgumentValue[2];
__args[0] = new JniArgumentValue(foo);
__args[1] = new JniArgumentValue(bar);
return _members.StaticMethods.InvokeInt32Method("setFooAndBar.(II)V", __args);
}
.NET Представления MAUI, по сути, являются объектами C# и имеют множество свойств, которые необходимо устанавливать точно так же в Java. Если мы перенесем это концептуальное приложение на .NET Каждый андроид в MAUI В View мы можем создать метод с примерно 18 параметрами для создания представления. Последующие изменения свойств могут напрямую вызывать стандартный Android. api。
Это значительное улучшение производительности очень простого элемента управления .NET MAUI:
метод | средний | ошибка | стандартное отклонение | Поколение 0 | назначенный |
---|---|---|---|---|---|
Border(Before) | 323.2 µs | 0.82 µs | 0.68 µs | 0.9766 | 5KB |
Border(After) | 242.3 µs | 1.34 µs | 1.25 µs | 0.9766 | 5KB |
CollectionView(Before) | 354.6 µs | 2.61 µs | 2.31 µs | 1.4648 | 6KB |
CollectionView(After) | 258.3 µs | 0.49 µs | 0.43 µs | 1.4648 | 6KB |
Дополнительные сведения об этом улучшении см. в dotnet/maui#3372.
▌Перенос XML Android на Java
Оглядываясь назад на результаты трассировки dotnet на Android, мы видим, что разумное количество времени тратится на:
20.32.ms mono.andorid!Andorid.Views.LayoutInflater.Inflate
Просмотрите трассировку стека,На самом деле время тратится на макет расширения Android/Java.,И В На стороне .NET никакой работы не происходит.
Если вы посмотрите на скомпилированные файлы .apk android и res/layouts/bottomtablayout. В Android Studio XML — это просто XML. Лишь немногие идентификаторы преобразуются в целые числа. Это означает, что Android должен анализировать XML и создавать объекты Java с помощью API-интерфейса отражения Java. Кажется, мы можем добиться более высокой производительности без использования XML?
Используя стандартное сравнение BenchmarkDotNet, мы обнаружили, что использование Android Layout работает даже хуже, чем использование C#, когда дело доходит до взаимодействия:
метод | метод | ошибка | стандартное отклонение | назначенный |
---|---|---|---|---|
Java | 338.4 µs | 4.21 µs | 3.52 µs | 744 B |
CSharp | 410.2 µs | 7.92 µs | 6.61 µs | 1,336 B |
XML | 490.0 µs | 7.77 µs | 7.27 µs | 2,321 B |
Далее мы настраиваем BenchmarkDotNet как одиночный запуск, чтобы лучше имитировать то, что происходит при запуске:
метод | медиана |
---|---|
Java | 4.619 ms |
CSharp | 37.337 ms |
XML | 39.364 ms |
нас В .NET В MAUI представлен более простой макет с навигацией по нижней вкладке:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/bottomtab.navarea"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="fill"
android:layout_weight="1" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomtab.tabbar"
android:theme="@style/Widget.Design.BottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
Мы можем портировать его на четыре метода Java, например:
@NonNull
public static List<View> createBottomTabLayout(Context context, int navigationStyle);
@NonNull
public static LinearLayout createLinearLayout(Context context);
@NonNull
public static FrameLayout createFrameLayout(Context context, LinearLayout layout);
@NonNull
public static BottomNavigationView createNavigationBar(Context context, int navigationStyle, FrameLayout bottom)
Это позволяет нам переключаться с C# на Java только 4 раза при создании навигации по нижней вкладке на Android. Это также позволяет операционной системе Android «раздувать» объекты Java, пропуская загрузку и анализ .xml. Мы реализовали эту идею в dotnet/maui, удалив все вызовы LayoutInflater.Inflate() при запуске.
Подробные сведения об этих улучшениях см. в dotnet/maui#5424, dotnet/maui#5493 и dotnet/maui#5528.
▌Удалить Microsoft.Extensions.Hosting
хостинг предоставляет универсальный хост .NET,Используется для управления внедрением зависимостей, ведением журнала, настройкой и жизненным циклом в приложении .NET. Это влияет на время запуска,Кажется неуместным переносить приложение.
Имеет смысл удалить использование Microsoft.Extensions.Hosting из .NET MAUI. .net MAUI не пытается взаимодействовать с «универсальным хостом» для создания DI-контейнеров, а имеет собственную простую реализацию, оптимизированную для мобильного запуска. Кроме того, .net MAUI больше не добавляет поставщиков журналов по умолчанию.
Благодаря этому изменению мы видим dotnet new maui Время запуска приложения Android уменьшено на 5-10%. На iOS уменьшает размер того же приложения с 19.2. MB => 18.0 MB。
Дополнительные сведения см. в dotnet/maui#4505 и dotnet/maui#4545.
▌Уменьшить инициализацию оболочки при запуске.
Xamarin. Forms Shell — это режим кроссплатформенной навигации по приложениям. Этот режим В .NET Представленный в MAUI, он рекомендуется в качестве способа создания приложения по умолчанию.
Когда мы узнали о стоимости использования оболочки при запуске (для Xamarin, Xamarin.form и .NET MAUI), мы обнаружили несколько областей, которые можно оптимизировать:
Подробные сведения об этом улучшении см. в dotnet/maui#5262.
▌Шрифты не должны использовать временные файлы.
Много времени уходит на загрузку шрифтов в приложении .NET MAUI:
32.19ms Microsoft.Maui!Microsoft.Maui.FontManager.CreateTypeface(System.ValueTuple`3<string, Microsoft.Maui.FontWeight, bool>)
При проверке кода он выполняет больше работы, чем необходимо:
1. Сохраните файл androidAsset во временную папку.
2. Используйте API Android, Typeface.CreateFromFile() для загрузки файлов.
На самом деле мы можем использовать Android API Typeface.CreateFromAsset() напрямую, вообще не используя временные файлы.
Дополнительные сведения об этом улучшении см. в dotnet/maui#4933.
▌Рассчитывается на платформе при составлении
Использование расширения тега {OnPlatform}:
<Label Text="Platform: " />
<Label Text="{OnPlatform Default=Unknown, android=android, iOS=iOS" />
... на самом деле можно вычислить во время компиляции, и net6.0-android и net6.0-ios получат соответствующее значение. В будущей версии .NET мы выполним ту же оптимизацию для элементов XML.
Дополнительные сведения см. в dotnet/maui#4829 и dotnet/maui#5611.
▌Использование преобразователей компиляции в XAML
Следующие типы теперь преобразуются во время компиляции XAML, а не во время выполнения:
Это приводит к более качественному/быстрому созданию IL-кодов из файлов .xaml.
▌Оптимизация анализа цвета
Исходный код Microsoft.Maui.Graphics.Color.Parse() можно переписать, чтобы лучше использовать Span и избежать выделения строк.
метод | средний | ошибка | стандартное отклонение | Поколение 0 | назначенный |
---|---|---|---|---|---|
Разбор (ранее) | 99.13 ns | 0.281 ns | 0.235 ns | 0.0267 | 168 B |
Разобрать (после) | 52.54 ns | 0.292 ns | 0.259 ns | 0.0051 | 32 B |
能够существоватьReadonlySpan<char> Использование операторов переключения в dotnet/csharpang#1881 еще больше улучшит эту ситуацию в будущих версиях .NET.
Дополнительные сведения об этом улучшении см. в dotnet/Microsoft.Maui.Graphics#343 и dotnet/Microsoft.Maui.Graphics#345.
▌Не используйте сравнения строк с учетом языка и региональных параметров.
Оглядываясь назад на результаты трассировки DotNet из нового проекта NAUI, можно увидеть истинную цену первого сравнения строк с учетом культуры на Android:
6.32ms Microsoft.Maui.Controls!Microsoft.Maui.Controls.ShellNavigationManager.GetNavigationState
3.82ms Microsoft.Maui.Controls!Microsoft.Maui.Controls.ShellUriHandler.FormatUri
3.82ms System.Private.CoreLib!System.String.StartsWith
2.57ms System.Private.CoreLib!System.Globalization.CultureInfo.get_CurrentCulture
Фактически, в этом примере мы даже не хотим использовать сравнение культур и региональных параметров — это просто код, взятый из Xamarin.Forms.
Например, если у вас есть:
if (text.StartsWith("f"))
{
// do something
}
В этом случае вы можете просто сделать это:
if (text.StartsWith("f", StringComparision.Ordinal))
{
// do something
}
Если выполняется на протяжении всего приложения,System.Globalization.CultureInfo.CurrentCulture можно избежать вызова,И может немного улучшить общую скорость операторов If.
Чтобы решить эту ситуацию в репозитории dotnet/maui, мы ввели правила анализа кода для их фиксации:
dotnet_diagnostic.CA1307.severity = error
dotnet_diagnostic.CA1309.severity = error
Подробные сведения об улучшениях см. в dotnet/maui#4988.
▌Ленивое создание журналов
API-интерфейс ConfigurationFonts() тратит некоторое время при запуске на выполнение некоторой работы, которую можно отложить на потом. Мы также можем улучшить общее использование инфраструктуры журналирования в Microsoft.Extensions.
Некоторые из улучшений, которые мы сделали, заключаются в следующем:
Дополнительные сведения об этом улучшении см. в dotnet/maui#5103.
▌Внедрение зависимостей с использованием фабричного метода
При использовании Microsoft.Extensions. DependencyInjection, служба регистрации, например:
IServiceCollection services /* ... */;
services.TryAddSingleton<IFooService, FooService>();
Microsoft.Extensions необходимо выполнить некоторые действия System.Reflection, чтобы создать первый экземпляр FooService. Стоит отметить вывод трассировки dotnet на Android.
Вместо этого, если вы сделаете это:
// If FooService has no dependencies
services.TryAddSingleton<IFooService>(sp => new FooService());
// Or if you need to retrieve some dependencies
services.TryAddSingleton<IFooService>(sp => new FooService(sp.GetService<IBar>()));
в этом случае,Microsoft.Extensions может просто вызвать метод lamdba/anonymous.,без необходимости использования системы. отражение.
Мы улучшили все dotnet/maui и использовали запрещенный анализатор, чтобы никто случайно не использовал более медленную перегрузку TryAddSingleton().
Дополнительные сведения об этом улучшении см. в dotnet/maui#5290.
▌Загружать ConfigurationManager лениво
ConfigurationManager не используется многими мобильными приложениями,А создать его очень дорого (например!,Около 7,59мс на андроиде)
В .NET В MAUI ConfigurationManager создается по умолчанию при запуске. Мы можем использовать Lazy, чтобы отложить его создание, поэтому он не будет создан, если не будет соответствующего запроса.
Дополнительные сведения об этом улучшении см. в dotnet/maui#5348.
▌по умолчаниюVerifyDependencyInjectionOpenGenericServiceTrimmability
Пример .NET Podcast занял 4–7 мс:
Microsoft.Extensions.DependencyInjection.ServiceLookup.CallsiteFactory.ValidateTrimmingAnnotations()
Атрибут MSBuild $(verifydependentinjectionopengenericservicetrimability) запускает этот метод. Этот переключатель функции гарантирует правильное использование динамически доступных элементов для включения универсальных типов при внедрении зависимостей.
В основах .NET В SDK при публикации =true, переключатель будет включен. Однако приложение Android не устанавливает публикацию в версии отладки. =true, поэтому разработчик пропустил эту проверку.
Напротив,В опубликованном приложении,Мы не хотим оплачивать стоимость этой проверки. Поэтому этот переключатель функции должен быть отключен в версии Release.
Дополнительные сведения об этом улучшении см. в разделах xamarin-android#6727 и xamarin-macios#14130.
▌Улучшить встроенный файл конфигурации AOT.
Среда выполнения Mono имеет отчет о времени JIT для каждого метода (см. нашу документацию), например:
Total(ms) | Self(ms) | Method
3.51 | 3.51 | Microsoft.Maui.Layouts.GridLayoutManager/GridStructure:.ctor (Microsoft.Maui.IGridLayout,double,double)
1.88 | 1.88 | Microsoft.Maui.Controls.Xaml.AppThemeBindingExtension/<>c__DisplayClass20_0:<Microsoft.Maui.Controls.Xaml.IMarkupExtension<Microsoft.Maui.Controls.BindingBase>.ProvideValue>g__minforetriever|0 ()
1.66 | 1.66 | Microsoft.Maui.Controls.Xaml.OnIdiomExtension/<>c__DisplayClass32_0:<ProvideValue>g__minforetriever|0 ()
1.54 | 1.54 | Microsoft.Maui.Converters.ThicknessTypeConverter:ConvertFrom (System.ComponentModel.ITypeDescriptorContext,System.Globalization.CultureInfo,object)
Это профиль с использованием Profiled Версия AOT находится в стадии разработки в .NET. Лучшие варианты выбора времени для подкастов. Кажется, это именно то, чего хотят разработчики в . net MAUIприложение Обычно используется вapi。
Чтобы гарантировать наличие этих методов в файле конфигурации AOT, мы используем эти API в dotnet/maui.
_=new Microsoft.Maui.Layouts.GridLayoutManager(new Grid()).Measure(100, 100);
<SolidColorBrush x:Key="ProfiledAot_AppThemeBinding_Color" Color="{AppThemeBinding Default=Black}"/>
<CollectionView x:Key="ProfiledAot_CollectionView_OnIdiom_Thickness" Margin="{OnIdiom Default=1,1,1,1}" />
Вызов этих методов в этом тестовом приложении гарантирует, что они находятся во встроенном .net-файле MAUI AOT.
После этого изменения мы рассмотрели обновленный JIT-отчет:
Total (ms) | Self (ms) | Method
2.61 | 2.61 | string:SplitInternal (string,string[],int,System.StringSplitOptions)
1.57 | 1.57 | System.Number:NumberToString (System.Text.ValueStringBuilder&,System.Number/NumberBuffer&,char,int,System.Globalization.NumberFormatInfo)
1.52 | 1.52 | System.Number:TryParseInt32IntegerStyle (System.ReadOnlySpan`1<char>,System.Globalization.NumberStyles,System.Globalization.NumberFormatInfo,int&)
Это привело к дальнейшим дополнениям:
var split = "foo;bar".Split(';');
var x = int.Parse("999");
x.ToString();
Мы внесли аналогичные изменения в Color.Parse() и Connectivity.NETworkAccess. Информация об устройстве. Идиома, AppInfo. .СЕТЬ MAUIприложениеследует часто использовать вrequestdtheme。
Подробные сведения об этих улучшениях см. в dotnet/maui#5559, dotnet/maui#5682 и dotnet/maui#6834.
если ты хочешь В .NET Чтобы записать собственный профиль AOT в 6, вы можете попробовать наш экспериментальный пакет Mono.Profiler.Android. Мы работаем над полной поддержкой регистрации пользовательских профилей в будущей версии .NET.
▌Включить отложенную загрузку изображений AOT.
до,Среда выполнения Mono загрузит все изображения AOT при запуске.,чтобы убедиться, что MVID размещенной сборки .NET (например, Foo.dll) соответствует образу AOT (libFoo.dll.so). В большинстве приложений .NET,Некоторые изображения AOT, возможно, не потребуется загружать позже.
В Mono появился новый параметр - aot-lazy-assembly-load или mono_opt_aot_lazy_assembly_load, рабочие нагрузки Android могут выбирать. Мы обнаружили, что это улучшило время запуска нового проекта maui на Pixel 6 Pro примерно на 25 мс.
По умолчанию это включено, но при желании вы можете установить его у себя. Отключите этот параметр в csproj через:
<AndroidAotEnableLazyLoad>false</AndroidAotEnableLazyLoad>
Подробные сведения об этих улучшениях см. в dotnet/runtime#67024 и xamarin-android #6940.
▌Удалите неиспользуемые объекты кодировки в System.Uri.
Результаты трассировки dotnet из приложения MAUI, показывающие, что загрузка системы при первой кодировке UTF32иLatin1 заняла около 7 мс. Использование Uri API:
namespace System
{
internal static class UriHelper
{
internal static readonly Encoding s_noFallbackCharUTF8 = Encoding.GetEncoding(
Encoding.UTF8.CodePage, new EncoderReplacementFallback(""), new DecoderReplacementFallback(""));
Это поле было случайно оставлено на месте. Любое использование System.Uri можно улучшить, просто удалив поле s_noFallbackCharUTF8. или связанный API. netприложение的запускать。
Дополнительные сведения об этом улучшении см. в dotnet/runtime#65326.