تقليل حجم .NET MAUI Android APK: من 84 إلى 31 ميجابايت (2026)

تجربة عملية لتقليل حجم تطبيق .NET MAUI Android من 84 إلى 31 ميجابايت باستخدام التقليم الكامل و R8 و Android App Bundle وتحسين الصور والخطوط دون خسارة أي ميزة.

تقليل حجم .NET MAUI APK 2026

آخر تحديث: 5 سبتمبر 2026

لتقليل حجم تطبيق .NET MAUI Android APK في 2026، فعّل التقليم الكامل (TrimMode=full)، وشغّل R8 مع تقليص الموارد، وانتقل من APK إلى Android App Bundle (AAB) للحصول على تسليم مُجزَّأ حسب المعمارية والدقّة. هذا المزيج وحده كافٍ لتقليص حجم التطبيق بنسبة 55–70٪ في الحالات النموذجية. في تطبيق الفينتك الذي أعمل عليه، انخفض حجم APK من 84 ميجابايت إلى 31 ميجابايت باستخدام هذه التقنيات مع تحسين الصور والخطوط، وذلك دون التضحية بأي ميزة أو تحمّل تكاليف NativeAOT التجريبي.

  • التقليم الكامل (TrimMode=full) في .NET MAUI 10 يوفّر 40–55٪ من الحجم في المتوسط، ويتطلب تعليقات DynamicallyAccessedMembers على الأنواع المستخدمة عبر الانعكاس.
  • Android App Bundle (AAB) يقلل الحجم النهائي بنسبة 25–45٪ إضافية لأن Google Play يسلّم فقط ملفات ABI والموارد المطابقة لجهاز كل مستخدم.
  • R8 مع AndroidLinkResources=true يزيل موارد Java/Kotlin غير المستخدمة من مكتبات AndroidX ويوفر عادةً 3–8 ميجابايت إضافية.
  • تحويل صور PNG/JPG إلى WebP يقلل حجم أصول الصور بـ 25–35٪ دون خسارة بصرية ملحوظة.
  • حصر التطبيق على arm64-v8a فقط أصبح آمناً في 2026 لأن Google Play يمنع رفع تطبيقات ARM32 جديدة منذ أغسطس 2025.
  • NativeAOT في MAUI 10 يقلل الحجم بشكل ملحوظ لكنه يزيد وقت البناء 3–4 أضعاف وليس متوافقاً مع كل المكتبات؛ استخدمه بحذر.

لماذا تصبح تطبيقات .NET MAUI ضخمة الحجم؟

عندما تبني تطبيق MAUI فارغ من قالب dotnet new maui وتحزمه في وضع Release، ستحصل عادةً على ملف APK بحجم يتراوح بين 45 و 65 ميجابايت. هذا الرقم يصدم كثيراً من المطورين القادمين من Xamarin.Android الكلاسيكي أو من التطوير الأصلي بـ Kotlin. السبب الرئيسي هو أن كل تطبيق MAUI يحمل نسخة كاملة من وقت تشغيل Mono، ومكتبة BCL (.NET Base Class Library)، ومكتبات MAUI الأساسية، إضافة إلى مكتبات AndroidX التي تعتمد عليها الحزم البشرية للتحكم مثل CollectionView و Shell.

الأسوأ من ذلك، أن التطبيق يُبنى افتراضياً لأربع معماريات: arm64-v8a، armeabi-v7a، x86، و x86_64. هذا يعني أن كل مكتبة أصلية (native library) موجودة أربع مرات داخل الحزمة. أضف إلى ذلك الصور بدقّات متعددة (mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi)، والخطوط بحجمها الكامل حتى لو كنت تستخدم 15 محرفاً فقط منها، ومكتبات Xamarin.Google.Android.Material التي تجرّ معها آلاف الفئات غير المستخدمة.

الخبر السار هو أن معظم هذا الحجم قابل للإزالة أو التأجيل إلى وقت التسليم. المُقلِّم (ILLink من Microsoft) قادر على إزالة أكثر من 60٪ من كود BCL و MAUI عند تفعيله بشكل صحيح، و R8 يفعل الشيء نفسه لجانب Java/Kotlin، و Google Play يتكفّل بالباقي عبر Android App Bundle. المشكلة الوحيدة أن هذه الميزات ليست مفعّلة افتراضياً في قوالب MAUI. يجب تفعيلها يدوياً وضبطها بعناية.

كيفية قياس حجم APK وتحليل محتواه

قبل أن تبدأ في تحسين أي شيء، تحتاج إلى معرفة ماذا يستهلك الحجم بالفعل. القاعدة عندي في فريق أساسات المحمول: لا تقم بأي تحسين قبل أن يكون لديك قياس دقيق قبل وبعد. الأداة الرسمية من Google لتحليل APK هي apkanalyzer التي تأتي مع Android Studio، لكنني أفضّل استخدامها من سطر الأوامر ضمن CI:

$ARM_SDK/cmdline-tools/latest/bin/apkanalyzer apk summary \
    bin/Release/net10.0-android/publish/com.example.app-Signed.apk

$ARM_SDK/cmdline-tools/latest/bin/apkanalyzer files list \
    bin/Release/net10.0-android/publish/com.example.app-Signed.apk \
    | sort -k3 -h -r | head -30

الأمر الثاني يعرض لك أكبر 30 ملفاً داخل APK مرتّبة تنازلياً حسب الحجم. في التطبيقات النموذجية سترى أن الملفات الأكبر عادةً هي:

  • lib/arm64-v8a/libmonosgen-2.0.so: وقت تشغيل Mono (حوالي 8–12 ميجابايت لكل معمارية).
  • assemblies/System.Private.CoreLib.dll: نواة BCL (6–10 ميجابايت قبل التقليم).
  • assemblies/Microsoft.Maui.Controls.dll: مكتبة MAUI الأساسية (2–4 ميجابايت).
  • res/drawable-*/: صور بدقّات متعددة.
  • ملفات AAR من AndroidX.

أنصح بإنشاء ملف نصي يسمّى size-baseline.txt في جذر المستودع يحتوي على القياس الأولي، ثم إضافة خطوة في مسار CI/CD (يمكن دمجها مع سير عمل GitHub Actions للـ MAUI) تفشل البناء إذا زاد الحجم عن نسبة معينة مقارنة بالقاعدة. أنشرها كتعليق على كل Pull Request. إن كنت تستخدم GitHub Actions لتطبيق MAUI الخاص بك، يمكنك دمج ذلك مع خطوات البناء الموجودة كما شرحت في دليل GitHub Actions لـ .NET MAUI CI/CD.

تفعيل التقليم الكامل (Full Trimming) في MAUI 10

التقليم (Trimming) هو العملية التي يقوم بها المُقلِّم بإزالة الكود غير المستخدم من التجميعات (assemblies) قبل حزمها. في MAUI 10، أصبح TrimMode=full هو الوضع المستقر للإنتاج، بينما كان في MAUI 8 و 9 يُوصف بأنه تجريبي. الفرق بين partial و full كبير: الوضع الجزئي يُقلّم فقط التجميعات المُعلَّمة بـ <IsTrimmable>true</IsTrimmable>، بينما الوضع الكامل يُقلّم كل شيء بما في ذلك BCL و MAUI نفسه.

لتفعيل التقليم الكامل، أضف الإعدادات التالية إلى ملف .csproj ضمن مجموعة <PropertyGroup> الخاصة بـ Release:

<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)'=='Release|net10.0-android'">
    <PublishTrimmed>true</PublishTrimmed>
    <TrimMode>full</TrimMode>
    <TrimmerSingleWarn>false</TrimmerSingleWarn>
    <SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
    <EnableTrimAnalyzer>true</EnableTrimAnalyzer>
    <RunAOTCompilation>true</RunAOTCompilation>
    <AndroidStripILAfterAOT>true</AndroidStripILAfterAOT>
</PropertyGroup>

الإعداد AndroidStripILAfterAOT=true مهم جداً — بدونه يبقى كود IL في الحزمة إلى جانب الكود المُترجم مسبقاً. مع تفعيله، يُحذف IL نهائياً بعد التجميع المسبق (AOT)، مما يوفر 5–8 ميجابايت إضافية.

معالجة تحذيرات المُقلِّم

سيبدأ المُقلّم بإطلاق تحذيرات IL2026، IL2075، وما شابه. هذه ليست ضوضاء يمكن تجاهلها — كل تحذير يعني كوداً يستخدم الانعكاس أو التحميل الديناميكي وقد يفشل في وقت التشغيل بعد التقليم. الحل الأنظف هو تعليق النقاط الحرجة بـ DynamicallyAccessedMembers:

using System.Diagnostics.CodeAnalysis;

public class JsonMapper
{
    public T Deserialize<[DynamicallyAccessedMembers(
        DynamicallyAccessedMemberTypes.PublicProperties |
        DynamicallyAccessedMemberTypes.PublicConstructors)] T>(string json)
    {
        return System.Text.Json.JsonSerializer.Deserialize<T>(json)!;
    }
}

للمكتبات التي لا تملك التحكم في كودها المصدري، استخدم ملف ILLink.Descriptors.xml لإعلان الأنواع التي يجب الحفاظ عليها. هذه التقنية موثّقة بالتفصيل في وثائق خيارات التقليم من Microsoft. من الأخطاء الشائعة الاعتماد على مُسلسل JSON عام في مكان مثل استدعاءات REST. عالجت هذا التحدي في دليل استهلاك REST API في MAUI باستخدام JsonSerializerContext المُولَّد من مصدر.

تشغيل R8 وتقليص الموارد على Android

R8 هو المُقلِّم والمُبَهِّم الرسمي من Google لكود Java/Kotlin. مهمته موازية لمهمة ILLink لكن على جانب JVM. في المشاريع النموذجية، مكتبات AndroidX تجلب معها 8–12 ميجابايت من الفئات، وأغلبها غير مستخدم في تطبيق MAUI متوسط الحجم. R8 يزيل هذا الكود الميت ويُبَهِّم الأسماء لتقليل حجم قاموس السلاسل.

لتفعيل R8 وتقليص الموارد، أضف هذه الخصائص:

<PropertyGroup Condition="'$(Configuration)'=='Release'">
    <AndroidLinkTool>r8</AndroidLinkTool>
    <AndroidLinkResources>true</AndroidLinkResources>
    <AndroidEnableProguard>true</AndroidEnableProguard>
    <AndroidR8IgnoreWarnings>false</AndroidR8IgnoreWarnings>
</PropertyGroup>

<ItemGroup Condition="'$(Configuration)'=='Release'">
    <ProguardConfiguration Include="proguard.cfg" />
</ItemGroup>

ثم أنشئ ملف proguard.cfg في جذر مشروع Android بالمحتوى التالي كنقطة بداية:

# Keep JNI-called classes and members
-keepclassmembers class * {
    @Java.Interop.Export *;
}

# Keep classes referenced by Xamarin/MAUI runtime
-keep class mono.MonoRuntimeProvider { *; }
-keep class mono.MonoPackageManager { *; }
-keep class md5* extends android.app.Activity

# Keep AndroidX WebView bridge
-keep class androidx.webkit.** { *; }

# Silence known warnings from optional dependencies
-dontwarn com.google.errorprone.annotations.**
-dontwarn org.jetbrains.annotations.**

حصر المعماريات المدعومة

منذ أغسطس 2025، Google Play يمنع رفع تطبيقات جديدة تدعم فقط ARM32. هذا يعني أن دعم armeabi-v7a أصبح اختيارياً بحتاً. في تطبيق الفينتك الذي أعمل عليه، حصرنا الدعم على arm64-v8a فقط بعد التأكد من أن أقل من 0.3٪ من قاعدة المستخدمين لدينا يستخدمون أجهزة 32-bit فقط:

<PropertyGroup Condition="'$(Configuration)'=='Release'">
    <RuntimeIdentifiers>android-arm64</RuntimeIdentifiers>
    <AndroidSupportedAbis>arm64-v8a</AndroidSupportedAbis>
</PropertyGroup>

هذا الإعداد وحده يوفّر 40–50٪ من حجم المكتبات الأصلية لأن كل ملف .so يوجد الآن مرة واحدة فقط بدلاً من مرتين أو أربع مرات.

الانتقال من APK إلى Android App Bundle

Android App Bundle (AAB) هو تنسيق النشر الرسمي في Google Play منذ 2021، لكن كثيرين من مطوري MAUI ما زالوا ينشرون ملفات APK مباشرة. الفرق الأساسي: عند رفع AAB، يقوم Google Play بتفكيك الحزمة وإعادة توليد APKs مُخصَّصة لكل جهاز، تحتوي فقط على المكتبات الأصلية للمعمارية الصحيحة، وموارد اللغة الحالية، وصور الدقّة المناسبة. النتيجة: التطبيق الذي يُثبَّت على جهاز المستخدم أصغر بنسبة 25–45٪ من AAB الأصلي.

الخاصيةAPK تقليديAndroid App Bundle
حجم الملف المرفوعمثل الحجم النهائيأكبر من APK المُسلَّم بـ 25–45٪
حجم التثبيت على الجهازكامل (كل المعماريات)مُخصَّص للمعمارية واللغة والدقّة
يدعم Dynamic Deliveryلانعم
مطلوب في Google Play للتطبيقات الجديدةلانعم (منذ أغسطس 2021)
يعمل خارج Google Playمباشرةيحتاج bundletool

لبناء AAB بدلاً من APK في MAUI، استخدم -p:AndroidPackageFormat=aab:

dotnet publish -f net10.0-android \
    -c Release \
    -p:AndroidPackageFormat=aab \
    -p:AndroidKeyStore=true \
    -p:AndroidSigningKeyStore=release.keystore \
    -p:AndroidSigningStorePass=$STORE_PASS \
    -p:AndroidSigningKeyAlias=release \
    -p:AndroidSigningKeyPass=$KEY_PASS \
    -o ./publish

يمكنك أيضاً تفعيل AAB بشكل دائم عبر إضافة <AndroidPackageFormat>aab</AndroidPackageFormat> إلى الـ .csproj. راجع الوثائق الرسمية لـ Android App Bundle لفهم آلية Dynamic Delivery وميزة تسليم الأصول عند الطلب (Asset Delivery).

تحسين الصور والخطوط والموارد الثابتة

بعد التقليم و R8 و AAB، يصبح كود التطبيق نحيفاً، والآن تبدأ الأصول (assets) بالظهور كحصة كبيرة من الحجم. في تطبيقنا، اكتشفنا أن ملف خط NotoSans-VariableFont وحده كان يستهلك 4.2 ميجابايت، وأن مجلد الصور كان يحتوي على 180 ملف PNG بدقّات متعددة معظمها كان يمكن تحويلها إلى WebP.

التحويل إلى WebP

WebP يوفر ضغطاً أفضل من PNG بنسبة 25–35٪ ويدعم الشفافية. MAUI يقبل WebP في مجلد Resources/Images منذ إصدار 8. استخدم cwebp من ImageMagick أو libwebp لتحويل جميع صور PNG في خطوة CI:

find Resources/Images -name "*.png" -exec sh -c '
    cwebp -q 85 -m 6 "$1" -o "${1%.png}.webp" && rm "$1"
' _ {} \;

تقليل الخطوط عبر Font Subsetting

خطوط النظام تحمل عادةً آلاف المحارف لدعم لغات متعددة. إن كان تطبيقك يستخدم العربية والإنجليزية فقط، يمكنك تقليم الخط ليحتوي فقط على النطاقات المطلوبة. أدوات مثل pyftsubset من مشروع FontTools تقوم بذلك:

pyftsubset NotoSans-Regular.ttf \
    --unicodes="U+0020-007E,U+0600-06FF,U+FE70-FEFF" \
    --output-file=NotoSans-Regular.subset.ttf \
    --layout-features='*'

في تطبيقنا وفّرت هذه الخطوة وحدها 3.1 ميجابايت (من 4.2 إلى 1.1 ميجابايت للخط الواحد).

NativeAOT مقابل Mono: المقايضة بين الحجم والتوافق

NativeAOT (المعروف سابقاً بـ CoreRT) هو نمط تجميع جديد يحوّل كود .NET إلى ثنائيات أصلية بالكامل دون الحاجة إلى وقت تشغيل Mono. في MAUI 10، أصبح NativeAOT متاحاً كخيار تجريبي لـ Android و iOS، وينتج ثنائيات أصغر بنسبة 15–25٪ من Mono AOT التقليدي، مع تحسّن ملحوظ في وقت الإقلاع.

لكن هناك ثمن: NativeAOT لا يدعم الانعكاس الديناميكي بالكامل، ويكسر مكتبات تعتمد بشدة عليه مثل بعض مُسلسِلات JSON القديمة و AutoMapper. كما أن وقت البناء يزيد 3–4 أضعاف. لتفعيله:

<PropertyGroup Condition="'$(Configuration)'=='Release'">
    <PublishAot>true</PublishAot>
    <OptimizationPreference>Size</OptimizationPreference>
    <IlcOptimizationPreference>Size</IlcOptimizationPreference>
</PropertyGroup>

توصيتي في 2026: ابدأ بالتقليم الكامل + R8 + AAB أولاً. هذه التقنيات الثلاث معاً كافية للوصول إلى حجم أقل من 35 ميجابايت في معظم التطبيقات. لا تدفع تكلفة NativeAOT إلا إذا كنت تحتاج آخر 5–8 ميجابايت أو تحسين وقت الإقلاع على أجهزة ضعيفة الأداء. اقرأ المزيد عن مقارنات الأداء في الدليل الشامل لتحسين أداء تطبيقات .NET MAUI.

قائمة تدقيق قبل النشر إلى Google Play

قبل رفع البناء إلى Google Play Console، تأكد من هذه النقاط:

  1. تم بناء التطبيق في وضع Release وليس Debug. تحقق بـ strings libmonosgen-2.0.so | grep -i debug.
  2. PublishTrimmed=true و TrimMode=full مفعّلان في مجموعة خصائص Release فقط.
  3. AndroidStripILAfterAOT=true مفعّل لحذف IL بعد AOT.
  4. AndroidLinkTool=r8 و AndroidLinkResources=true مفعّلان.
  5. تنسيق الحزمة aab وليس apk.
  6. المعماريات محصورة على arm64-v8a (ما لم يكن لديك سبب قوي لدعم ARM32).
  7. تم تحويل جميع صور PNG/JPG إلى WebP.
  8. ملفات الخطوط مُقلَّمة إلى النطاقات المستخدمة فعلياً.
  9. تم توقيع الحزمة بمفتاح الإنتاج (وليس مفتاح Debug).
  10. تم اختبار الحزمة النهائية على جهاز حقيقي قبل الرفع، لأن التقليم قد يكشف عن أخطاء وقت تشغيل غير موجودة في وضع Debug.

الأسئلة الشائعة

ما الحد الأقصى لحجم AAB الذي يقبله Google Play؟

يقبل Google Play ملفات AAB بحجم يصل إلى 200 ميجابايت للحزمة الأساسية (Base APK)، مع إمكانية توسيع ذلك إلى 2 غيغابايت عبر Play Asset Delivery للأصول الثقيلة مثل الصور عالية الدقة والفيديو والنماذج ثلاثية الأبعاد. لكن Google تنصح بإبقاء APK النهائي المُسلَّم للمستخدم أقل من 100 ميجابايت لتحسين معدلات التثبيت.

هل يجب أن أفعّل NativeAOT في تطبيق MAUI الإنتاجي في 2026؟

في معظم الحالات، لا. NativeAOT لا يزال يُوصف بأنه معاينة (preview) في MAUI 10، ولا تدعمه بعض المكتبات الشائعة مثل Xamarin.Google.Android.Material الكاملة. ابدأ بـ TrimMode=full و R8 و AAB، فهذه التقنيات تعطي 80٪ من فائدة NativeAOT بدون مخاطر التوافق.

لماذا يظل حجم تطبيقي كبيراً بعد تفعيل التقليم الكامل؟

الأسباب الشائعة: نسيان تفعيل AndroidStripILAfterAOT=true فيبقى كود IL بجانب الكود المُترجم، أو استخدام مكتبات مُعلَّمة بـ IsTrimmable=false تمنع تقليم فئاتها، أو الاعتماد على تعليقات DynamicDependency واسعة تحفظ أكثر مما هو ضروري. حلّل مخرجات apkanalyzer لمعرفة أي التجميعات لا تزال ضخمة.

هل يمكن استخدام Android App Bundle خارج Google Play؟

نعم، لكن ليس مباشرة. تحتاج أداة bundletool من Google لتوليد APKs من AAB وتسليمها للمستخدمين. متاجر تطبيقات بديلة مثل Amazon Appstore و Huawei AppGallery تدعم AAB أو APK، لكن التوزيع المباشر (Sideloading) يتطلب APK. الحل: أنشئ AAB للنشر الرسمي وحافظ على مسار بناء APK لأغراض الاختبار والتوزيع الخارجي.

كيف أختبر أن التقليم لم يكسر تطبيقي؟

الطريقة الوحيدة الموثوقة هي تشغيل مجموعة اختبارات واجهة المستخدم الكاملة على بناء Release مُقلَّم على جهاز حقيقي أو محاكي. تحذيرات المُقلّم أثناء البناء تكشف بعض المشاكل، لكن كثير من أخطاء التقليم تظهر فقط في وقت التشغيل عند سلوك معين. استثمر في اختبارات UI شاملة تغطي المسارات الحرجة قبل الاعتماد على التقليم الكامل في الإنتاج.

عن الكاتب Sofia Marchetti

Sofia is a mobile platform engineer with eleven years across native iOS, Xamarin, and now .NET MAUI. She spent three years at Spotify in Stockholm working on internal tooling for the mobile build infrastructure, then joined a fintech in Milan where she leads the mobile foundations team responsible for a MAUI app that handles around 2 million monthly active users across iOS and Android. Most of what she writes about lives in the build and release layer: deterministic builds, fastlane integration for MAUI, code signing on macOS runners, MAUI .NET 9 upgrade postmortems, and benchmarking startup time on cheap Android hardware. She co-organizes the Milano .NET meetup and gave a talk at NDC Oslo 2025 on shrinking a MAUI Android APK from 84 MB to 31 MB without losing features.