.NET MAUI 性能提升

點擊藍字

關注我們

作者:Jonathan Peppers

翻譯:Yijing Sun

校稿:Amy Peng

排版:Rani Sun

精彩預告

*本文干貨滿滿,預計閱讀時間32分鐘,建議收藏保存。

.NET多平臺應用程序UI (MAUI)將android、iOS、macOS和Windows API統一為一個API,這樣你就可以編寫一個應用程序在許多平臺上本機運行。我們專注于提高您的日常生產力以及您的應用程序的性能。我們認為,開發人員生產率的提高不應該以應用程序性能為代價。

應用程序的大小也是如此——在一個空白的.NET MAUI應用程序中存在什么開銷?當我們開始優化.NET MAUI時,很明顯iOS需要做一些工作來改善應用程序的大小,而android則缺乏啟動性能。

一個dotnet new maui項目的iOS應用程序最初大約是18MB。同樣,在之前的預覽中.NET MAUI在android上的啟動時間也不是很理想:

應用程序

框架

啟動時間(ms)

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

這是在Pixel 5設備上平均運行10次得到的結果。有關這些數字是如何獲得的,請參閱我們的maui-profiling文件。

我們的目標是讓.NET MAUI比它的前身Xamarin更快。很明顯,我們在.NET MAUI本身也有一些工作要做。dotnet new android 模板的發布速度已經超過Xamarin.Android,主要是因為.NET 6中新的BCL和Mono運行時。

新的.NET maui模板還沒有使用Shell導航模式,但是計劃將其作為.NET maui的默認導航模式。當我們采用這個更改時,我們知道會對模板中的性能造成影響。

幾個不同團隊的合作才有了今天的成就。我們改進了Microsoft.Extensions ,依賴注入的使用,AOT編譯,Java互操作,XAML,.NET MAUI代碼,等等方面。

塵埃落定后,我們達到了一個更好的階段:

應用程序

框架

啟動時間(ms)

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模板,沒有使用Shell。

下面的細節,享受吧!

.NET Podcast

https://github.com/microsoft/dotnet-podcasts

maui-profiling

https://github.com/jonathanpeppers/maui-profiling

Shell

https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/shell/

.NET Podcast App (Shell)

https://github.com/microsoft/dotnet-podcasts

主要內容

啟動性能的改進

  • 在移動設備上進行分析

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#profiling-on-mobile

  • 測量隨著時間的推移

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#measuring-over-time

  • Profiled AOT

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#profiled-aot

  • 單文件程序集存儲器

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#single-file-assembly-stores

  • Spanify.RegisterNativeMembers

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#spanify-registernativemembers

  • System.Reflection.Emit和構造函數

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#systemreflectionemit-and-constructors

  • System.Reflection.Emit和方法

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#systemreflectionemit-and-methods

  • 更新的Java.Interop APIs

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#multi-dimensional-java-arrays

  • 多維Java數組

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#multi-dimensional-java-arrays

  • 為android圖像使用Glide

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#multi-dimensional-java-arrays

  • 減少Java互操作調用

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#reduce-java-interop-calls

  • 將android XML移植到Java

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#port-android-xml-to-java

  • 刪除Microsoft.Extensions.Hosting

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-microsoftextensionshosting

  • 在啟動時減少Shell初始化

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#less-shell-initialization-on-startup

  • 字體不應該使用臨時文件

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#fonts-should-not-use-temporary-files

  • 編譯時在平臺上計算

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#compute-onplatform-at-compile-time

  • 在XAML中使用編譯轉換器

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#use-compiled-converters-in-xaml

  • 優化顏色解析

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#optimize-color-parsing

  • 不要使用區域性識別的字符串比較

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#dont-use-culture-aware-string-comparisons

  • 懶惰地創建日志

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#create-loggers-lazily

  • 使用工廠方法進行依賴注入

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#use-factory-methods-for-dependency-injection

  • 懶惰地負載ConfigurationManager

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#create-loggers-lazily

  • 默認VerifyDependencyInjectionOpenGenericServiceTrimmability

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#default-verifydependencyinjectionopengenericservicetrimmability

  • 改進內置AOT配置文件

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#improve-the-built-in-aot-profile

  • 啟用AOT圖像的延遲加載

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#enable-lazy-loading-of-aot-images

  • 刪除System.Uri中未使用的編碼對象

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-unused-encoding-object-in-systemuri

應用程序大小的改進

  • 修復默認的MauiImage大小

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#fix-defaults-for-mauiimage-sizes

  • 刪除Application.Properties 和DataContractSerializer

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-applicationproperties-and-datacontractserializer

  • 修剪未使用的HTTP實現

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#trim-unused-http-implementations

.NET Podcast?示例中的改進(https://github.com/microsoft/dotnet-podcasts)

  • 刪除Microsoft.Extensions.Http用法

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-microsoftextensionshttp-usage

  • 刪除Newtonsoft.Json使用

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-newtonsoftjson-usage

  • 在后臺運行第一個網絡請求

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#run-first-network-request-in-background

實驗性或高級選項

  • 修剪Resource.designer.cs

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#trimming-resourcedesignercs

  • R8 Java代碼收縮器

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#r8-java-code-shrinker

  • AOT一切

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#aot-everything

  • AOT和LLVM

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#aot-and-llvm

  • 記錄自定義AOT配置文件

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#record-a-custom-aot-profile

啟動性能的改進

在移動設備上進行分析

我必須提到移動平臺上可用的.NET診斷工具,因為它是我們使.NET MAUI更快的第0步。

分析.NET 6 android應用程序需要使用一個叫做?dotnet-dsrouter?的工具。該工具使dotnet跟蹤連接到一個運行的移動應用程序在 android, iOS 等。這可能是我們用來分析 .NET MAUI 的最有影響力的工具。

要開始使用dotnet trace和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 trace輸出時,你會注意到連接:

Press <Enter> or <Ctrl+C> to exit...812  (KB)

在您的應用程序完全啟動后,只需按下enter鍵就可以得到一個保存在當前目錄的*.speedscope。你可以在https://speedscope.app上打開這個文件,深入了解每個方法在應用程序啟動期間所花費的時間:

1dafd69a51f8ec64f3f928b3a898d41b.png

在android應用程序中使用dotnet跟蹤的更多細節,請參閱我們的文檔。我建議在android設備上分析Release版本,以獲得應用在現實世界中的最佳表現。

dotnet-dsrouter

https://docs.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-dsrouter

我們的文檔

https://github.com/xamarin/xamarin-android/blob/main/Documentation/guides/tracing.md

測量隨著時間的推移

我們在.NET基礎團隊的朋友建立了一個管道來跟蹤.NET MAUI性能場景,例如:

  • 包大小

  • 磁盤大小(未壓縮)

  • 單個文件分類

  • 應用程序啟動

隨著時間的推移,這使我們能夠看到改進或回歸的影響,看到dotnet/maui回購的每個提交的數字。我們還可以確定這種差異是否是由xamarin-android、xamarin-macios或dotnet/runtime中的變化引起的。

例如,在物理Pixel 4a設備上運行的dotnet new maui模板的啟動時間(以毫秒為單位)圖:

733230e221099ace6e718ef1840e78b7.png

注意,Pixel 4a比Pixel 5要慢得多。

我們可以精確地指出在dotnet/maui中發生的回歸和改進。這對于追蹤我們的目標是非常有用的。

同樣地,我們可以在相同的Pixel 4a設備上看到.NET Podcast應用隨著時間的推移所取得的進展:

349fdff314a09f2798aaf75852007e58.png

這張圖表是我們真正關注的焦點,因為它是一款“真正的應用”,接近于開發者在自己的手機應用中看到的內容。

至于應用程序大小,它是一個更穩定的數字——當情況變得更糟或更好時,它很容易歸零:

03ef0ab357124ca092c4d2d15f1a6d48.png

請參閱dotnet-podcasts#58,?Android x# 520和dotnet/maui#6419了解這些改進的詳細信息。

dotnet-podcasts#58

https://github.com/microsoft/dotnet-podcasts

Android x# 520

https://github.com/xamarin/AndroidX/pull/520

dotnet/maui#6419

https://github.com/dotnet/maui/pull/6419

異形AOT

在我們對.NET MAUI的初始性能測試中,我們看到了JIT(及時)和AOT(提前)編譯的代碼是如何執行的:

應用

JIT 時間(ms)

AOT 時間(ms)

dotnet 新maui

1078.0ms

683.9ms

每次調用c#方法時都會發生JIT處理,這會隱式地影響移動應用程序的啟動性能。

另一個問題是AOT導致的應用程序大小增加。每個.NET程序集都會在最終應用中添加一個android本地庫。為了更好地利用這兩個世界,啟動跟蹤或分析AOT是Xamarin.Android當前的一個特性。這是一種AOT應用程序啟動路徑的機制,它顯著提高了啟動時間,而只增加了適度的應用程序大小。

在.NET 6版本中,這是完全有意義的默認選項。在過去,使用Xamarin.Android進行任何類型的AOT都需要Android NDK(下載多個gb)。我們在沒有安裝android NDK的情況下構建了AOT應用程序,使其成為可能。

我們為 dotnet new android, maui,和maui-blazor模板的內置配置文件,使大多數應用程序受益。如果你想在.NET 6中記錄一個自定義配置文件,你可以試試我們的實驗性的Mono.Profiler. Android包。我們正在努力在未來的.NET版本中完全支持記錄自定義概要文件。

查看xamarin-Android#6547和dotnet/maui#4859了解這個改進的細節。

啟動跟蹤或分析AOT

https://devblogs.microsoft.com/xamarin/faster-startup-times-with-startup-tracing-on-android/

Mono.Profiler. Android

https://github.com/jonathanpeppers/Mono.Profiler.Android

xamarin-Android#6547

https://github.com/xamarin/xamarin-android/pull/6547

dotnet/maui#4859

https://github.com/dotnet/maui/pull/4859

單文件程序集存儲器

之前,如果你在你最喜歡的zip文件實用程序中查看Release android .apk內容,你可以看到.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程序集的成本。這是在android工作負載中用C/ c++實現的,使用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程序集的應用程序產生了明顯的影響。

如果你需要檢查編譯過的android應用程序中這些程序集的IL,我們創建了一個程序集存儲讀取器工具來“解包”這些文件。

另一個選擇是在構建應用程序時禁用這些設置:

dotnet build -c Release -p:AndroidUseAssemblyStore=false -p:Android EnableAssemblyCompression=false

這樣你就可以用你喜歡的壓縮工具解壓生成的.apk文件,并使用ILSpy這樣的工具來檢查.NET程序集。這是一個很好的方法來診斷修剪器/鏈接器問題。

查看xamarin-android#6311了解關于這個改進的詳細信息。

mmap系統調用

https://man7.org/linux/man-pages/man2/mmap.2.html

mmap

https://man7.org/linux/man-pages/man2/mmap.2.html

程序集存儲讀取器

https://github.com/xamarin/xamarin-android/tree/main/tools/assembly-store-reader

ILSpy

https://github.com/icsharpcode/ILSpy

xamarin-android#6311

https://github.com/xamarin/xamarin-android/pull/6311

Spanify RegisterNativeMembers

當用Java創建c#對象時,會調用一個小型的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#代碼中被重寫。對于在c#中重寫的每個Java方法,您都會得到一個這樣的方法。

當實際的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運行時,并調用c#中的OnCreate()方法。

拆分\n和:-分隔的方法列表的代碼是在Xamarin早期使用string.Split()編寫的。可以說,Span在那時還不存在,但我們現在可以使用它!這提高了任何繼承Java類的c#類的成本,因此這是一個比.NET MAUI更廣泛的改進。

你可能會問,“為什么要使用字符串呢?”使用Java數組似乎比分隔字符串對性能的影響更大。在我們的測試中,調用JNI來獲取Java數組元素,性能比字符串差。Split和Span的新用法。對于如何在未來的.NET版本中重新構建它,我們有一些想法。

除了.NET 6之外,針對當前客戶Xamarin. Android的最新版本也附帶了這一更改。

查看xamarin-android#6708了解關于此改進的詳細信息。

Java本機接口(JNI)

https://en.wikipedia.org/wiki/Java_Native_Interface

Span

https://docs.microsoft.com/en-us/archive/msdn-magazine/2018/january/csharp-all-about-span-exploring-a-new-net-mainstay

xamarin-android#6708

https://github.com/xamarin/xamarin-android/pull/6708

System.Reflection.Emit和構造函數

在使用Xamarin的早期,我們有一個從Java調用c#構造函數的有點復雜的方法。

首先,我們有一些在啟動時發生的反射調用:

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用法,并在System.Reflection.ConstructorInfo中傳遞一個cinfo實例:

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子類的句柄,而對象[]是該特定c#構造函數的任何參數。emit對于在啟動時第一次使用它以及以后的每次調用都有很大的成本。

經過仔細的審查,我們可以將handle字段設置為內部的,并將此代碼簡化為:

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上都是有效的。構造函數內部的任何Java互操作(比如調用類上的其他Java方法)以及調用任何基本Java構造函數都需要Handle。

新代碼顯著改進了從Java調用的任何c#構造函數,因此這個特殊的更改改進的不僅僅是.NET MAUI。除了.NET 6之外,針對當前客戶Xamarin. android的最新版本也附帶了這一更改。

查看xamarin-android#6766了解這個改進的詳細信息。

xamarin-android#6766

https://github.com/xamarin/xamarin-android/pull/6766

System.Reflection.Emit和方法

當你在c#中重寫一個Java方法時,比如:

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次為一個dotnet 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);

這樣我們就可以列出所有使用過的dotnet 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 assemblycase 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 methodsinternal 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了解這個改進的詳細信息。

xamarin-android#6657

https://github.com/xamarin/xamarin-android/pull/6657

xamarin- android #6707

https://github.com/xamarin/xamarin-android/pull/6707

更新的Java.Interop APIs

Java.Interop.dll中原始的Xamarin api是這樣的api:

  • JNIEnv.CallStaticObjectMethod

在Java中調用的“新方法”每次調用占用的內存更少:

  • JniEnvironment.StaticMethods.CallStaticObjectMethod

當在構建時為Java方法生成c#綁定時,默認使用更新/更快的方法—在Xamarin.Android中已經有一段時間了。以前,Java綁定項目可以將$(AndroidCodegenTarget)設置為XAJavaInterop1,它在每次調用中緩存和重用jmethodID實例。請參閱java.interop文檔獲取關于該特性的歷史記錄。

其他有問題的地方是有“手動”綁定的地方。這些往往也是經常使用的方法,所以值得修復這些!

一些改善這種情況的例子:

  • JNIEnv.FindClass()在xamarin-android#6805

  • JavaList 和 JavaList在?xamarin-android#6812

AndroidCodegenTarget

https://docs.microsoft.com/en-us/xamarin/android/deploy-test/building-apps/build-properties#androidcodegentarget

java.interop

https://github.com/xamarin/Java.Interop/commit/d9b43b52a2904e00b74b96c82a7c62c6a0c214ca

xamarin-android#6805

https://github.com/xamarin/xamarin-android/pull/6805

xamarin-android#6812

https://github.com/xamarin/xamarin-android/pull/6812

多維Java數組

當向Java來回傳遞c#數組時,中間步驟必須復制數組,以便適當的運行時能夠訪問它。這真的是一個開發者體驗的情況,因為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 arrayJNIEnv.DeleteLocalRef (native_items); // Delete our Java local reference}
}

JNIEnv.NewArray()訪問一個“類型映射”,以知道需要將哪個Java類用于數組的元素。

dotnet new maui項目使用的特定android API有問題:

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了解這個改進的詳細信息。

dotnet/maui#5654

https://github.com/dotnet/maui/pull/5654

xamarin-android#6870

https://github.com/xamarin/xamarin-android/pull/6870

為android圖像使用Glide

Glide是現代android應用程序推薦的圖片加載庫。谷歌文檔甚至推薦使用它,因為內置的android Bitmap類可能很難正確使用。glidex.forms是在Xamarin.Forms中使用Glide的原型。但我們將 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#中子類化以處理托管代碼中的完成。其結果是,來自web的圖像的性能應該比以前在Xamarin.Forms中得到的性能有了顯著提高。

詳見dotnet/maui#759和dotnet/maui#5198。

Glide

https://github.com/bumptech/glide

glidex.forms

https://github.com/jonathanpeppers/glidex

dotnet/maui#759

https://github.com/dotnet/maui/pull/759

dotnet/maui#5198

https://github.com/dotnet/maui/pull/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 void setFooAndBar(int foo, int bar)
{setFoo(foo);setBar(bar);

.NET MAUI視圖本質上是c#對象,有很多屬性需要在Java中以完全相同的方式設置。如果我們將這個概念應用到.NET MAUI中的每個android View中,我們可以創建一個~18參數的方法用于View創建。后續的屬性更改可以直接調用標準的android api。

對于非常簡單的.NET MAUI控件來說,這在性能上有了顯著的提高:

方法

平均

錯誤

標準差

0代

已分配

Border(Before)

323.2 μs

0.82 μs

323.2?

0.9766

5 KB

Border(After)

242.3 μs

1.34 μs

1.25 μs

0.9766

5 KB

CollectionView(Before)

354.6 μs

2.61 μs

2.31 μs

1.4648

6 KB

CollectionView(After)

258.3 μs

0.49 μs

0.43 μs

1.4648

6 KB

請參閱dotnet/maui#3372了解有關此改進的詳細信息。

dotnet/maui#3372

https://github.com/dotnet/maui/pull/3372

將android XML移植到Java

回顧android上的dotnet跟蹤輸出,我們可以看到合理的時間花費在:

20.32.ms mono.andorid!Andorid.Views.LayoutInflater.Inflate

回顧堆棧跟蹤,時間實際上花在了android/Java擴展布局上,而在.NET端沒有任何工作發生。

如果你看看編譯過的android .apk和res/layouts/bottomtablayout。在android Studio中,XML只是普通的XML。只有少數標識符被轉換為整數。這意味著android必須解析XML并通過Java的反射api創建Java對象——似乎我們不使用XML就可以獲得更快的性能?

通過標準的BenchmarkDotNet對比,我們發現在涉及互操作時,使用android布局的表現甚至比使用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"?>
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><FrameLayoutandroid: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.BottomNavigationViewandroid: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)

這使得我們在android上創建底部標簽導航時只能從c#切換到Java 4次。它還允許android操作系統跳過加載和解析.xml來“膨脹”Java對象。我們在dotnet/maui中執行了這個想法,在啟動時刪除所有LayoutInflater.Inflate()調用。

請參閱dotnet/maui#5424,?dotnet/maui#5493,和dotnet/maui#5528了解這些改進的詳細信息

dotnet/maui#5424

https://github.com/dotnet/maui/pull/5424

dotnet/maui#5493

https://github.com/dotnet/maui/pull/5493

dotnet/maui#5528

https://github.com/dotnet/maui/pull/5528

刪除Microsoft.Extensions.Hosting

hosting提供了一個.NET通用主機,用于在.NET應用程序中管理依賴注入、日志記錄、配置和應用生命周期。這對啟動時間有影響,似乎不適合移動應用程序。

從.NET MAUI中移除Microsoft.Extensions.Hosting使用是有意義的。. net MAUI沒有試圖與“通用主機”互操作來構建DI容器,而是有自己的簡單實現,它針對移動啟動進行了優化。此外,. net MAUI默認不再添加日志記錄提供程序。

通過這一改變,我們看到dotnet new maui android應用程序的啟動時間減少了5-10%。在iOS上,它減少了相同應用程序的大小,從19.2 MB => 18.0 MB。

詳見dotnet/maui#4505和dotnet/maui#4545。

.NET通用主機

https://docs.microsoft.com/en-us/dotnet/core/extensions/generic-host

dotnet/maui#4505

https://github.com/dotnet/maui/pull/4505

dotnet/maui#4545

https://github.com/dotnet/maui/pull/4545

在啟動時減少Shell初始化

Xamarin. Forms Shell是跨平臺應用程序導航的一種模式。這個模式是在.NET MAUI中提出的,它被推薦作為構建應用程序的默認方式。

當我們發現在啟動時使用Shell的成本(對于Xamarin和Xamarin.form和.NET MAUI),我們找到了幾個可以優化的地方:

  • 不要在啟動時解析路由——要等到一個需要它們的導航發生。

  • 如果沒有為導航提供查詢字符串,則只需跳過處理查詢字符串的代碼。這將刪除過度使用System.Reflection的代碼路徑。

  • 如果頁面沒有可見的BottomNavigationView,那么不要設置菜單項或任何外觀元素。

請參閱dotnet/maui#5262了解此改進的詳細信息。

Xamarin. Forms Shell

https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/shell/

dotnet/maui#5262

https://github.com/dotnet/maui/pull/5262

字體不應該使用臨時文件

大量的時間花在.NET MAUI應用程序加載字體上:

32.19ms Microsoft.Maui!Microsoft.Maui.FontManager.CreateTypeface(System.ValueTuple`3<string, Microsoft.Maui.FontWeight, bool>)

檢查代碼時,它所做的工作比需要的更多:

  1. 將androidAsset文件保存到臨時文件夾。

  2. 使用android API, Typeface.CreateFromFile()來加載文件。

我們實際上可以直接使用Typeface.CreateFromAsset() android API,根本不用臨時文件。

請參閱dotnet/maui#4933了解有關此改進的詳細信息。

dotnet/maui#4933

https://github.com/dotnet/maui/pull/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。

dotnet/maui#4829

https://github.com/dotnet/maui/pull/4829

dotnet/maui#5611

https://github.com/dotnet/maui/pull/5611

在XAML中使用編譯轉換器

以下類型現在在XAML編譯時轉換,而不是在運行時:

  • 顏色:dotnet /maui# 4687

    https://github.com/dotnet/maui/pull/4687

  • 角半徑:?dotnet / maui # 5192

    https://github.com/dotnet/maui/pull/5192

  • 字形大小:dotnet / maui # 5338

    https://github.com/dotnet/maui/pull/5338

  • 網格長度, 行定義, 列定義:dotnet/maui#5489

    https://github.com/dotnet/maui/pull/5489

這導致從.xaml文件生成更好/更快的IL。

優化顏色解析

Microsoft.Maui.Graphics.Color.Parse()的原始代碼可以重寫,以更好地使用Span并避免字符串分配。

方法

平均

錯誤

標準差

0代

已分配

Parse (之前)

99.13 ns

0.281 ns

0.235 ns

0.0267

168 B

Parse (之后)

52.54 ns

0.292 ns

0.259 ns

0.0051

32 B

能夠在ReadonlySpan<char>dotnet/csharplang#1881上使用switch語句,將在未來的.NET版本中進一步改善這種情況。

看到dotnet / Microsoft.Maui.Graphics # 343和dotnet / Microsoft.Maui.Graphics # 345關于這個改進的細節。

dotnet/csharplang#1881

https://github.com/dotnet/csharplang/issues/1881

dotnet / Microsoft.Maui.Graphics # 343

https://github.com/dotnet/Microsoft.Maui.Graphics/pull/343

dotnet / Microsoft.Maui.Graphics # 345

https://github.com/dotnet/Microsoft.Maui.Graphics/pull/345

不要使用區域性識別的字符串比較

回顧一個新的naui項目的dotnet跟蹤輸出,可以看到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"))
{// do something
}

如果在整個應用程序中執行,System.Globalization.CultureInfo.CurrentCulture可以避免被調用,并且可以稍微提高If語句的總體速度。

為了解決整個dotnet/maui回購的這種情況,我們引入了代碼分析規則來捕捉這些:

dotnet_diagnostic.CA1307.severity = error
dotnet_diagnostic.CA1309.severity?=?error

請參閱dotnet/maui#4988了解有關改進的詳細信息。

dotnet/maui#4988

https://github.com/dotnet/maui/pull/4988

懶惰地創建日志

ConfigureFonts() API在啟動時花費了一些時間來做一些可以延遲到以后的工作。我們還可以改進Microsoft.Extensions中日志基礎設施的一般用法。

我們所做的一些改進如下:

  • 推遲創建“記錄器”類,直到需要它們時再創建。

  • 內置的日志記錄基礎設施在默認情況下是禁用的,必須顯式啟用。

  • 延遲調用android的EmbeddedFontLoader中的Path.GetTempPath(),直到需要它。

  • 不要使用ILoggerFactory創建通用記錄器。而是直接獲取ILogger服務,這樣它就被緩存了。

請參閱dotnet/maui#5103了解有關此改進的詳細信息。

dotnet/maui#5103

https://github.com/dotnet/maui/pull/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/匿名方法,而不需要系統。反射。

我們在所有的dotnet/maui上進行了改進,并使用了bannedapianalyzer,這樣就不會有人意外地使用TryAddSingleton()更慢的重載。

請參閱dotnet/maui#5290了解有關此改進的詳細信息。

bannedapianalyzer

https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md

dotnet/maui#5290

https://github.com/dotnet/maui/pull/5290

默認VerifyDependencyInjectionOpenGenericServiceTrimmability

.NET Podcast樣本花費了4-7ms的時間:

Microsoft.Extensions.DependencyInjection.ServiceLookup.CallsiteFactory.ValidateTrimmingAnnotations()

MSBuild屬性$(verifydependencyinjectionopengenericservicetrimability)觸發該方法運行。這個特性開關確保dynamallyaccessedmembers被正確地應用于打開依賴注入中的泛型類型。

在基礎.NET SDK中,當publishtrim =true時,該開關將被啟用。然而,android應用程序在Debug版本中并沒有設置publishtrim =true,所以開發者錯過了這個驗證。

相反,在已發布的應用程序中,我們不想支付這種驗證的成本。所以這個特性開關應該在Release版本中關閉。

查看xamarin-android#6727和xamarin-macios#14130了解關于這個改進的詳細信息。

.NET Podcast

https://github.com/dotnet/runtime/pull/65326

xamarin-android#6727

https://github.com/xamarin/xamarin-android/pull/6727

xamarin-macios#14130

https://github.com/xamarin/xamarin-macios/pull/14130

懶惰地負載ConfigurationManager

configurationmanager并沒有被許多移動應用程序使用,而且創建一個是非常昂貴的!(例如,在android上約為7.59ms)

在.NET MAUI中,一個ConfigurationManager在啟動時默認被創建,我們可以使用Lazy延遲它的創建,所以它將不會被創建,除非請求。

請參閱dotnet/maui#5348了解有關此改進的詳細信息。

dotnet/maui#5348

https://github.com/dotnet/maui/pull/5348

改進內置AOT配置文件

Mono運行時有一個關于每個方法的JIT時間的報告(參見我們的文檔),例如:

Total(ms) | Self(ms) | Method3.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 Podcast示例中的頂級jit時間選擇。這些似乎是開發人員希望在. net MAUI應用程序中使用的常用api。

為了確保這些方法在AOT配置文件中,我們在dotnet/maui中使用了這些api

_ = 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報告:

_ = 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}"?/>

這導致了進一步的補充:

var split = "foo;bar".Split(';');
var x = int.Parse("999");
x.ToString();

我們對Color.Parse()、Connectivity做了類似的修改.NETworkAccess DeviceInfo。成語,AppInfo。.NET MAUI應用程序中應該經常使用的requestdtheme。

請參閱dotnet/maui#5559,?dotnet/maui#5682,和dotnet/maui#6834了解這些改進的詳細信息。

如果你想在.NET 6中記錄一個自定義的AOT配置文件,你可以嘗試我們的實驗包Mono.Profiler.Android。我們正在努力在未來的.NET版本中完全支持記錄自定義概要文件。

參見我們的文檔

https://github.com/xamarin/xamarin-android/blob/main/Documentation/guides/profiling.md#profiling-the-jit-compiler

.NET Podcast

https://github.com/microsoft/dotnet-podcasts

dotnet/maui#5559

https://github.com/dotnet/maui/pull/5559

dotnet/maui#5682

https://github.com/dotnet/maui/pull/5682

dotnet/maui#6834

https://github.com/dotnet/maui/pull/6834

Mono.Profiler.Android

https://github.com/jonathanpeppers/Mono.Profiler.Android

啟用AOT圖像的延遲加載

以前,Mono運行時將在啟動時加載所有AOT圖像,以驗證托管.NET程序集(例如Foo.dll)的MVID是否與AOT圖像(libFoo.dll.so)匹配。在大多數.NET應用程序中,一些AOT映像可能稍后才需要加載。

Mono中引入了一個新的——aot-lazy-assembly-load或mono_opt_aot_lazy_assembly_load設置,android工作負載可以選擇。我們發現這將dotnet new maui項目在Pixel 6 Pro上的啟動時間提高了約25ms。

這是默認啟用的,但如果需要,你可以在你的。csproj中通過以下方式禁用此設置:

<AndroidAotEnableLazyLoad>false</AndroidAotEnableLazyLoad>

查看dotnet/runtime#67024和xamarin-android #6940了解這些改進的詳細信息。

dotnet/runtime#67024

https://github.com/dotnet/runtime/pull/67024

xamarin-android #6940

https://github.com/xamarin/xamarin-android/pull/6940

刪除System.Uri中未使用的編碼對象

一個MAUI應用程序的dotnet跟蹤輸出,顯示大約7ms花費了加載UTF32和Latin1編碼的第一次系統。使用Uri api:

<AndroidAotEnableLazyLoad>false</AndroidAotEnableLazyLoad>

這個字段是不小心留在原地的。只需刪除s_noFallbackCharUTF8字段,就可以改進任何使用System.Uri 或相關的api的. net應用程序的啟動。

參見dotnet/runtime#65326了解有關此改進的詳細信息。

dotnet/runtime#65326

https://github.com/dotnet/runtime/pull/65326

應用程序大小的改進

修復默認的MauiImage大小

dotnet new maui模板顯示一個友好的"網絡機器人”的形象。這是通過使用一個.svg文件作為一個MauiImage和內容來實現的:

<svg width="419" height="519" viewBox="0 0 419 519" fill="none" xmlns="http://www.w3.org/2000/svg">
<!--?everything?else?-->

默認情況下,MauiImage使用.svg中的寬度和高度值作為圖像的“基礎大小”。回顧構建輸出,這些圖像被縮放為:

objReleasenet6.0-androidresizetizerrmipmap-xxxhdpiappiconfg.png = 1824x1824dotnet_bot.png?=?1676x2076

這對于android設備來說似乎有點太大了?我們可以簡單地在模板中指定%(BaseSize),它還提供了一個如何為這些圖像選擇合適大小的示例:

<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\appiconfg.svg" Color="#512BD4" BaseSize="128,128" /><!-- Images -->
<MauiImage Include="Resources\Images\*" />
<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208" />

這就產生了更合適的尺寸:

obj\Release\net6.0-android\resizetizer\r\mipmap-xxxhdpi\appiconfg.png = 512x512dotnet_bot.png?=?672x832

我們還可以修改.svg內容,但這可能不可取,這取決于圖形設計師如何在其他設計工具中使用該圖像。

在另一個例子中,一個3008×5340 .jpg圖像:

<MauiImage Include="Resources\Images\large.jpg" />

正在升級到21360×12032!設置Resize="false"將防止圖像被調整大小,但我們將此設置為非矢量圖像的默認選項。接下來,開發人員應該能夠依賴默認值,或者根據需要指定%(基本尺寸)和%(調整大小)。

這些改變改善了啟動性能和應用程序的大小。請參閱dotnet/maui#4759和dotnet/maui#6419了解這些改進的細節。

dotnet/maui#4759

https://github.com/dotnet/maui/pull/4759

dotnet/maui#6419

https://github.com/dotnet/maui/pull/6419

刪除Application.Properties 和DataContractSerializer

Xamarin.Forms 有一個 API,用于通過 Application.Properties 字典持久化鍵值對。這在內部使用了DataContractSerializer,這對于自包含和修剪的移動應用程序不是最佳選擇。來自BCL的System.Xml的部分可能相當大,我們不想在每個.NET MAUI應用程序中都為此付出代價。

簡單地刪除這個API和所有DataContractSerializer的使用,在android上可以提高約855KB,在iOS上提高約1MB。

請參閱dotnet/maui#4976了解有關此改進的詳細信息。

dotnet/maui#4976

https://github.com/dotnet/maui/pull/4976

修剪未使用的HTTP實現

System.NET.Http.UseNativeHttpHandler沒有適當地削減底層托管HTTP處理程序(SocketsHttpHandler)。默認情況下,androidMessageHandler和NSUrlSessionHandler被用來利用底層的android和iOS網絡棧。

通過修正這個問題,在任何.NET MAUI應用程序中都可以刪除更多的IL代碼。在一個例子中,一個使用HTTP的android應用程序能夠完全刪除幾個程序集:

  • Microsoft.Win32.Primitives.dll

  • System.Formats.Asn1.dll

  • System.IO.Compression.Brotli.dll

  • System.NET.NameResolution.dll

  • System.NET.NETworkInformation.dll

  • System.NET.Quic.dll

  • System.NET.Security.dll

  • System.NET.Sockets.dll

  • System.Runtime.InteropServices.RuntimeInformation.dll

  • System.Runtime.Numerics.dll

  • System.Security.Cryptography.Encoding.dll

  • System.Security.Cryptography.X509Certificates.dll

  • System.Threading.Channels.dll

查看dotnet/runtime#64852,?xamarin-android#6749,和xamarin-macios#14297關于這個改進的詳細信息。

dotnet/runtime#64852

https://github.com/dotnet/runtime/pull/64852

xamarin-android#6749

https://github.com/xamarin/xamarin-android/pull/6749

xamarin-macios#14297

https://github.com/xamarin/xamarin-macios/pull/14297

.NET Podcast示例中的改進

我們對樣本本身做了一些調整,其中更改被認為是“最佳實踐”。

刪除Microsoft.Extensions.Http用法

使用Microsoft.Extensions.Http對于移動應用程序來說太重了,并且在這種情況下沒有提供任何真正的價值。

因此,HttpClient不使用DI:

builder.Services.AddHttpClient<ShowsService>(client => 
{client.BaseAddress = new Uri(Config.APIUrl);
});// Then in the service ctor
public ShowsService(HttpClient httpClient, ListenLaterService listenLaterService)
{this.httpClient = httpClient;// ...
}

我們簡單地創建一個HttpClient來在服務中使用:

public ShowsService(ListenLaterService listenLaterService)
{this.httpClient = new HttpClient() { BaseAddress = new Uri(Config.APIUrl) };// ...
}

我們建議對應用程序需要交互的每個web服務使用一個單獨的HttpClient實例。

請參閱dotnet/runtime#66863和dotnet podcasts#44了解有關改進的詳細信息。

dotnet/runtime#66863

https://github.com/dotnet/runtime/issues/66863

dotnet podcasts#44

https://github.com/microsoft/dotnet-podcasts/pull/44

刪除Newtonsoft.Json使用

.NET Podcast?樣本使用了一個名為MonkeyCache的庫,它依賴于Newtonsoft.Json。這本身并不是一個問題,只是.NET MAUI + Blazor應用程序依賴于一些ASP.NET Core庫反過來依賴于System.Text.Json。這款應用實際上是為JSON解析庫“付了兩倍錢”,這對應用的大小產生了影響。

我們移植了MonkeyCache 2.0來使用System.Text。Json,不需要Newtonsoft。這將iOS上的應用大小從29.3MB減少到26.1MB!

參見monkey-cache#109和dotnet-podcasts#58了解有關改進的詳細信息。

.NET Podcast?

https://github.com/microsoft/dotnet-podcasts

MonkeyCache

https://github.com/jamesmontemagno/monkey-cache

monkey-cache#109

https://github.com/jamesmontemagno/monkey-cache/pull/109

dotnet-podcasts#58

https://github.com/microsoft/dotnet-podcasts/pull/58

在后臺運行第一個網絡請求

回顧dotnet跟蹤輸出,初始請求在ShowsService阻塞UI線程初始化連接.NETworkAccess Barrel.Current。得到,HttpClient。這項工作可以在后臺線程中完成-在這種情況下導致更快的啟動時間。在Task.Run()中封裝第一個調用,可以在一定程度上提高這個示例的啟動效率。

在Pixel 5a設備上平均運行10次:

Before
Average(ms): 843.7
Average(ms): 847.8
After
Average(ms): 817.2
Average(ms):?812.8

對于這種類型的更改,總是建議根據dotnet跟蹤或其他分析結果來做出決定,并度量更改前后的變化。

請參閱dotnet-podcasts#57了解有關此改進的詳細信息。

dotnet-podcasts#57

https://github.com/microsoft/dotnet-podcasts/pull/57

實驗性或高級選項

如果你想在android上進一步優化你的.NET MAUI應用程序,這里有一些高級或實驗性的特性,默認情況下不是啟用的。

修剪Resource.designer.cs

自從Xamarin誕生以來,android應用程序就包含了一個生成的Properties/Resource.designer.cs文件,用于訪問androidResource文件的整數標識符。這是R.java類的c# /托管版本,允許使用這些標識符作為普通的c#字段(有時是const),而無需與Java進行任何互操作。

在一個android Studio“庫”項目中,當你包含一個像res/drawable/foo.png這樣的文件時,你會得到一個像這樣的字段:

package com.yourlibrary;public class R
{public class drawable
{// The actual integer here maps to a table inside the final .apk filepublic final int foo = 1234;}
}

你可以使用這個值,例如,在ImageView中顯示這個圖像:

ImageView imageView = new ImageView(this);
imageView.setImageResource(R.drawable.foo);

當你構建com.yourlibrary.aar時, android的gradle插件實際上并沒有把這個類放在包中。相反,android應用程序實際上知道整數的值是多少。因此,R類是在android應用程序構建時生成的,為每個android庫生成一個R類。

Xamarin.Android采取了不同的方法,在運行時進行整數修復。用c#和MSBuild做這樣的事情真的沒有一個很好的先例嗎?例如,一個c# android庫可能有:

public class Resource
{public class Drawable{// The actual integer here is *not* finalpublic int foo = -1;}
}

然后主應用程序就會有如下代碼:

public class Resource
{public class Drawable{public Drawable()
{// Copy the value at runtimeglobal::MyLibrary.Resource.Drawable.foo = foo;}// The actual integer here *is* finalpublic const int foo = 1234;}
}

這種情況已經很好地運行了一段時間,但不幸的是,像androidX、Material、谷歌Play Services等谷歌的庫中的資源數量已經開始復合。例如,在dotnet/maui#2606中,啟動時設置了21497個字段!我們創建了一種方法來解決這個問題,但我們也有一個新的自定義修剪步驟來執行修復在構建時(在修剪期間)而不是在運行時。

<AndroidLinkResources>true</ AndroidLinkResources>

這將使你的版本版本替換案例如下:

ImageView imageView = new(this);
imageView.SetImageResource(Resource.Drawable.foo);

相反,直接內聯整數:

ImageView imageView = new(this);
imageView.SetImageResource(1234);?//?The?actual?integer?here?*is*?final

這個特性的一個已知問題是:

public partial class Styleable
{public static int[] ActionBarLayout = new int[] { 16842931 };
}

目前不支持替換int[]值,這使得我們不能默認啟用它。一些應用程序將能夠打開這個功能,dotnet新的maui模板,也許許多.NET maui android應用程序不會遇到這個限制。

在未來的.NET版本中,我們可能會默認啟用$(androidLinkResources),或者完全重新設計。

查看xamarin-android#5317,?xamarin-android#6696,和dotnet/maui#4912了解該功能的詳細信息。

dotnet/maui#2606

https://github.com/dotnet/maui/pull/2606

xamarin-android#5317

https://github.com/xamarin/xamarin-android/pull/5317

xamarin-android#6696

https://github.com/xamarin/xamarin-android/pull/6696

dotnet/maui#4912

https://github.com/dotnet/maui/pull/4912

R8 Java代碼收縮器

R8是全程序優化、收縮和縮小工具,將java字節代碼轉換為優化的dex代碼。R8使用Proguard keep規則格式為應用程序指定入口點。如您所料,許多應用程序需要額外的Proguard規則來保持工作。R8可能過于激進,并且刪除了Java反射所調用的一些東西,等等。我們還沒有一個很好的方法讓它成為所有.NET android應用程序的默認設置。

要選擇使用R8 for Release版本,請在你的.csproj中添加以下內容:

<!-- NOTE: not recommended for Debug builds! -->
<AndroidLinkTool?Condition="'$(Configuration)'?==?'Release'">r8</AndroidLinkTool>

如果啟動你的應用程序的Release構建在啟用后崩潰,檢查adb logcat輸出,看看哪里出了問題。

如果你看到java.lang. classnotfoundexception或java.lang。你可能需要添加一個ProguardConfiguration文件到你的項目中,比如:

<ItemGroup><ProguardConfiguration Include="proguard.cfg" />
</ItemGroup>-keep?class?com.thepackage.TheClassYouWantToPreserve?{?*;?<init>(...);?}

我們正在研究在未來的.NET版本中默認啟用R8的選項。

詳情請參閱我們的D8/R8文檔。

我們的D8/R8文檔

https://github.com/xamarin/xamarin-android/blob/main/Documentation/guides/D8andR8.md

AOT

Profiled AOT是默認的,因為它在應用程序大小和啟動性能之間給出了最好的權衡。如果應用程序的大小與你的應用程序無關,你可以考慮對所有.NET程序集使用AOT。

要選擇加入,在你的.csproj中添加以下Release配置:

<PropertyGroup Condition="'$(Configuration)' == 'Release'"><RunAOTCompilation>true</RunAOTCompilation><androidEnableProfiledAot>false</androidEnableProfiledAot>
</PropertyGroup>

這將減少在應用程序啟動期間發生的JIT編譯量,以及導航到后面的屏幕等。

AOT和LLVM

LLVM提供了一個獨立于源和目標的現代優化器,可以與Mono AOT Compiler輸出相結合。其結果是,應用的尺寸略大,發行構建時間更長,運行時性能更好。

要選擇將LLVM用于Release版本,請將以下內容添加到你的.csproj中:

<PropertyGroup Condition="'$(Configuration)' == 'Release'"><RunAOTCompilation>true</RunAOTCompilation><EnableLLVM>true</EnableLLVM>
</PropertyGroup>

此特性可以與Profiled AOT(或AOT-ing一切)結合使用。對比應用程序的前后,了解EnableLLVM對應用程序大小和啟動性能的影響。

目前,需要安裝一個android NDK來使用這個功能。如果我們能夠解決這個需求,EnableLLVM將成為未來.NET版本中的默認選項。

有關詳細信息,請參閱我們關于EnableLLVM的文檔。

LLVM

https://llvm.org/

EnableLLVM的文檔

https://docs.microsoft.com/en-us/xamarin/android/deploy-test/building-apps/build-properties

記錄自定義AOT配置文件

概要AOT默認使用我們在.NET MAUI和android工作負載中提供的“內置”概要文件,對大多數應用程序都很有用。為了獲得最佳的啟動性能,理想情況下應該記錄應用程序特定的配置文件。針對這種情況,我們有一個實驗性的Mono.Profiler.Android包。

記錄配置文件:

dotnet add package Mono.AotProfiler.android
dotnet build -t:BuildAndStartAotProfiling
# Wait until app launches, or you navigate to a screen
dotnet build -t:FinishAotProfiling

這將在你的項目目錄下產生一個custom.aprof。要在未來的構建中使用它:

<ItemGroup><androidAotProfile Include="custom.aprof" />
</ItemGroup>

我們正在努力在未來的.NET版本中完全支持記錄自定義概要文件。

Mono.Profiler.Android

https://github.com/jonathanpeppers/Mono.Profiler.Android

結論

我希望您喜歡我們的.NET MAUI性能論述。請嘗試.NET MAUI并且可以在http://dot.net/maui了解更多!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/285175.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/285175.shtml
英文地址,請注明出處:http://en.pswp.cn/news/285175.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

類與類之間的關系

橫向關系依賴 關聯 聚合 組合 判斷方法&#xff1a; 生命周期有關系&#xff1a;組合&#xff0c;聚合  聚合&#xff1a;包含多個相同的類  組合&#xff1a;定義的時候就要有  依賴&#xff1a;只要使用就必須要有  關聯&#xff1a;可有可無 縱向關系繼承 基類( 父類…

C語言試題155之有五個學生,每個學生有 3 門課的成績,從鍵盤輸入以上數據(包括學生號,姓名,三門課成績),計算出 平均成績,況原有的數據和計算出的平均分數存放在磁盤文件“stud“中

??個人主頁:個人主頁 ??系列專欄:C語言試題200例 ??推薦一款模擬面試、刷題神器?? 點擊跳轉進入網站 ?作者簡介:大家好,我是碼莎拉蒂,CSDN博客專家(全站排名Top 50),阿里云博客專家、51CTO博客專家、華為云享專家 1、題目 題目:有五個學生,每個學生有 3 門…

仿照支付寶賬單界面--listview分組顯示 用來做!發!財樹充值交易明細

QQ圖片20150430155638.png (151.65 KB, 下載次數: 32) 下載鏈接: http://pan.baidu.com/s/1kVMY1SV 密碼: i8ta

系統中常用的目錄

Linux系統中常用的目錄 目錄 /bin 存放二進制可執行文件(ls,cat,mkdir等)&#xff0c;常用命令一般都在這里。 /etc 存放系統管理和配置文件 /home 存放所有用戶文件的根目錄&#xff0c;是用戶主目錄的基點&#xff0c;比如用戶user的主目錄就是/home/user&#xff0…

C語言試題156之有兩個磁盤文件 A 和 B,各存放一行字母,要求把這兩個文件中的信息合并(按字母順序排列), 輸出到一個新文件 C 中。

??個人主頁:個人主頁 ??系列專欄:C語言試題200例 ??推薦一款模擬面試、刷題神器?? 點擊跳轉進入網站 ?作者簡介:大家好,我是碼莎拉蒂,CSDN博客專家(全站排名Top 50),阿里云博客專家、51CTO博客專家、華為云享專家 1、題目 題目:有兩個磁盤文件 A 和 B,各存…

【ArcGIS微課1000例】0002:創建漁網(Create fishnet)

本文講解ArcGIS軟件中漁網(fishnet)工具的原理,方法及使用技巧。 文章目錄 微課目標工具介紹實現過程微課目標 如下圖所示,影像為無人機航測生產的DOM,現在需要在ArcGIS平臺中進行DLG數據采集(數字化),由于測區較大,需要創建500*500的漁網,并對影像進行裁剪下發給多…

基于http協議的api接口對于客戶端的身份認證方式以及安全措施[轉]

基于http協議的api接口對于客戶端的身份認證方式以及安全措施 由于http是無狀態的&#xff0c;所以正常情況下在瀏覽器瀏覽網頁&#xff0c;服務器都是通過訪問者的cookie(cookie中存儲的jsessionid)來辨別客戶端的身份的&#xff0c;當客戶端進行登錄服務器也會將登錄信息存放…

使用 Scrutor 快速實現“裝飾者模式”

裝飾者模式介紹裝飾器模式&#xff08;Decorator Pattern&#xff09;是在不改變原類和使用繼承的情況下&#xff0c;動態地給一個對象添加一些額外的職責。它是通過創建一個包裝對象&#xff0c;也就是裝飾來包裹真實的對象。可以在如下使用場景中使用裝飾器模式&#xff1a;在…

各個 Android Gradle 插件版本所需的 Gradle 版本

下表列出了各個 Android Gradle 插件版本所需的 Gradle 版本。 要獲得最佳性能&#xff0c;您應該使用 Gradle 和插件這兩者的最新版本。 插件版本所需的 Gradle 版本1.0.0 - 1.1.32.2.1 - 2.31.2.0 - 1.3.12.2.1 - 2.91.5.02.2.1 - 2.132.0.0 - 2.1.22.10 - 2.132.1.3 - 2.2…

JS時間戳和時間互轉

https://www.cnblogs.com/nield-bky/p/6040853.html http://blog.csdn.net/csdn565973850/article/details/73838583 時間轉時間戳&#xff1a; javascript獲得時間戳的方法有四種&#xff0c;都是通過實例化時間對象 new Date() 來進一步獲取當前的時間戳1.var timestamp1 Da…

C語言試題157之從鍵盤輸入一個字符串,將小寫字母全部轉換成大寫字母,然后輸出到一個磁盤文件“test”中保存。 輸入的字符串以!結束

??個人主頁:個人主頁 ??系列專欄:C語言試題200例 ??推薦一款模擬面試、刷題神器?? 點擊跳轉進入網站 ?作者簡介:大家好,我是碼莎拉蒂,CSDN博客專家(全站排名Top 50),阿里云博客專家、51CTO博客專家、華為云享專家 1、題目 題目:從鍵盤輸入一個字符串,將小…

【ArcGIS微課1000例】0001:添加XY數據(Add XY data)生成shp

用過CASS的人都知道&#xff0c;野外數字測圖得到的點數據&#xff08;平面坐標&#xff09;可以直接在CASS中展點&#xff0c;進一步繪制地形圖。那么&#xff0c;帶有坐標的數據能不能在ArcGIS中實現點圖層的生成呢&#xff1f;答案是必須的&#xff01; 本文以氣象臺站Excel…

算法導論--廣度優先搜索和深度優先搜索

廣度優先搜索 在給定圖G(V,E)和一個特定的源頂點s的情況下&#xff0c;廣度優先搜索系統地探索G中的邊&#xff0c;以期“發現”可從s 到達的所有頂點&#xff0c;并計算s 到所有這些可達頂點之間的距離&#xff08;即最少的邊數&#xff09;。該搜索算法同時還能生成一棵根為s…

動手學 docker

背景動手學 docker最近&#xff0c;終于完成了 動手學 docker 系列的編寫。動手學 docker 是 動手學系列 的首個系列。如果反饋的效果不錯&#xff0c;后續還將推出 動手學 devops動手學 kubernetes動手學 istio 等系列。動手學系列 的構思來源于 李沐 老師的 動手學深度學習 。…

Linux零基礎入學之1-1課程介紹了解RHEL7安裝RHEL7

【本節內容】* 課程介紹* RHEL7了解* RHEL7.2的安裝* 實戰&#xff1a;組裝服務器【Linux介紹】服務器種類&#xff1a;刀片式、塔式&#xff08;機架式&#xff09;1U&#xff1a;4.45cm 三指寬 指服務器的高度貝爾實驗室 Unix 肯湯普森 & 丹尼斯里奇二人合作用…

【ArcGIS微課1000例】0003:按屬性選擇(Select by Attributes)

在使用ArcGIS時,可以使用 SQL 表達式基于要素屬性來選擇要素。 通過按屬性選擇 對話框可為 SQL 表達式定義一個或多個由屬性、運算符和計算構成的條件。給定圖層上生成的要素選擇內容滿足這些條件。 文章目錄 按屬性選擇工具1. 工具箱→按屬性選擇圖層2. 菜單欄→按屬性選擇3.…

[轉]Android 常見安全漏洞修復理論與實踐

前言 前段時間公司對應用在愛加密上進行了安全掃描&#xff0c;本文將基于愛加密的漏洞分析報告&#xff0c;針對部分內容&#xff0c;介紹理論修復實踐 最小化特權準則概念介紹 最小化特權準則&#xff0c;即指組件只能供自身應用調用&#xff0c;盡可能禁止其他應用訪問及…

C語言試題158之從鍵盤輸入一些字符,逐個把它們送到磁盤上去,直到輸入一個#為止。

??個人主頁:個人主頁 ??系列專欄:C語言試題200例 ??推薦一款模擬面試、刷題神器?? 點擊跳轉進入網站 ?作者簡介:大家好,我是碼莎拉蒂,CSDN博客專家(全站排名Top 50),阿里云博客專家、51CTO博客專家、華為云享專家 1、題目 題目:從鍵盤輸入一些字符,逐個把…

html5--3.2 input元素(1)

html5--3.2 input元素(1) 學習要點 input元素及其屬性input元素 用來設置表單中的內容項&#xff0c;比如輸入內容的文本框&#xff0c;按鈕等不僅可以布置在表單中&#xff0c;也可以在表單之外的元素使用input元素的屬性 type屬性&#xff1a;指定輸入內容的類型&#xff0c;…

css3 3D

● css3 3D旋轉 ○ rotateX() 方法&#xff0c;元素圍繞其 X 軸以給定的度數進行旋轉 ○ rotateY() 方法&#xff0c;元素圍繞其 Y 軸以給定的度數進行旋轉 ○ rotateZ(angle)&#xff0c;定義沿 Z 軸的 3D 旋轉。 ● 3D景深 ○ perspective(n)&#xff0c;景深&#xff0c;離屏…