NativeAOT ב-.NET MAUI 10 ל-iOS מקטין את גודל האפליקציה ב-50% ומאיץ את זמן ההפעלה. הפעלת PublishAot נכון, תיקון אזהרות ILLink IL2xxx, דפוסי Reflection לא-תואמים ובנצ'מרקים אמיתיים מפרויקט פרודקשן.
NativeAOT ב-.NET MAUI 10 ל-iOS הוא מודל הפרסום שבו הקומפיילר מבצע קומפילציה מלאה מראש (ahead-of-time) של כל קוד ה-managed לקוד מכונה נייטיבי, יחד עם trimming אגרסיבי של אסמבלים לא-מקושרים, ומפיק אפליקציה קטנה יותר בכ-50% וזמן הפעלה מהיר יותר בכ-50% מול מודל ה-Mono המסורתי. במאמר הזה אני מפרקת את הצינור המלא: מה ההבדל בין AOT ל-Trimming, איך להפעיל PublishAot נכון על iOS ו-Mac Catalyst, איך לפתור את אזהרות ה-IL2xxx שיצוצו לך ברגע שתלחצי publish, ואיך לדבג את מה שנשבר. את כל זה למדתי בדרך הקשה: PR אחד סירב לעלות ל-TestFlight במשך שבועיים בגלל אזהרת IL2075 אחת שהתעלמתי ממנה, וזה מה שאני רוצה לחסוך לך.
NativeAOT זמין ב-.NET MAUI 10 ל-iOS ול-Mac Catalyst בלבד (לא לאנדרואיד), ומחייב <MtouchLink>Full</MtouchLink> באופן קבוע.
שילוב של IsAotCompatible ו-PublishAot מפעיל מנתחי trimming ו-AOT שמייצרים אזהרות IL2xxx/IL3xxx בזמן build ו-publish.
גודל ה-IPA טיפוסי יורד מ-80–120MB במודל Mono ל-40–60MB ב-NativeAOT, וזמן ההפעלה הראשוני על iPhone 13 יורד מ-1.8s ל-0.9s בממוצע.
Reflection דינמי, Activator.CreateInstance על טיפוסים לא ידועים ו-System.Text.Json בלי source generator יישברו בזמן ריצה אם לא תסמני אותם עם DynamicDependency.
אי אפשר לצרף את ה-managed debugger ל-binary של NativeAOT. הדיבוג נעשה עם lldb על ה-native binary או דרך לוגים מובנים.
ל-build-ים מהירים ב-development השתמשי ב-Mono Interpreter (UseInterpreter=true), ושמרי את PublishAot ל-Release בלבד, אבל בלי להתנות אותו ב-Configuration.
מה ההבדל בין AOT ל-Trimming ב-.NET MAUI?
שני המושגים נשמעים דומים אבל הם עושים דברים שונים לחלוטין בצינור ה-build של MAUI. Trimming (מבוצע על ידי הכלי ILLink) הוא שלב סטטי שסורק את ה-IL של האפליקציה שלך ושל האסמבלים שהיא צורכת, ומזהה אילו טיפוסים, מתודות, שדות ואירועים אינם ניתנים לגישה סטטית מנקודת הכניסה. הכל שאינו ניתן לגישה — נמחק. התוצאה: אסמבלים דקים יותר, פחות מטא-דאטה, ופחות קוד לטעון בזמן ריצה. AOT לעומת זאת הוא שלב קומפילציה נוסף: הוא לוקח את ה-IL (שכבר עבר trimming) והופך אותו לקוד מכונה נייטיבי של ARM64 עוד לפני שהאפליקציה מגיעה למכשיר.
ב-Mono המסורתי על iOS, ה-JIT אסור לחלוטין (Apple לא מאפשרת קוד ניתן להרצה בזמן ריצה), אז MAUI תמיד עשה AOT דרך Mono. מה שחדש ב-NET 8/9/10 הוא NativeAOT: קומפיילר חדש (המחליף חלקית את Mono) שמייצר binaries נייטיביים ללא Mono VM כלל. זה מוריד את ה-runtime overhead, מקטין את ה-IPA באופן דרמטי, ומאלץ אותך לכתוב קוד AOT-safe. הצירוף של trimming מלא + NativeAOT הוא מה שנותן את שיפור ה-50%/50% שמייקרוסופט מפרסמת ב-.NET 9 Performance Improvements in .NET MAUI.
המפתח כאן: אי אפשר להפעיל NativeAOT בלי trimming מלא. הם צמד. אם באחד הפרויקטים שלך יש <MtouchLink>SdkOnly</MtouchLink> תישבר בזמן ה-publish עם שגיאת clang++ exited with code 1, כי NativeAOT דורש Full באופן מוחלט.
איך להפעיל NativeAOT ב-.NET MAUI 10 על iOS
ההגדרה ה-canonical לפרויקט MAUI 10 שרוצה NativeAOT ל-iOS ול-Mac Catalyst היא הבאה, וזו ההגדרה שאני מציבה בכל פרויקט חדש אצל הלקוחות שלי. שימי לב לדפוס התניית ה-PublishAot ב-TargetPlatformIdentifier ולא ב-Configuration. זו נקודה קריטית שהתיעוד הרשמי מדגיש.
<PropertyGroup>
<!-- Enable trimming + AOT analyzers on every platform (surfaces IL2xxx early) -->
<IsAotCompatible>true</IsAotCompatible>
<!-- Only iOS and Mac Catalyst actually run NativeAOT -->
<PublishAot Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">true</PublishAot>
<PublishAot Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">true</PublishAot>
<!-- Do NOT condition PublishAot on $(Configuration): trimming feature switches
must match across Debug and Release, or you get runtime inconsistencies. -->
</PropertyGroup>
כדי לפרסם את ה-IPA:
dotnet publish -f net10.0-ios \
-c Release \
-p:RuntimeIdentifier=ios-arm64 \
-p:ArchiveOnBuild=true \
-p:CodesignKey="Apple Distribution: Your Company (TEAMID)" \
-p:CodesignProvision="AppStore Provision"
כשאת מריצה dotnet publish בפעם הראשונה, את כמעט תמיד תראי ערמה של אזהרות. אל תתעלמי מהן. כפי שהתיעוד של Native AOT deployment on iOS and Mac Catalyst אומר במפורש: הדרך היחידה לוודא שהאפליקציה שלך עובדת נכון תחת NativeAOT היא לפרסם, לאמת שאין אזהרות trimming/AOT, ולבדוק על מכשיר אמיתי.
MtouchLink: Full מול SdkOnly ומול None
ה-MtouchLink קובע מה ה-linker (ILLink) יעשה עם האסמבלים בזמן ה-build של iOS. שלוש האפשרויות שלו הן:
מאפיין
MtouchLink=Full
MtouchLink=SdkOnly
MtouchLink=None
מה עובר trimming
SDK + קוד האפליקציה + כל התלויות
רק אסמבלים של SDK
שום דבר
נדרש ל-NativeAOT?
כן (חובה)
לא (יגרום ל-clang error)
לא (לא תואם)
גודל IPA טיפוסי
40–60MB
70–90MB
110–150MB
סיכון לשגיאות ריצה
גבוה אם יש Reflection
נמוך
מינימלי
זמן build
ארוך יותר ב-30–60%
ממוצע
הכי מהיר
מומלץ ל-
Release, App Store
QA/Beta builds
debugging בלבד
בפועל, ברוב הפרויקטים שאני עובדת עליהם היום ההגדרה היא MtouchLink=Full כברירת המחדל ל-Release, ו-SdkOnly ל-Beta/TestFlight אם ה-QA מבקש לבודד תקלות linker. אף פעם לא None ב-CI, כי הגודל האמיתי של האפליקציה לא מייצג את מה שהמשתמשים יורידו מה-App Store.
קטע חשוב שלוקח לרבים לגלות: MtouchLink הוא רק ל-iOS ול-Mac Catalyst. ב-Windows וב-Android, המקבילה שלו היא TrimMode ו-PublishTrimmed. אם את מעבירה קודנית פרויקט מ-Xamarin.Forms (ראי את מדריך ההגירה מ-Xamarin.Forms ל-.NET MAUI 10 שלי), הרי בפרויקטים ישנים עדיין נשארו הגדרות MtouchLink ב-iOS/ ולא הועברו. Upgrade Assistant לא תמיד מנקה אותן, בדקי ידנית.
איך לתקן אזהרות ILLink IL2xxx ו-IL3050
ברגע שתפעילי IsAotCompatible=true, הקומפיילר יתחיל להוציא אזהרות עם קידומות IL2xxx (trimming) ו-IL3xxx (AOT). לכל אזהרה יש תרגום ישיר לבעיה בקוד:
IL2026: קריאה למתודה שמסומנת [RequiresUnreferencedCode]. משמעות: היעד עלול לגשת דינמית לקוד ש-Trimming ימחק. פתרון: או להעביר את הקריאה למסלול AOT-safe, או לסמן את המתודה הקוראת גם היא ב-[RequiresUnreferencedCode].
IL2075: שימוש ב-GetType(string) על טיפוס שלא ניתן לחשב סטטית. פתרון: להעביר את הטיפוס במפורש כ-typeof(MyClass), או להוסיף [DynamicallyAccessedMembers] על הפרמטר.
IL2091: פרמטר טיפוס גנרי לא מסומן ב-[DynamicallyAccessedMembers]. פתרון: להוסיף את התכונה על ה-generic constraint.
IL3050: קריאה למתודה עם [RequiresDynamicCode]. זה השווה-ערך של IL2026 עבור AOT. תלוי בהקשר. הרבה APIs של System.Text.Json הישנים ו-Expression.Compile() נופלים לכאן.
הנה דוגמה קלאסית שאני רואה בכל migration מ-Xamarin. הקוד המקורי:
// Triggers IL2026 + IL3050 under NativeAOT
public T LoadConfig<T>(string path)
{
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<T>(json);
}
הפתרון: לעבור ל-System.Text.Json source generator. הוא מייצר את קוד ה-serialization בזמן compile, כך שאין reflection דינמי:
[JsonSerializable(typeof(AppConfig))]
[JsonSerializable(typeof(UserProfile))]
internal partial class AppJsonContext : JsonSerializerContext { }
public AppConfig LoadConfig(string path)
{
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize(json, AppJsonContext.Default.AppConfig)!;
}
דפוסי Reflection לא-תואמי-AOT ומה לעשות איתם
ה-footgun הגדול ביותר בהעברת אפליקציה קיימת ל-NativeAOT זה reflection. כל שימוש ב-Activator.CreateInstance(Type.GetType("Namespace.Foo")), Assembly.GetTypes(), או deserialization מבוססי-reflection ייכשל אחרי trimming, לא בזמן build, אלא ברגע שהקוד ירוץ על מכשיר.
הכלים העיקריים לפתרון:
[DynamicDependency]: מסמן על מתודה שהיא צריכה טיפוס/מתודה מסוימת גם אם הן לא נראות סטטית. שומר את הקוד מטרימינג.
TrimmerRootAssembly ב-.csproj: שומר על אסמבלי שלם. שימושי לתלויות של צד שלישי שאת לא יכולה לגעת בקוד שלהן.
ILLink descriptor XML: קובץ XML שמפרט מפורשות מה לשמור. אני משתמשת בו רק כמפלט אחרון.
Source generators: במקום reflection בזמן ריצה. Roslyn מייצר את הקוד בזמן compile, ואין שום דבר לטרם או לאבד.
דוגמה של TrimmerRootAssembly ב-.csproj:
<ItemGroup>
<!-- Third-party lib does dynamic type loading we can't refactor -->
<TrimmerRootAssembly Include="Acme.LegacyRuntime" />
</ItemGroup>
וב-DI: אם את משתמשת ב-Microsoft.Extensions.DependencyInjection הרגיל עם services.AddTransient<IFoo, Foo>(), את בסדר, כי הרישום סטטי לחלוטין. הבעיה מתחילה עם ספריות שמשתמשות ב-assembly scanning (Autofac, ה-Scrutor וכו'). אני ממליצה לעבור להזרקה מפורשת בפרויקטים שרוצים NativeAOT. זה יותר קוד — אבל את מרוויחה binary קטן וודאי.
איך להקטין את גודל אפליקציית .NET MAUI ל-iOS
מעבר ל-PublishAot ו-MtouchLink=Full, יש עוד כמה מנופים שאני תמיד מפעילה בפרויקטי פרודקשן. הרשימה למטה מסודרת לפי סדר יורד של אימפקט טיפוסי על גודל ה-IPA:
<OptimizePNGs>true</OptimizePNGs> ב-.csproj: לוחץ את כל ה-PNGs. חסך לי 8MB בפרויקט אחרון.
הפעלת EnableAssetPackDelivery: מחלקת asset-ים כבדים (מודלים, וידאו) ל-App Store on-demand resources. ה-IPA הבסיסי יורד דרסטית.
Deduplicate NuGet dependencies: הריצי dotnet list package --include-transitive ובדקי אם יש double-references של Newtonsoft.Json, Serilog וכו'. Trimming לא יכול לחסל כפילויות ברמת אסמבלי.
MtouchNoSymbolStrip=false: מוודא ש-symbols של native libs מוסרים. חוסך 5–10MB.
הסרת RIDs מיותרים: אם את לא תומכת ב-ios-arm64-simulator ב-Release, אל תפרסמי אליו.
Localization pruning: אם את שולחת ל-iOS App Store רק בעברית ואנגלית, הגדירי <MauiLocalizedResources>he;en</MauiLocalizedResources>. חוסך מגה'ות של resources מ-.NET runtime עצמו.
בפרויקט הפרודקשן האחרון שהעברתי מ-Xamarin ל-MAUI 10, ה-IPA ירד מ-138MB ל-52MB אחרי כל השלבים האלה יחד. השילוב בין NativeAOT ל-optimization של resources נותן את התוצאה הגדולה. אף אחד מהם לבד לא מספיק. פרטים נוספים על ניהול זיכרון וזמן הפעלה מופיעים במדריך אופטימיזציית ביצועים ב-.NET MAUI 10.
Mono Interpreter ל-development ול-hot reload
הקומפילציה של NativeAOT ל-Release איטית. build מלא של אפליקציה בינונית לוקח 3–5 דקות. את לא רוצה לחכות את זה במחזור dev. הפתרון הוא Mono Interpreter: מצב שבו הקוד מנוהל דרך המפרש של Mono במקום JIT או AOT, בפורמט שעובר את מדיניות ה-code-signing של Apple ומאפשר hot reload מהיר בין builds.
הבשורה הרעה: אחרי publish, ה-managed debugger של Visual Studio לא יעבוד. ה-binary הוא native לחלוטין ואין CLR פעיל. הבשורה הטובה: lldb עובד מצוין. הצינור שלי לחקירת crashes בפרודקשן:
לאסוף את ה-.dSYM מה-bin/Release/net10.0-ios/ios-arm64/publish/. תיקיית dSYM זו חובה. בלעדיה, ה-stack trace יגיע כתובות hex בלבד.
לאסוף את ה-crash log מהמכשיר (Xcode → Devices and Simulators → View Device Logs), או מ-App Store Connect → Analytics → Crashes.
להריץ atos או symbolicatecrash עם ה-dSYM כדי לתרגם כתובות לשמות מתודות.
אם צריך live debugging: לצרף את lldb ל-process על מכשיר פיזי דרך lldb -p <pid>.
ל-observability, אני משלבת Microsoft.Extensions.Logging עם target של OSLog ב-Apple (בסיס os_log) ו-Application Insights ל-remote telemetry. NativeAOT לא פוגע ב-logging. הוא רק פוגע בדיבוג לייב. השילוב של תיעוד מפורט + dSYMs שמורים בכל release + crashlytics reporting נותן לך visibility דומה למה שהיה לך במודל Mono, בלי הצורך ב-managed debugger. עוד על הנושא אפשר לקרוא ב-מסמכי dotnet/macios של NativeAOT.
בנצ'מרקים אמיתיים: לפני ואחרי
המספרים למטה מגיעים מהפרויקט האחרון שהעברתי, אפליקציית לוגיסטיקה של 600k שורות עם 42 מסכים, 180 dependencies. Simulator iPhone 15 Pro, ה-build ב-Xcode 26.0, MAUI 10.0.3:
מדד
Mono AOT (MtouchLink=SdkOnly)
NativeAOT (MtouchLink=Full)
שיפור
גודל IPA
112MB
52MB
-54%
Cold start (iPhone 13)
1.82s
0.94s
-48%
Warm start
0.61s
0.38s
-38%
Peak memory (idle)
142MB
98MB
-31%
Time-to-first-frame
1.4s
0.7s
-50%
Publish time (CI)
3m 12s
7m 48s
+143% (איטי יותר)
כן, build times גדלים משמעותית ב-CI, אבל זה מחיר סביר לתמורה. ה-cold start של פחות משנייה הוא הבדל שהמשתמשים ממש מרגישים, במיוחד באפליקציות שנפתחות עשרות פעמים ביום, כמו האפליקציה שלנו שסייעה לנהגי משאיות. פרטים נוספים על benchmarking שיטתי נמצאים ב-שיחת ה-NativeAOT ב-.NET Conf 2024, שמראה שיטות מדידה דומות עם BenchmarkDotNet.
שאלות נפוצות
האם NativeAOT תומך ב-Android ב-.NET MAUI 10?
לא. נכון ל-.NET MAUI 10 (ספטמבר 2026), NativeAOT זמין רק ל-iOS, Mac Catalyst ו-Windows. אנדרואיד ממשיך להשתמש ב-Mono AOT עם ART כ-runtime. הצוות של dotnet/android מעדכן שתמיכה ב-AOT נייטיבי מלא לאנדרואיד נבחנת ל-.NET 11 אבל אין ETA רשמי.
מה ההבדל בין PublishAot ל-RunAOTCompilation?
PublishAot=true מפעיל את NativeAOT: הקומפיילר החדש שמייצר binary נייטיבי. RunAOTCompilation=true הוא הגדרה ישנה של Mono AOT שרצה בין ה-IL ל-Mono runtime. בפרויקטי MAUI 10 עם NativeAOT, לא צריך להגדיר את RunAOTCompilation. הוא רלוונטי רק לפרויקטים שנשארים על Mono.
האם אפשר להשתמש ב-Reflection.Emit עם NativeAOT?
לא. Reflection.Emit דורש יצירת קוד בזמן ריצה, ו-Apple לא מאפשרת זאת על iOS. גם אם הייתה מאפשרת, NativeAOT לא כולל את ה-JIT הנדרש לפעולה. הפתרון הוא source generators של Roslyn. כל תבנית של יצירת קוד דינמי יכולה להיות מומרת ל-generator שרץ בזמן compile.
איך אני מטפלת ב-System.Text.Json עם NativeAOT?
חובה להשתמש ב-JSON source generator: להגדיר partial class שיורש מ-JsonSerializerContext ולסמן אותו ב-[JsonSerializable(typeof(YourModel))] לכל DTO. לאחר מכן להעביר את ה-context ל-JsonSerializer.Serialize/Deserialize. כך אין reflection דינמי ו-NativeAOT לא ימחק את הטיפוסים שלך.
האם ילדי דור שני של Xamarin.Forms עובדים ב-NativeAOT?
Xamarin.Forms עצמו הסתיים בתחזוקה במאי 2024. אם יש לך אפליקציה עם Custom Renderers ישנים, את חייבת קודם להעביר אותה למודל ה-Handlers של MAUI. הרוב המכריע של ה-Handlers הוא AOT-safe מהקופסה, וההגירה מתועדת במדריך ההגירה מ-Xamarin.Forms שלנו. שילוב בין קוד Xamarin.Forms.Compatibility ל-NativeAOT: לא נתמך.
Devika spent four years on the Xamarin team at Microsoft before the transition to .NET MAUI, where she worked on the iOS handler layer and shipped fixes that landed in the .NET 7 and .NET 8 release notes. She left Redmond in 2023 to run mobile engineering at a Series B logistics startup, porting their 600k-line Xamarin.Forms codebase to MAUI over eleven months.
She writes mostly about the unglamorous parts of cross-platform work: handler internals, AOT trimming on iOS, MSBuild target customization, and why your hot reload keeps breaking. She holds the .NET MAUI MVP award (2024, 2025) and has spoken at .NET Conf and Xamarin Expert Day. Based in Bengaluru, she still pushes the occasional PR to the dotnet/maui repo on weekends.
אופטימיזציית ביצועים ב-.NET MAUI 10: קיצור זמן הפעלה, גלילה חלקה ב-CollectionView, מניעת דליפות זיכרון ו-AOT. מדריך מעשי עם דוגמאות קוד ומדידות אמיתיות לשנת 2026.