لنفرض أن لديك فريقاً صغيراً من ثلاثة مطورين أنهى تطبيق طلبات لمتجر إلكتروني ناشئ، والتطبيق يعمل على جوالات الفريق منذ أسابيع، والعميل حدد موعد الإطلاق يوم الأحد القادم. وفي يوم الخميس يكتشف أحدهم أن كل نسخ الاختبار كانت موقعة بمفتاح التصحيح Debug Key الموجود على جهازه، وأن أحداً لم ينشئ مفتاح الإصدار بعد، وأن حساب Google Play أنشئ قبل أسبوع فقط، وأن Apple طلبت في رسالة الرفض الأولى حساباً تجريبياً لتسجيل الدخول لم يرسله أحد. وهذه ليست قصة نادرة، فالفرق تقضي شهوراً في بناء التطبيق ثم تكتشف أن الخطوة الأخيرة، أي إيصاله إلى المتجرين، لها قواعدها الخاصة ومواعيدها التي لا تتحرك من أجل موعد إطلاقك.
والمشكلة أن هذه الخطوة تجمع ثلاثة أشياء مختلفة في وقت واحد: التوقيع الرقمي Code Signing الذي يربط كل تحديث قادم بمفتاح واحد لا يمكن تغييره بسهولة، والتغليف Packaging الذي يحدد ما الذي يصل إلى جوال المستخدم وكم حجمه، والمراجعة Review التي يقرأ فيها إنسان أو نظام آلي تطبيقك مقابل قائمة طويلة من الإرشادات. وأي خطأ في الأول يبقى معك طوال عمر التطبيق، أما الخطآن الآخران فيكلفانك أياماً من التأخير في كل مرة.
وفي هذا المقال سوف نأخذ تطبيق الطلبات نفسه الذي بنيناه في مقالات هذه المرحلة، ونجهز له إصداراً حقيقياً لنظام Android على جهازنا: نولد مفتاح الرفع، ونرى ماذا يفعل R8 بالحجم، ونوقع ونتحقق، ثم نخطئ عمداً لنرى رسائل الرفض الحقيقية من الجهاز، ونجرب تدوير المفتاح، ونبني حزمة App Bundle ونرى ما الذي يصل إلى جوال واحد منها. ولاحظ أننا لم نرسل التطبيق إلى Play Console ولا إلى App Store Connect، فجزء المتاجر في المقال مبني على صفحاتها الرسمية مع تاريخ كل اقتباس، وجزء التوقيع والتغليف مبني على تجربة نفذناها فعلاً.
سوف نناقش في هذا المقال ما يلي:
- لماذا يوقع كل تطبيق جوال، وكيف تطورت مخططات التوقيع في Android من v1 إلى v3.1.
- الفرق بين مفتاح الرفع Upload Key ومفتاح توقيع التطبيق App Signing Key، وماذا يحدث إذا ضاع أحدهما.
- سلسلة التوقيع عند Apple: الشهادة والمعرف والملف التعريفي Provisioning Profile.
- تجربة كاملة: R8 والتوقيع والتحقق والعبث بالملف والتحديث بمفتاح مختلف وتدوير المفتاح وحزمة App Bundle ومحاذاة 16 KB.
- ما تطلبه Google Play في 2026: مستوى API المستهدف والاختبار المغلق وأمان البيانات والتحقق من هوية المطور.
- كيف تقرأ إرشادات مراجعة Apple، وما الذي يقوله تقرير الشفافية عن أسباب الرفض.
- جدول بأسباب الرفض الشائعة وكيف تتجنبها، وقائمة تحقق قبل الإرسال.
والصورة التالية تبين المسارين من الكود إلى جوال المستخدم، وأين يقع التوقيع والمراجعة في كل منهما:

لماذا يوقع كل تطبيق؟ التوقيع الرقمي Code Signing
لنبدأ من السؤال الذي يطرحه كل من يرى خطوة التوقيع لأول مرة: لماذا يحتاج نظام التشغيل أن يعرف من كتب التطبيق؟ والإجابة على ذلك أن الجوال لا يهمه اسمك، وإنما يهمه أمر واحد هو أن التحديث القادم جاء من نفس الجهة التي جاء منها الإصدار المثبت. فإذا ثبت المستخدم تطبيق الطلبات وأعطاه صلاحية الإشعارات والموقع، وحفظ فيه التطبيق توكن تسجيل الدخول في ملفاته الخاصة، ثم وصل ملف APK آخر يحمل نفس اسم الحزمة Package Name ويريد أن يحل محله، فالنظام يجب أن يقرر: هل هذا تحديث حقيقي من المطور نفسه، أم تطبيق آخر يريد أن يرث بيانات الأول وصلاحياته؟ والطريقة الوحيدة التي يعرف بها ذلك هي التوقيع: كل إصدار يحمل توقيعاً بمفتاح خاص Private Key، والنظام يقارن شهادة التحديث بشهادة النسخة المثبتة، فإذا اختلفتا يرفض التحديث.
وهذا يعني أن التوقيع في Android لا يحتاج جهة إصدار شهادات Certificate Authority كما في مواقع الويب، فالشهادة غالباً موقعة ذاتياً Self-signed، وقيمتها في ثباتها لا في من أصدرها. ولذلك تقول صفحة توقيع التطبيق في توثيق Android إن مفتاح التوقيع لا يتغير طوال عمر التطبيق ضمن نموذج التحديث الآمن في النظام. أما في iOS فالأمر مختلف، فالشهادة تصدرها Apple نفسها لحسابك، ولا يقبل الجهاز أي تطبيق غير موقع بسلسلة تنتهي عند Apple، وسوف نرى ذلك بعد قليل.
مخططات التوقيع Signature Schemes من v1 إلى v3.1
في بدايات Android كان التوقيع هو توقيع ملفات JAR المعروف من Java، ويسمى اليوم المخطط الأول v1: يحسب الهاش لكل ملف داخل الأرشيف ويضعه في ملفات داخل المجلد META-INF ثم يوقعها. وهذه الطريقة لها مشكلتان ذكرهما توثيق توقيع APK في مشروع Android: لا تحمي أجزاء من ملف ZIP مثل بياناته الوصفية، والجهاز يجب أن يفك ضغط كل الملفات ليتحقق منها، وهذا بطيء ويفتح مساحة للهجوم على المحلل نفسه. لذلك جاء المخطط الثاني APK Signature Scheme v2 في Android 7.0، وهو يوقع الملف كاملاً كسلسلة بايتات ويضع التوقيع في كتلة خاصة اسمها APK Signing Block بين محتوى الملفات والفهرس المركزي Central Directory، فأي تغيير في أي بايت يفسد التوقيع، والتحقق لا يحتاج فك الضغط.
ثم جاء المخطط الثالث v3 في Android 9، وهو نفس v2 مع معلومات إضافية أهمها إثبات التدوير proof-of-rotation، أي إمكانية أن ينتقل التطبيق من مفتاح قديم إلى مفتاح جديد بشرط أن يوقع القديم على الجديد. ثم أضاف Android 13 المخطط v3.1 الذي يسمح بأن يستخدم الجهاز المفتاح الجديد بدءاً من إصدار معين من النظام، وأن تبقى الأجهزة الأقدم على المفتاح القديم، وسوف نرى هذا بأعيننا في التجربة. وهناك أيضاً المخطط v4 الذي يضع التوقيع في ملف منفصل لخدمة التثبيت التدريجي Incremental Install، ولا يعنيك غالباً في النشر.

مفتاح الرفع Upload Key ومفتاح توقيع التطبيق App Signing Key
إذا كان المفتاح لا يتغير طوال عمر التطبيق، فهناك سؤال مخيف: ماذا يحدث إذا ضاع؟ قبل سنوات كان الجواب بسيطاً وقاسياً: لا يمكنك تحديث التطبيق أبداً، وعليك أن تنشر تطبيقاً جديداً باسم حزمة جديد وتطلب من المستخدمين الانتقال إليه، وتخسر التقييمات وعدد التنزيلات. ولذلك أضافت Google خدمة Play App Signing، والفكرة أن هناك مفتاحين: مفتاح توقيع التطبيق App Signing Key الذي يوقع به الملف الذي يصل إلى الجوال، وتحتفظ به Google في بنيتها، ومفتاح الرفع Upload Key الذي تحتفظ به أنت وتوقع به الحزمة قبل رفعها إلى Play Console فقط، لتثبت أنها جاءت منك.
وتشرح صفحة توقيع التطبيق الفائدة مباشرة: باستخدام مفتاح رفع منفصل يمكنك طلب إعادة تعيين مفتاح الرفع إذا ضاع أو انكشف، أما التطبيقات التي لم تشترك في Play App Signing فإذا فقدت مفتاح التوقيع تفقد القدرة على تحديث التطبيق. والخدمة اختيارية فقط للتطبيقات التي أنشئت قبل أغسطس 2021، لأن Google تطلب منذ ذلك الوقت أن تنشر التطبيقات الجديدة بصيغة Android App Bundle، والحزمة لا يمكن أن تصل إلى الجوال إلا بعد أن تقطعها Google إلى ملفات APK وتوقعها، وبالتالي يجب أن يكون مفتاح التوقيع عندها. وطلب إعادة تعيين مفتاح الرفع يتم من Play Console كما في صفحة المساعدة الخاصة به.

وقد يتساءل البعض: هل يعني هذا أن Google تستطيع توقيع أي شيء باسم تطبيقي؟ نعم، وهذا هو الثمن، فأنت تثق بأن المتجر الذي يوزع تطبيقك لن يوزع غيره باسمك، وهي ثقة تعطيها أصلاً عندما تنشر فيه. وإذا كنت تريد نشر نفس التطبيق في متاجر أخرى بنفس التوقيع، فالتوثيق ينبه إلى أن ترفع مفتاحك أنت عند إعداد Play App Signing بدلاً من أن تترك Google تولد مفتاحاً جديداً، لأن المفتاح الذي تولده Google لا يمكنك إخراجه لتوقع به في مكان آخر.
ماذا يحدث إذا ضاع المفتاح Key Loss؟
لنضع الحالات في جدول واحد، لأن الفرق يكتشفونها غالباً في أسوأ يوم ممكن:
| ما الذي ضاع | مع Play App Signing | دون Play App Signing | في iOS |
|---|---|---|---|
| مفتاح الرفع Upload Key | تطلب إعادة تعيينه من Play Console، وتتوقف عن الرفع حتى يقبل الطلب | غير موجود، المفتاح الوحيد هو مفتاح التطبيق | لا يوجد مقابل مباشر |
| مفتاح توقيع التطبيق | عند Google، لا تفقده | لا يمكن تحديث التطبيق أبداً، تنشر تطبيقاً جديداً | تصدر شهادة جديدة من حسابك، فالتوقيع مرتبط بالحساب لا بملف |
| كلمة مرور ملف المفاتيح Keystore | نفس حالة ضياع مفتاح الرفع | نفس حالة ضياع المفتاح | تعيد إصدار الشهادة |
ولاحظ أن الفرق في iOS جوهري، فالجهاز لا يقارن مفتاحك بمفتاح سابق، وإنما يتحقق أن التطبيق موقع بسلسلة تنتهي عند Apple وأن معرف الفريق Team ID ومعرف الحزمة Bundle ID كما هما، ولذلك يمكنك إلغاء شهادة وإصدار أخرى دون أن يتأثر المستخدمون. وفي مقال أسئلة مقابلة مطور الجوال سؤال كامل عن هذه الحالة، فراجعه إذا كنت تستعد لمقابلة.
التوقيع عند Apple: الشهادات Certificates والملفات التعريفية Provisioning Profiles
في Apple لا يكفي مفتاح واحد، وإنما سلسلة من أربع حلقات يجب أن تتطابق كلها. الحلقة الأولى شهادة التوزيع Apple Distribution التي يصدرها حساب المطور، ومفتاحها الخاص يبقى في Keychain على جهاز Mac الذي أنشأ الطلب أو في نظام البناء، كما تشرح صفحة نظرة عامة على الشهادات. والحلقة الثانية معرف التطبيق App ID، وهو معرف الفريق مع معرف الحزمة، ومعه القدرات Capabilities التي يستخدمها التطبيق مثل الإشعارات Push Notifications والنطاقات المرتبطة Associated Domains. والحلقة الثالثة الملف التعريفي Provisioning Profile، وهو ملف يربط الشهادة بالمعرف وبالقدرات، ونوعه للنشر في المتجر هو App Store Provisioning Profile. والحلقة الرابعة الأرشيف الموقع الذي يخرج من Xcode ويرفع إلى App Store Connect.

والسبب في هذا التعقيد أن Apple تتحكم في كل ما يعمل على الجهاز، فالملف التعريفي هو الذي يقول للجهاز إن هذا التطبيق مسموح له باستخدام الإشعارات أو الروابط العامة، وإذا أضفت قدرة جديدة في الكود ولم تحدث الملف التعريفي فسوف يفشل الرفع أو يعمل التطبيق دون تلك القدرة. ولهذا السبب تحديداً ينصح الكثيرون بترك Xcode يدير التوقيع تلقائياً Automatic Signing في المشاريع الصغيرة، ولكن عليك أن تفهم الحلقات لأن البناء في خادم التكامل المستمر CI لا يملك Keychain جهازك، وهناك ستحتاج أن تنقل الشهادة والملف بنفسك. ولاحظ أن حساب Apple Developer Program باشتراك سنوي قيمته 99 دولاراً، بينما رسوم التسجيل في Google Play 25 دولاراً تدفع مرة واحدة، وكلاهما يحتاج تحقق هوية قد يأخذ أياماً، فابدأ به قبل موعد الإطلاق بوقت كاف.
تجربة عملية Lab: تجهيز إصدار Release لتطبيق Android وتوقيعه
الآن سوف نطبق كل ما سبق على تطبيق الطلبات المكتوب بلغة Kotlin وواجهة Jetpack Compose، وهو التطبيق نفسه الذي قارناه بنسختي Flutter وReact Native في مقال اختيار التقنية من هذه المرحلة. وكل الأدوات من حزمة Android SDK الرسمية: keytool وjarsigner من JDK المرفق مع Android Studio، وapksigner وzipalign وaapt2 من Build-Tools 37.0.0، والأداة bundletool بالإصدار 1.18.3، والجهاز محاكي Android 15. والملف sign.sh في مجلد التجربة ينفذ الخطوات بالترتيب، ونعرض هنا كل خطوة مع مخرجها كما ظهر.
lab-pass-123 في الأوامر للتجربة فقط. في مشروع حقيقي لا تكتب كلمة مرور ملف المفاتيح في سكربت ولا في ملف داخل المستودع، وإنما تقرؤها من متغير بيئة يملؤه نظام البناء من خزنة أسرار Secrets Vault، وتحتفظ بنسخة من ملف المفاتيح نفسه في مكانين منفصلين على الأقل.إنشاء مفتاح الرفع Upload Key
نبدأ بمفتاح الرفع بصيغة PKCS12، وهي الصيغة القياسية التي يستخدمها keytool اليوم، ومعه مفتاح ثان من نوع EC سوف نستخدمه لاحقاً في تجربة التدوير:
keytool -genkeypair -keystore upload.p12 -storetype PKCS12 \
-storepass lab-pass-123 -keypass lab-pass-123 \
-alias upload -keyalg RSA -keysize 3072 -validity 10000 \
-dname "CN=Orders Upload, O=Example"
keytool -genkeypair -keystore new.p12 -storetype PKCS12 \
-storepass lab-pass-123 -keypass lab-pass-123 \
-alias new -keyalg EC -groupname secp256r1 -validity 10000 \
-dname "CN=Orders 2027, O=Example"
keytool -list -v -keystore upload.p12 -storepass lab-pass-123والمخرج سوف يكون كما يلي:
Generating 3,072 bit RSA key pair and self-signed certificate (SHA384withRSA) with a validity of 10,000 days
for: CN=Orders Upload, O=Example
Generating 256 bit EC (secp256r1) key pair and self-signed certificate (SHA384withECDSA) with a validity of 10,000 days
for: CN=Orders 2027, O=Example
Alias name: upload
Valid from: Fri Oct 30 22:34:16 AST 2026 until: Tue Mar 17 22:34:16 AST 2054
SHA256: 2A:16:1A:34:0B:E0:59:FE:FF:9D:0F:C8:98:65:F2:EA:C8:A9:95:8A:A9:25:9C:86:BC:30:BD:9C:F0:3E:51:B3
Signature algorithm name: SHA384withRSA
Subject Public Key Algorithm: 3072-bit RSA keyفي المخرج أعلاه لاحظ التالي:
- الشهادة موقعة ذاتياً Self-signed، ولا توجد جهة إصدار، وهذا طبيعي في Android كما ذكرنا.
- مدة الصلاحية 10,000 يوم، أي حتى سنة 2054. وتوثيق Android ينصح بصلاحية 25 سنة أو أكثر لمفتاح التطبيق، والسبب كما يقول أن المستخدمين لا يستطيعون الترقية إلى الإصدارات الجديدة دون عوائق بعد انتهاء صلاحية المفتاح، وأنت لا تريد أن ينتهي مفتاحك قبل أن ينتهي تطبيقك.
- السطر
SHA256هو بصمة الشهادة Fingerprint، وهي الرقم الذي ستحتاجه في أماكن كثيرة: في ملفassetlinks.jsonللروابط العميقة، وفي إعدادات Firebase وخرائط Google، وعند طلب إعادة تعيين مفتاح الرفع. ولاحظ أن بصمة مفتاح الرفع ليست بصمة مفتاح التطبيق الذي تحتفظ به Google، فخدمات مثل الروابط العميقة تحتاج بصمة مفتاح التطبيق من صفحة App Signing في Play Console، وهذا خطأ شائع يجعل الرابط يعمل في نسخة الاختبار ولا يعمل في نسخة المتجر.
ماذا فعل R8 بحجم التطبيق؟ تقليص الكود Code Shrinking
قبل التوقيع ننظر إلى ما سوف نوقعه. بنينا التطبيق نفسه مرتين: مرة بإعدادات الإصدار مع تقليص الكود والموارد عبر R8، ومرة بنوع بناء أضفناه اسمه noR8 يرث كل إعدادات الإصدار ولكن يوقف التقليص:
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
}
create("noR8") {
initWith(getByName("release"))
isMinifyEnabled = false
isShrinkResources = false
}
}والنتيجة:
-rw-r--r-- 1 wajdy 197609 8196328 Oct 30 22:26 noR8-unsigned.apk
-rw-r--r-- 1 wajdy 197609 927869 Oct 30 22:26 v1-unsigned.apk
-- noR8
14078768 1981-01-01 01:01 classes.dex
7959232 1981-01-01 01:01 classes2.dex
472076 1981-01-01 01:01 resources.arsc
-- v1
1457828 1981-01-01 01:01 classes.dex
69156 1981-01-01 01:01 resources.arscالملف دون R8 حجمه 8,196,328 بايت، ومعه 927,869 بايت، أي أقل من ثمن الحجم. والسبب واضح في الأسطر التالية: الكود دون تقليص يحتاج ملفي dex بحجم 14 MB و8 MB تقريباً قبل الضغط، لأن مكتبات Compose وMaterial 3 تحمل آلاف الكلاسات والدوال التي لا يستخدم تطبيقنا الصغير إلا جزءاً منها، وR8 يتتبع ما يصل إليه الكود فعلاً من نقطة البداية ويحذف الباقي، ثم يختصر الأسماء ويدمج الدوال الصغيرة. وكذلك ملف الموارد resources.arsc نزل من 472,076 بايت إلى 69,156 لأن تقليص الموارد حذف ما لم يشر إليه الكود.
وهل هذا مجاني؟ لا، وهناك ثمنان. الأول أن أسماء الكلاسات في تقارير الأعطال تصبح مختصرة وغير مقروءة، وتحتاج ملف mapping.txt من نفس البناء لتعيدها، وقد شرحنا ذلك عملياً في قسم رفع ملف ال Mapping من مختبر الأعطال، فاحفظ هذا الملف مع كل إصدار ترفعه. والثاني أن الكود الذي يستخدم الانعكاس Reflection، مثل بعض مكتبات تحويل JSON، قد يفقد كلاسات يحتاجها وقت التشغيل ولا تظهر المشكلة إلا في نسخة الإصدار، ولذلك اختبر نسخة الإصدار نفسها لا نسخة التصحيح قبل الرفع.
التوقيع والتحقق بأداة apksigner
أولاً نتأكد أن الملف غير الموقع مصفوف بالشكل الصحيح، والأداة zipalign تتحقق من أن الملفات غير المضغوطة داخل الأرشيف تبدأ عند حدود محددة، والخيار -P 16 يطلب أن تكون المكتبات الأصلية على حدود 16 KB، وسوف نعود إليه:
zipalign -c -P 16 4 app-release-unsigned.apk && echo "zipalign: aligned"
apksigner sign --ks upload.p12 --ks-pass pass:lab-pass-123 \
--out orders-1.apk app-release-unsigned.apk
apksigner verify -v --print-certs orders-1.apkوالمخرج سوف يكون كما يلي:
zipalign: aligned
Verifies
Verified using v1 scheme (JAR signing): false
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): false
Verified using v3.2 scheme (APK Signature Scheme v3.2): false
Verified using v4 scheme (APK Signature Scheme v4): false
Verified for SourceStamp: false
Number of signers: 1
V3.0 Signer: certificate DN: CN=Orders Upload, O=Example
V3.0 Signer: certificate SHA-256 digest: 2a161a340be059feff9d0fc89865f2eac8a9958aa9259c86bc30bd9cf03e51b3
V3.0 Signer: key algorithm: RSA
V3.0 Signer: key size (bits): 3072في المخرج أعلاه لاحظ التالي:
- الترتيب مهم:
zipalignقبلapksignerدائماً، لأن أي تعديل على الملف بعد التوقيع يفسده كما سنرى، والأداةapksignerنفسها تحافظ على المحاذاة. v1 scheme: falseليس خطأ، فالأداة apksigner تقرأminSdkمن الملف، وهو 26 في تطبيقنا، وكل الأجهزة من Android 7.0 فما فوق تفهم v2، فلا داعي للمخطط القديم. ولو كانminSdkأقل من 24 لأضافته الأداة تلقائياً.- v2 وv3 كلاهما
true، والجهاز يختار أحدث مخطط يفهمه. - البصمة
2a161a34…هي نفسها التي رأيناها فيkeytool، مكتوبة بحروف صغيرة ودون نقطتين.
ماذا يحدث إذا تغير ملف بعد التوقيع Tampering؟
الآن نجرب ما يحدث عندما يعدل أحد الملف بعد توقيعه، سواء كان مهاجماً يريد حقن كود، أو أداة بناء تضيف ملفاً بعد خطوة التوقيع بالخطأ. نضيف ملفاً نصياً واحداً من سطر واحد إلى نسخة من الملف الموقع ثم نتحقق:
cp orders-1.apk tampered.apk
python -I -c "import zipfile; z=zipfile.ZipFile('tampered.apk','a'); z.writestr('res/extra.txt','x'); z.close()"
apksigner verify tampered.apkDOES NOT VERIFY
ERROR: JAR signer UPLOAD.RSA: JAR signature META-INF/UPLOAD.SF indicates the APK is signed using APK Signature Scheme v2 but no such signature was found. Signature stripped?
ERROR: JAR signer UPLOAD.RSA: JAR signature META-INF/UPLOAD.SF indicates the APK is signed using APK Signature Scheme v3 but no such signature was found. Signature stripped?وهذه نتيجة مثيرة أكثر مما توقعنا. فعندما أعادت مكتبة Python كتابة الفهرس المركزي لتضيف الملف، أزالت كتلة التوقيع APK Signing Block بالكامل لأنها لا تعرفها، فلم يبق في الملف إلا ملفات META-INF التي وضعتها apksigner للتوافق، وهذه الملفات نفسها تقول إن الملف موقع بالمخطط v2 وv3، فتسأل الأداة: أين ذهب التوقيع؟ والعبارة Signature stripped? هي الحماية المقصودة من هجوم حذف التوقيع الأحدث والاكتفاء بالأقدم الأضعف. والدرس العملي أن أي خطوة في خط البناء Pipeline تعدل الملف، مثل إضافة ملف إعداد أو أداة حماية تعيد تغليفه، يجب أن تأتي قبل التوقيع لا بعده.
التحديث Update بمفتاح مختلف وبرقم إصدار versionCode أقدم
هذه هي الحالة التي حدثت لفريقنا يوم الخميس: نسخ الاختبار موقعة بمفتاح التصحيح، والنسخة الجديدة بمفتاح الرفع. نثبت أولاً ملفاً غير موقع، ثم نسخة موقعة بمفتاح التصحيح، ثم نحاول تثبيت نسخة مفتاح الرفع فوقها، ثم نحذف التطبيق ونثبتها من جديد:
adb install noR8-unsigned.apk
apksigner sign --ks ~/.android/debug.keystore --ks-pass pass:android \
--out orders-debugkey.apk app-release-unsigned.apk
adb install orders-debugkey.apk
adb install -r orders-1.apk
adb uninstall io.example.orders.native
adb install orders-1.apkadb: failed to install noR8-unsigned.apk: Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: Failed to collect certificates from /data/app/vmdl1416264317.tmp/base.apk: Attempt to get length of null array]
Success
adb: failed to install orders-1.apk: Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Existing package io.example.orders.native signatures do not match newer version; ignoring!]
Successالسطر الأول يبين أن Android لا يقبل ملفاً دون توقيع إطلاقاً، حتى للتجربة، وهذا سبب وجود مفتاح التصحيح أصلاً: Android Studio يوقع به كل بناء تصحيح تلقائياً. والسطر الثالث هو المهم: INSTALL_FAILED_UPDATE_INCOMPATIBLE مع العبارة signatures do not match، فالجهاز رفض التحديث لأن الشهادة مختلفة، ولم يعرض على المستخدم خياراً. والحل الوحيد كان حذف التطبيق، وهذا يعني حذف كل بياناته. ولذلك إذا وزعت نسخ اختبار موقعة بمفتاح التصحيح على عملاء حقيقيين، فكل واحد منهم سيضطر لحذف التطبيق وبياناته عندما ينتقل إلى نسخة المتجر، فوزع نسخ الاختبار من مسارات الاختبار في المتجر نفسه بدلاً من ذلك.
ثم نجرب رقم الإصدار versionCode. بنينا نسخة برقم 2 ثم حاولنا الرجوع إلى رقم 1:
Success
Success
versionCode=2 minSdk=26 targetSdk=36
adb: failed to install orders-1.apk: Failure [INSTALL_FAILED_VERSION_DOWNGRADE: Downgrade detected: Update version code 1 is older than current 2]تثبيت نفس الرقم 1 فوق نفسه نجح، ثم الرقم 2 نجح، أما الرجوع إلى 1 فرفضه الجهاز بالرسالة INSTALL_FAILED_VERSION_DOWNGRADE. والمتجر أشد من الجهاز، فهو لا يقبل أصلاً رفع حزمة برقم سبق استخدامه، كما تشرح صفحة إصدارات التطبيق. وهذا يعني أن الرجوع عن إصدار سيئ في المتجر لا يكون بإعادة رفع النسخة القديمة، وإنما ببناء النسخة القديمة من جديد برقم أعلى، أو بإيقاف الإطلاق التدريجي قبل أن يصل إلى الجميع، وقد شرحنا ذلك في قسم الإطلاق التدريجي Staged Rollout من مقال تقارير الأعطال.
تدوير المفتاح Key Rotation بسلسلة النسب Lineage
لنفرض الآن أن الفريق قرر بعد سنة أن ينتقل إلى مفتاح جديد، ربما لأن الموظف الذي أنشأ المفتاح القديم غادر، أو لأن الشركة تريد مفتاحاً من نوع EC أقصر وأحدث. لو وقعنا الإصدار 3 بالمفتاح الجديد مباشرة فسوف يحدث ما رأيناه قبل قليل. والطريقة الصحيحة أن نصنع سلسلة نسب Lineage يوقع فيها المفتاح القديم على الجديد، ثم نوقع الإصدار بالمفتاحين:
apksigner rotate --out lineage.bin \
--old-signer --ks upload.p12 --ks-pass pass:lab-pass-123 \
--new-signer --ks new.p12 --ks-pass pass:lab-pass-123
apksigner lineage --in lineage.bin --print-certs
# the new key alone
apksigner sign --ks new.p12 --ks-pass pass:lab-pass-123 --out orders-3-nolineage.apk v3-unsigned.apk
adb install -r orders-3-nolineage.apk
# both keys and the lineage
apksigner sign --ks upload.p12 --ks-pass pass:lab-pass-123 \
--next-signer --ks new.p12 --ks-pass pass:lab-pass-123 \
--lineage lineage.bin --out orders-3.apk v3-unsigned.apk
apksigner verify -v --print-certs orders-3.apk
adb install -r orders-3.apkSigner #1 in lineage certificate DN: CN=Orders Upload, O=Example
Signer #1 in lineage certificate SHA-256 digest: 2a161a340be059feff9d0fc89865f2eac8a9958aa9259c86bc30bd9cf03e51b3
Signer #2 in lineage certificate DN: CN=Orders 2027, O=Example
Signer #2 in lineage certificate SHA-256 digest: 6a0255f9c473394063a4327bef8dc1b81247b40e2235f893f4e9639aaea0eec7
-- v3 signed with the NEW key only, no lineage
adb: failed to install orders-3-nolineage.apk: Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Existing package io.example.orders.native signatures do not match newer version; ignoring!]
-- v3 signed with both keys and the lineage
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): true
V3.1 Signer: (minSdkVersion=33, maxSdkVersion=2147483647) certificate SHA-256 digest: 6a0255f9c473394063a4327bef8dc1b81247b40e2235f893f4e9639aaea0eec7
V3.0 Signer: (minSdkVersion=24, maxSdkVersion=32) certificate SHA-256 digest: 2a161a340be059feff9d0fc89865f2eac8a9958aa9259c86bc30bd9cf03e51b3
Success
versionCode=3 minSdk=26 targetSdk=36
signatures=PackageSignatures{ee88945 version:4, signatures:[1a941d14], past signatures:[e55f6d02 flags: 17, 1a941d14 flags: 17]}في المخرج أعلاه لاحظ التالي:
- السلسلة فيها موقعان بالترتيب: القديم ثم الجديد، وهذا ما يسميه التوثيق إثبات التدوير.
- الإصدار الموقع بالمفتاح الجديد وحده رفضه الجهاز بنفس رسالة اختلاف التوقيع، فالمفتاح الجديد وحده لا يعني شيئاً للجهاز.
- الإصدار الموقع مع السلسلة يحمل كتلتين: كتلة v3.1 بالمفتاح الجديد للأجهزة من SDK 33 فما فوق، وكتلة v3.0 بالمفتاح القديم للأجهزة من 24 إلى 32. وهذا السلوك هو ما يصفه توثيق v3.1: الأجهزة من Android 13 تستخدم المفتاح الجديد، والأقدم تتجاهله وتبقى على القديم، لأن التوثيق نفسه لا ينصح بالتدوير على Android 12 وما قبله.
- المحاكي يعمل بنظام Android 15، أي SDK 35، فقبل التحديث، وظهر في
dumpsysأن للتطبيق توقيعاً حالياً وتوقيعات سابقة past signatures تضم المفتاحين.

ولاحظ أن هذا التدوير يخص مفتاح التطبيق الذي يراه الجوال، فإذا كنت تستخدم Play App Signing فمفتاح التطبيق عند Google، والتدوير يتم من Play Console لا من جهازك، أما مفتاح الرفع فتغييره أسهل بكثير لأنه مجرد طلب إعادة تعيين. والتجربة هنا تبين لك ما يحدث داخل الملف عندما تطلب ذلك من المتجر.
حزمة التطبيق App Bundle وما يصل إلى كل جهاز
Google Play لا يقبل من التطبيقات الجديدة ملف APK، وإنما حزمة AAB نوقعها بمفتاح الرفع بالأداة jarsigner، ثم نستخدم bundletool لنرى ما سيفعله المتجر بها. وعرفنا للأداة جهازاً بمعالج arm64 ولغة عربية وكثافة شاشة 420 وSDK 35:
jarsigner -keystore upload.p12 -storepass lab-pass-123 \
-signedjar orders.aab app-release.aab upload
cat > pixel.json <<'EOF'
{ "supportedAbis": ["arm64-v8a"], "supportedLocales": ["ar"], "screenDensity": 420, "sdkVersion": 35 }
EOF
java -jar bundletool.jar build-apks --bundle=orders.aab --output=orders.apks \
--ks=upload.p12 --ks-pass=pass:lab-pass-123 --ks-key-alias=upload --key-pass=pass:lab-pass-123
java -jar bundletool.jar get-size total --apks=orders.apks
java -jar bundletool.jar get-size total --apks=orders.apks --device-spec=pixel.json
java -jar bundletool.jar extract-apks --apks=orders.apks --device-spec=pixel.json --output-dir=pixel-rw-r--r-- 1 wajdy 197609 2247031 Oct 30 22:26 app-release.aab
jar signed.
Warning:
The signer's certificate is self-signed.
MIN,MAX
776254,782440
MIN,MAX
781753,781753
-rw-r--r-- 1 wajdy 197609 12435 Oct 30 22:35 base-ar_3.apk
-rw-r--r-- 1 wajdy 197609 37039 Oct 30 22:35 base-arm64_v8a_3.apk
-rw-r--r-- 1 wajdy 197609 1526473 Oct 30 22:35 base-master_3.apk
total apks: 238وهذا أهم مخرج في التجربة كلها لمن يهتم بحجم التطبيق، فلنقرأه سطراً سطراً:
- الحزمة نفسها 2,247,031 بايت، وهي أكبر من ملف APK الذي وقعناه سابقاً، لأنها تحمل كل شيء: المكتبات الأصلية لأربعة معالجات، والموارد لكل الكثافات، والترجمات لكل اللغات.
- تحذير
self-signedمنjarsignerطبيعي، وسببه نفس ما ذكرناه: لا توجد جهة إصدار في توقيع Android. - السطر
MIN,MAXالأول هو حجم التنزيل المقدر لأي جهاز ممكن، بين 776,254 و782,440 بايت، والثاني لجهازنا المحدد 781,753 بايت. وهذا الرقم حجم التنزيل المضغوط Download Size، وليس الحجم على القرص. - الأداة صنعت 238 ملف APK داخل ملف
orders.apks: ملف أساسي master، وملف لكل معالج ABI، وملف لكل لغة من نحو 85 لغة، وبعضها بأكثر من نسخة لنطاقات مختلفة من إصدارات Android، ولذلك تحمل الأسماء لاحقة مثل_3. - الجهاز العربي بمعالج arm64 أخذ ثلاثة ملفات فقط: الأساسي بحجم 1.5 MB، وملف اللغة العربية 12 KB، وملف المعالج 37 KB. وعندما فتحنا ملف المعالج وجدنا فيه مكتبة واحدة هي
libandroidx.graphics.path.soبحجم 10,096 بايت تأتي مع Compose، والباقي من حجم الملف هو ملف Manifest الخاص به وبيانات التوقيع والمحاذاة.

وماذا يحدث لو لم تستخدم الحزمة ووزعت ملف APK واحداً؟ سوف يحمل كل مستخدم مكتبات أربعة معالجات وترجمات كل اللغات. وفي تطبيقنا الصغير الفرق صغير، أما في تطبيقات Flutter وReact Native فالمكتبات الأصلية هي أغلب الحجم، وقد رأينا في مقال اختيار التقنية أن ملف APK الشامل لنسخة React Native كان 55 MB لأنه يحمل مكتبات أربعة معالجات، بينما كل جوال يحتاج مكتبات معالج واحد فقط. وإذا كان تطبيقك لا يدعم إلا لغتين، فيمكنك أيضاً تقليل ملفات اللغات من البداية بإعداد اللغات المدعومة في التطبيق بدلاً من أن ترث 85 لغة من المكتبات.
محاذاة الصفحات 16 KB Page Size في مكتبات Flutter وReact Native
هناك متطلب جديد يمس كل تطبيق فيه كود أصلي Native Code، وهذا يشمل كل تطبيقات Flutter وReact Native تقريباً: بعض أجهزة Android الحديثة تعمل بحجم صفحة ذاكرة 16 KB بدلاً من 4 KB، والمكتبات الأصلية يجب أن تكون مبنية ومصفوفة لذلك. وتقول صفحة دعم أحجام الصفحات 16 KB إن كل التطبيقات التي تستهدف Android 15 فما فوق يجب أن تدعم صفحات 16 KB على الأجهزة 64-bit في Google Play، وإنه بدءاً من 1 فبراير 2027 لن تتمكن من نشر تحديثات لا تدعمها. فحصنا ملفي الإصدار من نسختي Flutter وReact Native لنفس التطبيق:
zipalign -c -P 16 -v 4 app-arm64-v8a-release.apk | grep -E "\.so|Verification" 245760 lib/arm64-v8a/libapp.so (OK)
3538944 lib/arm64-v8a/libflutter.so (OK)
Verification successfulوالملف الأول من Flutter فيه مكتبتان: libflutter.so وهي المحرك نفسه، وlibapp.so وهي كود Dart الخاص بتطبيقنا بعد ترجمته مسبقاً. أما ملف React Native فمكتباته أكثر:
zipalign -c -P 16 -v 4 app-release.apk | grep -E "arm64.*\.so|Verification" 4210688 lib/arm64-v8a/libappmodules.so (OK)
4358144 lib/arm64-v8a/libc++_shared.so (OK)
5652480 lib/arm64-v8a/libfbjni.so (OK)
5832704 lib/arm64-v8a/libhermestooling.so (OK)
5980160 lib/arm64-v8a/libhermesvm.so (OK)
8699904 lib/arm64-v8a/libimagepipeline.so (OK)
8716288 lib/arm64-v8a/libjsi.so (OK)
9142272 lib/arm64-v8a/libnative-filters.so (OK)
9420800 lib/arm64-v8a/libnative-imagetranscoder.so (OK)
10010624 lib/arm64-v8a/libreact_codegen_safeareacontext.so (OK)
10174464 lib/arm64-v8a/libreactnative.so (OK)
Verification successfulالرقم في بداية كل سطر هو موضع المكتبة داخل الملف، وكلها مضاعفات 16384، أي مصفوفة على حدود 16 KB، والأداة كتبت OK لكل مكتبة. وهذا يعني أن الإصدارات الحديثة من الإطارين تبني بالشكل الصحيح. ولكن هذا الفحص يتحقق من الموضع داخل الملف فقط، أما المكتبة نفسها فيجب أن تكون مبنية بمحاذاة 16 KB لأقسامها، وهذا يعتمد على إصدار NDK الذي بنيت به، والمشكلة الحقيقية تظهر عادة في مكتبة قديمة من طرف ثالث لم يحدثها صاحبها، فافحص الملف النهائي بعد إضافة كل مكتبة أصلية جديدة، وPlay Console يعرض تحذيراً عند الرفع إذا وجد مكتبة غير متوافقة.
الصلاحيات Permissions التي لم تطلبها: الملف المدمج Merged Manifest
آخر فحص قبل الرفع هو ما يقرؤه المتجر من الملف. أخذنا تطبيق دورة الحياة من هذه المرحلة، وملف AndroidManifest.xml الذي كتبناه فيه لا يطلب أي صلاحية، ثم قرأنا الصلاحيات من الملف النهائي بالأداة aapt2:
grep -c "uses-permission" app/src/main/AndroidManifest.xml
aapt2 dump permissions app-debug.apk0
package: io.example.life
uses-permission: name='android.permission.WAKE_LOCK'
uses-permission: name='android.permission.ACCESS_NETWORK_STATE'
uses-permission: name='android.permission.RECEIVE_BOOT_COMPLETED'
uses-permission: name='android.permission.FOREGROUND_SERVICE'
permission: io.example.life.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION
uses-permission: name='io.example.life.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION'صفر صلاحيات في ملفنا، وأربع صلاحيات نظام في الملف النهائي. والسبب أن أداة البناء تدمج ملف Manifest الخاص بكل مكتبة في ملفك، ومكتبة WorkManager تحتاج هذه الصلاحيات لتعمل: إبقاء المعالج مستيقظاً أثناء المهمة، ومعرفة حالة الشبكة لقيد الاتصال، وإعادة جدولة المهام بعد إعادة تشغيل الجهاز، وتشغيل خدمة في المقدمة للمهام الطويلة. وهذه الصلاحيات ليست خطيرة، ولكن المراجع والمستخدم يرونها، ونموذج أمان البيانات في Play Console يجب أن يطابق سلوك التطبيق الفعلي بما فيه المكتبات. والقاعدة: اقرأ الصلاحيات من الملف النهائي لا من ملفك، واحذف ما لا تحتاجه بالوسم tools:node="remove" بعد أن تتأكد أن المكتبة لا تحتاجه فعلاً.
وبنفس الأداة نرى ما يعرضه المتجر عن التطبيق:
package: name='io.example.orders.native' versionCode='1' versionName='1.0' platformBuildVersionName='17' platformBuildVersionCode='37' compileSdkVersion='37' compileSdkVersionCodename='17'
targetSdkVersion:'36'
application-label:'طلباتي'
(+ 85 application-label-<locale> lines, one per locale the libraries ship resources for)
supports-screens: 'small' 'normal' 'large' 'xlarge'
native-code: 'arm64-v8a' 'armeabi-v7a' 'x86' 'x86_64'لاحظ targetSdkVersion:'36'، وهو الرقم الذي يحدد هل يقبل المتجر الرفع أصلاً كما سنرى في القسم التالي، وcompileSdkVersion='37' الذي اضطررنا إليه لأن إصدار Compose الحالي يطلب ذلك. ولاحظ السطر الذي اختصرناه: 85 اسماً مترجماً للتطبيق، كلها طلباتي، لأن المكتبات تحمل ترجمات لهذه اللغات، وهذا ما رأيناه في ملفات اللغات داخل الحزمة. والسطر الأخير يبين أن الملف الشامل يحمل مكتبات أربعة معالجات، وهذا ما تحله الحزمة.
ما الذي تطلبه Google Play في 2026؟ متطلبات النشر Publishing Requirements
بعد أن أصبح لدى فريقنا حزمة موقعة وصحيحة، يأتي دور Play Console. وقبل الرفع هناك أربعة متطلبات تمنع النشر إذا لم تستوفها، وكلها تتغير كل سنة تقريباً، ولذلك نقتبس كل واحد منها من صفحته الرسمية بتاريخ قراءتنا لها في أكتوبر 2026، وعليك أن تراجع الصفحة نفسها قبل أن ترفع.
مستوى API المستهدف Target API Level
تقول صفحة متطلبات مستوى API المستهدف إنه بدءاً من 31 أغسطس 2026 يجب أن تستهدف التطبيقات الجديدة وتحديثات التطبيقات Android 16، أي API 36 أو أعلى، لكي تقبل في Google Play، مع استثناءات لتطبيقات Wear OS وAndroid Automotive التي تكفيها API 35، وتطبيقات التلفاز وAndroid XR التي تكفيها API 34. أما التطبيقات الموجودة التي لا تحدث، فيجب أن تستهدف API 35 على الأقل لتبقى متاحة للمستخدمين الجدد على الأجهزة التي تعمل بنظام أحدث من مستواها المستهدف.
ولماذا تفرض Google ذلك؟ لأن رقم targetSdkVersion هو الذي يقول للنظام أي قواعد سلوك يطبقها على التطبيق. فالتطبيق الذي يستهدف إصداراً قديماً يعمل على جوال حديث بقواعد الإصدار القديم في أشياء مثل الصلاحيات وعمل الخلفية، وهذا ما تريد المنصة أن توقفه. والأثر العملي على فريقنا أن رفع targetSdk ليس تعديل رقم فقط، فكل رفع يغير سلوكاً، مثل قيود الخدمات في الخلفية التي جربناها في مقال دورة الحياة من هذه المرحلة، ولذلك اختبر التطبيق بعد رفع الرقم لا قبله. ولاحظ الفرق بين الأرقام الثلاثة: minSdk أقدم نظام يعمل عليه التطبيق، وtargetSdk النظام الذي اختبرت سلوكه، وcompileSdk الواجهات التي يرى الكود أنها موجودة، وفي تجربتنا كانت 26 و36 و37 بالترتيب.
الاختبار المغلق Closed Testing للحسابات الشخصية
هذا المتطلب يفاجئ المطورين المستقلين والفرق الصغيرة أكثر من غيره. تقول صفحة متطلبات الاختبار للحسابات الشخصية إن المطورين الذين أنشأوا حسابات شخصية بعد 13 نوفمبر 2023 يجب أن يشغلوا اختباراً مغلقاً لتطبيقهم مع 12 مختبراً على الأقل مشتركين فيه باستمرار لمدة 14 يوماً على الأقل، وبعدها فقط يمكنهم التقدم بطلب الوصول إلى الإنتاج Production من لوحة Play Console، ويجيبون في الطلب على أسئلة عن التطبيق وطريقة اختباره.
وهذا يعني لفريقنا الذي أنشأ حساباً شخصياً قبل أسبوع أن موعد الأحد مستحيل على Android مهما كان التطبيق جاهزاً، فأقرب موعد بعد أسبوعين من بدء الاختبار المغلق، ثم وقت مراجعة طلب الإنتاج. والحل الذي كان يجب أن يحدث من البداية واحد من اثنين: حساب منظمة Organization Account، لأن هذا المتطلب مكتوب للحسابات الشخصية كما تقول الصفحة، مع أن حساب المنظمة يحتاج وثائق الشركة ووقتاً للتحقق منها، أو بدء الاختبار المغلق مع أول نسخة قابلة للاستخدام لا مع النسخة الأخيرة.
قسم أمان البيانات Data Safety وحذف الحساب Account Deletion
كل تطبيق في Google Play يجب أن يملأ نموذج أمان البيانات Data Safety، وهو ما يظهر للمستخدم في صفحة التطبيق: ما البيانات التي يجمعها، وهل يشاركها، وهل هي مشفرة أثناء النقل، وهل يمكن طلب حذفها. والنموذج يجب أن يشمل ما تجمعه المكتبات داخل تطبيقك، مثل أداة تقارير الأعطال التي تجمع معرف الجهاز، أو مكتبة التحليلات، وهذا سبب فحصنا للملف المدمج قبل قليل.
وإذا كان تطبيقك يسمح بإنشاء حساب، فصفحة متطلبات حذف الحساب تطلب أمرين: مساراً داخل التطبيق يحذف به المستخدم حسابه والبيانات المرتبطة به، ورابط ويب يطلب منه حذف الحساب والبيانات دون أن يحتاج إلى تثبيت التطبيق. وتطبيق الطلبات فيه حساب وعنوان توصيل وسجل طلبات، ولذلك يحتاج الفريق صفحة ويب صغيرة لطلب الحذف وزراً في شاشة الإعدادات، ويحتاج قبل ذلك قراراً قانونياً: ما الذي يحذف فعلاً وما الذي تحتفظ به الشركة لأغراض محاسبية، وهذا ما يجب أن يكتب في سياسة الخصوصية.
التحقق من هوية المطور Developer Verification
وهناك تغيير أكبر بدأ هذه السنة ويمس التوزيع خارج Google Play أيضاً. تقول صفحة التحقق من هوية مطوري Android إن Android أطلق برنامجاً يتحقق من هوية المطورين لحماية المستخدمين من البرمجيات الخبيثة والاحتيال، وإن Google Play يسجل 99% من التطبيقات تلقائياً، والجدول الزمني في الصفحة كما يلي: في أغسطس 2026 أطلقت الواجهات البرمجية وحسابات التوزيع المحدود ومسار المستخدمين المتقدمين، وفي 30 سبتمبر 2026 بدأت الحماية لكل المستخدمين الذين يثبتون تطبيقات من المتاجر المشاركة في البرازيل وإندونيسيا وسنغافورة وتايلاند على الأجهزة المعتمدة بنظام Android 7 فما فوق، وفي 2027 تتوسع لكل التطبيقات على الأجهزة المعتمدة في العالم. وإذا كان تطبيقك يوزع على Google Play فقط فغالباً لا تحتاج شيئاً إضافياً، أما إذا كان الفريق يوزع ملف APK مباشرة لعملاء من الشركات أو من موقعه، فعليه أن يسجل في Android Developer Console قبل أن يصل التطبيق إلى أجهزة لا تثبت إلا تطبيقات المطورين الموثقين.
المراجعة في Google Play Review والإطلاق التدريجي
لا تفترض أن مراجعة Google تنتهي في ساعات لأن تحديثاتك السابقة انتهت كذلك، فتقول صفحة حالة النشر إن بعض التطبيقات قد تخضع لمراجعات ممتدة قد تصل إلى سبعة أيام أو أكثر في حالات استثنائية. وبعد القبول لا تنشر الإصدار لكل المستخدمين مرة واحدة، وإنما بالإطلاق التدريجي Staged Rollout الذي تشرحه صفحة الإطلاق التدريجي، فتبدأ بنسبة صغيرة وتراقب الأعطال ثم ترفع النسبة، وتوقف الإطلاق إذا ظهرت مشكلة. ولن نكرر هنا كيف تقرأ أرقام الأعطال أثناء الإطلاق، فقد شرحناه في مقال تقارير الأعطال.
مراجعة Apple App Review: ما الذي تقرؤه الإرشادات Guidelines فعلاً؟
في Apple يراجع كل إصدار إنسان، والمرجع هو إرشادات مراجعة App Store، وقد كان آخر تحديث لها حين قرأناها في 16 أكتوبر 2026. والإرشادات مقسمة إلى خمسة أقسام: السلامة Safety، والأداء Performance، والأعمال Business، والتصميم Design، والقانوني Legal. وقبل أن نقرأ أهمها لفريقنا، لننظر إلى الأرقام لنعرف أين يسقط أغلب الناس.
أرقام المراجعة في تقرير الشفافية Transparency Report
تنشر Apple كل سنة تقرير شفافية App Store، وفي تقرير 2024، وهو آخر تقرير وجدناه منشوراً، راجعت Apple 7,771,599 طلباً، ورفضت منها 1,931,400، أي نحو الربع، ثم قبلت 295,109 طلباً بعد رفضها، أي بعد أن أصلحها أصحابها وأعادوا إرسالها. وتوزيع الرفض على الأقسام يقول الكثير: قسم الأداء Performance وحده 1,235,471، ثم القانوني 445,696، ثم التصميم 378,300، ثم الأعمال 209,845، ثم السلامة 116,105. ولاحظ أن مجموع الأقسام أكبر من عدد الطلبات المرفوضة، لأن التقرير ينبه إلى أن الطلب الواحد قد يرفض لأكثر من إرشاد.

وقسم الأداء يبدأ بالإرشاد 2.1 اكتمال التطبيق App Completeness، وتقول صفحة App Review نفسها إن أكثر من 40% من المشكلات غير المحلولة في المتوسط مرتبطة بالإرشاد 2.1، الذي يشمل الأعطال والمحتوى المؤقت والمعلومات الناقصة. وتقول الصفحة أيضاً إن فريق المراجعة يراجع عادة 50% على الأقل من الطلبات في أقل من 24 ساعة و90% في أقل من 48 ساعة، فالرفض لا يكلفك أسابيع في الغالب، ولكن كل دورة رفض وإصلاح وإعادة إرسال تأكل يومين أو ثلاثة من موعدك.
الإرشادات التي توقف التطبيقات الجديدة Guidelines
لن نقرأ الإرشادات كلها، وإنما ما يمس تطبيق طلبات من فريق صغير، مع نص كل إرشاد بالإنجليزية كما هو ثم معناه العملي:
- اكتمال التطبيق 2.1: تطلب الفقرة (a) أن تكون الطلبات «final versions with all necessary metadata and fully functional URLs»، وأن يزال النص المؤقت والمواقع الفارغة، وأن ترسل «demo account info (and turn on your back-end service!)» إذا كان التطبيق يحتاج تسجيل دخول. وهذا بالضبط ما رفض به تطبيق فريقنا في المرة الأولى: المراجع لم يستطع تجاوز شاشة تسجيل الدخول برقم الجوال، لأن التطبيق يرسل رمز تحقق برسالة نصية إلى رقم لا يملكه المراجع. والحل حساب تجريبي برقم ثابت يقبل رمزاً ثابتاً، أو وضع تجريبي داخل التطبيق كما تسمح الفقرة نفسها، وسيرفر الاختبار يجب أن يكون شغالاً طوال فترة المراجعة.
- دقة البيانات الوصفية 2.3: يجب أن تعكس الصور والوصف «the app’s core experience»، والإرشاد 2.3.3 يطلب أن تظهر لقطات الشاشة التطبيق وهو يعمل، لا شاشة الشعار ولا شاشة تسجيل الدخول فقط، والإرشاد 2.3.7 يحدد اسم التطبيق بثلاثين حرفاً ويمنع حشو الكلمات المفتاحية بأسماء تطبيقات أخرى.
- الحد الأدنى من الوظائف 4.2: «Your app should include features, content, and UI that elevate it beyond a repackaged website». والتطبيق الذي يغلف موقع المتجر داخل WebView فقط يرفض غالباً بهذا الإرشاد، وهذا من أهم الأسباب التي تجعل بعض الفرق تختار بناء واجهة أصلية أو متعددة المنصات بدلاً من الغلاف.
- الرسائل المزعجة Spam 4.3: «Don’t create multiple Bundle IDs of the same app». فإذا أراد العميل تطبيقاً لكل فرع أو لكل مدينة، فالإرشاد يطلب تطبيقاً واحداً يختار فيه المستخدم الفرع.
- سياسة الخصوصية 5.1.1(i): «All apps must include a link to their privacy policy in the App Store Connect metadata field and within the app in an easily accessible manner». الرابط مطلوب في مكانين، وصفحة App Review تذكر أن الروابط المعطلة، بما فيها رابط الدعم وسياسة الخصوصية، من أسباب الرفض الشائعة.
- تسجيل الدخول وحذف الحساب 5.1.1(v): «If your app supports account creation, you must also offer account deletion within the app». وتشرح Apple التفاصيل في صفحة حذف الحساب داخل التطبيق. ولاحظ أن Google تطلب الشيء نفسه مع رابط ويب إضافي، فابن الميزة مرة واحدة للمنصتين.
- خدمات تسجيل الدخول 4.8: إذا استخدمت تسجيل دخول من طرف ثالث مثل Google أو Facebook للحساب الأساسي، فيجب أن تقدم خياراً مكافئاً يحصر البيانات بالاسم والبريد، ويسمح بإخفاء البريد، ولا يجمع التفاعلات للإعلان دون موافقة. وتسجيل الدخول برقم الجوال وحده لا يدخل في هذا الإرشاد لأنه ليس خدمة طرف ثالث.
- الشراء داخل التطبيق 3.1.1 و3.1.3(e): فتح ميزات رقمية داخل التطبيق يجب أن يمر بالشراء داخل التطبيق In-App Purchase، أما بيع السلع المادية والخدمات التي تستهلك خارج التطبيق فيجب أن يستخدم طرق دفع أخرى مثل Apple Pay أو البطاقة، وهذه حالة تطبيق الطلبات. والخلط بين الحالتين في الاتجاهين سبب رفض معروف، وقد شرحنا الشراء داخل التطبيق في مقال الروابط العميقة والإشعارات من هذه المرحلة.
- تنزيل الكود 2.5.2: لا يجوز للتطبيق أن ينزل كوداً «which introduces or changes features or functionality of the app». وهذا يمس أدوات التحديث عبر الهواء Over-the-Air في React Native، فتحديث نص أو إصلاح خطأ شيء، وإضافة ميزة جديدة دون مراجعة شيء آخر يخالف الإرشاد.
الخصوصية Privacy: ملصق البيانات Privacy Label وملف Privacy Manifest
عند Apple طبقتان للخصوصية غير سياسة الخصوصية نفسها. الأولى تفاصيل الخصوصية في صفحة التطبيق، وهي أسئلة تجيب عنها في App Store Connect عن البيانات التي تجمعها أنت والمكتبات، وتظهر للمستخدم قبل التنزيل، وهي المقابل المباشر لقسم أمان البيانات في Google Play. والثانية ملف Privacy Manifest باسم PrivacyInfo.xcprivacy داخل التطبيق وداخل كل مكتبة، ويسجل كما تقول الوثيقة أنواع البيانات التي يجمعها التطبيق أو المكتبة، والواجهات البرمجية التي تتطلب سبباً Required Reasons APIs مع سبب استخدام كل واحدة. وهنا يظهر الفرق بين المنصتين: في Android نكتشف صلاحيات المكتبات من الملف المدمج كما فعلنا، وفي iOS يجب أن تحمل كل مكتبة ملفها، ويجمع Xcode تقرير الخصوصية Privacy Report من الملفات كلها، فإذا كانت مكتبة قديمة لا تحمل ملفاً فسوف تجد نفسك تشرح استخدامها بدلاً عنها أو تستبدلها.
متطلبات الأدوات Xcode SDK والحد الأدنى للنظام
وكما في Google، تفرض Apple حداً أدنى لأدوات البناء. تقول صفحة المتطلبات القادمة إنه منذ 28 أبريل 2026 يجب أن تبنى التطبيقات المرفوعة إلى App Store Connect بإصدار Xcode 26 أو أحدث مع SDK الإصدار 26 من أنظمة Apple، وإنه منذ 9 سبتمبر 2026 يجب أن تستهدف تطبيقات iOS وiPadOS المرفوعة iOS 13 أو أحدث. والأثر على الفرق التي تستخدم Flutter أو React Native أن عليها تحديث الإطار نفسه ليبني مع Xcode الجديد، ولذلك لا تترك إصدار الإطار قديماً لسنة كاملة ثم تكتشف قبل الإطلاق أنه لا يبني مع الأداة المطلوبة.
قبل الإنتاج Production: TestFlight ومسارات الاختبار Testing Tracks
القاعدة التي تعلمها فريقنا يوم الخميس: لا توزع نسخة اختبار خارج المتجر. في Apple يوجد TestFlight لتوزيع النسخ التجريبية على مختبرين داخليين من فريقك وعلى مختبرين خارجيين بعد مراجعة خفيفة للنسخة التجريبية، وفي Google Play توجد مسارات الاختبار الداخلي Internal والمغلق Closed والمفتوح Open. والفائدة أكبر من مجرد التوزيع: النسخة التي يجربها المختبر هي نفس الحزمة الموقعة بنفس المفتاح والتي ستصل إلى الإنتاج، فلا توجد مفاجأة INSTALL_FAILED_UPDATE_INCOMPATIBLE التي رأيناها في التجربة، والمختبر الذي يثبت من المسار يحصل على التحديث التالي تلقائياً. وفي Android تحديداً، الاختبار المغلق ليس خياراً للحسابات الشخصية الجديدة كما رأينا، فاجعل المسار المغلق جزءاً من خطتك من الأسبوع الأول.
وأثناء الاختبار، استخدم أدوات الاختبار الآلي على النسخة التي ستنشرها، لا على نسخة التصحيح، وقد شرحنا تشغيل سيناريوهات الواجهة في مقال اختبار تطبيقات الجوال مع Appium وMaestro. والسبب ما رأيناه في قسم R8: نسخة الإصدار مختلفة في الكود نفسه، وعطل الانعكاس لا يظهر إلا فيها.
أسباب الرفض الشائعة Rejection Reasons وكيف تتجنبها
الجدول التالي يجمع أسباب الرفض التي يقع فيها تطبيق مثل تطبيقنا، مع المنصة والإرشاد أو المتطلب، والطريقة العملية لتجنبه قبل الإرسال:
| السبب | المنصة والمرجع | كيف تتجنبه |
|---|---|---|
| المراجع لا يستطيع تسجيل الدخول | Apple 2.1 | حساب تجريبي بكلمة مرور أو رمز ثابت في معلومات المراجعة، والسيرفر يعمل أثناء المراجعة |
| عطل أو شاشة فارغة عند المراجع | Apple 2.1، Google قبل الإنتاج | اختبر نسخة الإصدار نفسها على جهاز حقيقي وبأحدث نظام، لا نسخة التصحيح |
| نص مؤقت أو صور غير نهائية | Apple 2.1 و2.3 | راجع كل الشاشات والصور، ولقطات الشاشة من التطبيق وهو يعمل |
| رابط سياسة الخصوصية أو الدعم معطل | Apple 5.1.1(i)، Google Data Safety | رابط يعمل في المتجر وداخل التطبيق، وصفحة تشرح البيانات والحذف |
| لا يوجد حذف للحساب | Apple 5.1.1(v)، Google Account Deletion | زر حذف داخل التطبيق، ورابط ويب لطلب الحذف في Google |
| تسجيل دخول بطرف ثالث فقط | Apple 4.8 | أضف خياراً مكافئاً يحفظ الخصوصية، أو تسجيلاً بالبريد أو الجوال |
| غلاف لموقع الويب فقط | Apple 4.2 | وظائف أصلية حقيقية: إشعارات، عمل دون اتصال، واجهة مصممة للجوال |
| تطبيق لكل فرع أو عميل | Apple 4.3 | تطبيق واحد باختيار الفرع داخله |
| طريقة دفع خاطئة | Apple 3.1.1 و3.1.3(e) | السلع المادية بالبطاقة أو Apple Pay، والميزات الرقمية بالشراء داخل التطبيق |
| نموذج البيانات لا يطابق سلوك التطبيق | Google Data Safety، Apple Privacy Details | اقرأ الملف المدمج وملفات Privacy Manifest للمكتبات، واملأ النموذج منها |
| مستوى API مستهدف قديم | Google target API | targetSdk 36 للتحديثات والتطبيقات الجديدة منذ 31 أغسطس 2026 |
| مكتبة أصلية غير مصفوفة على 16 KB | Google page sizes | zipalign -c -P 16 -v 4 على الملف النهائي، وحدث المكتبة القديمة |
| رفع حزمة برقم إصدار مستخدم | Google versioning | رقم versionCode يزيد تلقائياً من نظام البناء |
| حزمة موقعة بمفتاح خاطئ | Google Play App Signing | تحقق من البصمة بالأمر keytool -list -v قبل الرفع |
أخطاء شائعة Common Mistakes
- ملف المفاتيح على جهاز مطور واحد دون نسخة احتياطية: إذا كنت لا تستخدم Play App Signing فهذا يعني أن التطبيق كله معلق بقرص واحد. احفظ الملف وكلمة المرور في مكانين منفصلين على الأقل، واحدهما خزنة أسرار.
- كتابة كلمة المرور في
build.gradle.ktsأو في ملف داخل المستودع: أي شخص يقرأ المستودع يستطيع توقيع تحديث باسمك إذا وصل إلى الملف. - توزيع نسخ تجريبية موقعة بمفتاح التصحيح على عملاء: رأينا أن الانتقال منها إلى نسخة المتجر يحتاج حذف التطبيق وبياناته.
- استخدام بصمة مفتاح الرفع في
assetlinks.jsonأو في إعدادات خدمات Google: الجوال يرى مفتاح التطبيق الذي تحتفظ به Google، فخذ البصمة من صفحة App Signing. - تعديل الملف بعد التوقيع في خط البناء: رأينا
DOES NOT VERIFYمن إضافة ملف واحد. - عدم حفظ ملف
mapping.txtمع كل إصدار: تقارير الأعطال من المستخدمين ستأتي بأسماء مختصرة لا يمكنك قراءتها. - تغيير مفتاح التوقيع مباشرة دون سلسلة نسب: الجهاز يرفض التحديث كما رأينا، والتدوير الصحيح يمر بالأمر
apksigner rotateأو بخيار التدوير في Play Console. - بدء الاختبار المغلق في الأسبوع الأخير: الحسابات الشخصية الجديدة تحتاج 14 يوماً على الأقل مع 12 مختبراً قبل طلب الإنتاج.
- افتراض أن الرفض نهاية الطريق: في 2024 قبلت Apple 295,109 طلباً بعد رفضها، فاقرأ رسالة الرفض بدقة، وأجب عن المراجع من App Store Connect إذا كان قد فهم شيئاً بشكل خاطئ.
قائمة تحقق Checklist قبل الإرسال إلى المتجرين
هذه القائمة رتبناها بالترتيب الزمني الذي كان يجب أن يتبعه فريقنا، من الحساب إلى زر الإرسال:
- الحسابات: حساب Apple Developer Program وحساب Google Play، ويفضل حساب منظمة، وابدأ التحقق قبل الإطلاق بأسابيع لا بأيام.
- الاختبار المغلق في Google Play بدأ قبل 14 يوماً على الأقل مع 12 مختبراً إذا كان الحساب شخصياً.
- مفتاح الرفع بصيغة PKCS12 وصلاحية 25 سنة أو أكثر، مع نسختين احتياطيتين، وكلمة المرور في خزنة أسرار.
- Play App Signing مفعل، وبصمة مفتاح التطبيق مسجلة في كل خدمة تحتاجها.
- في iOS: شهادة Apple Distribution صالحة، والملف التعريفي يحمل كل القدرات التي يستخدمها التطبيق.
targetSdk36، والتطبيق مختبر بعد رفع الرقم، والبناء في iOS بإصدار Xcode 26 أو أحدث.- R8 مفعل، وملف
mapping.txtمحفوظ ومرفوع إلى أداة تقارير الأعطال. apksigner verify --print-certsيعطي البصمة المتوقعة، وzipalign -c -P 16 -v 4ينجح على الملف النهائي.- الصلاحيات في الملف المدمج مقروءة ومفهومة، ونموذج أمان البيانات وتفاصيل الخصوصية مملوءة منها، وكل مكتبة iOS تحمل ملف Privacy Manifest.
- سياسة الخصوصية ورابط الدعم يعملان، داخل التطبيق وفي المتجر.
- حذف الحساب داخل التطبيق، ورابط ويب لطلب الحذف.
- حساب تجريبي للمراجعين مع شرح أي إعداد خاص، والسيرفر يعمل طوال فترة المراجعة.
- لقطات الشاشة من التطبيق وهو يعمل، والاسم ثلاثون حرفاً أو أقل، ولا يوجد نص مؤقت.
- خطة إطلاق تدريجي بنسبة صغيرة، ومن يراقب الأعطال في الساعات الأولى، ومتى نوقف الإطلاق.
الخلاصة Summary
وصلنا لنهاية الموضوع، وأهم ما فيه:
- التوقيع في Android يربط كل تحديث بمفتاح واحد، والجهاز رفض أمامنا التحديث بمفتاح مختلف بالرسالة
INSTALL_FAILED_UPDATE_INCOMPATIBLE، ورفض الرجوع إلى رقم إصدار أقدم بالرسالةINSTALL_FAILED_VERSION_DOWNGRADE. - Play App Signing يفصل مفتاح الرفع الذي يمكن إعادة تعيينه عن مفتاح التطبيق الذي تحتفظ به Google، وهو إلزامي للتطبيقات الجديدة منذ أغسطس 2021.
- R8 جعل تطبيق Compose الصغير 927,869 بايت بدلاً من 8,196,328، وثمنه ملف mapping تحفظه وكود انعكاس تختبره في نسخة الإصدار.
- التوقيع v2 وما بعده يحمي كل بايت، وإضافة ملف واحد بعد التوقيع أعطت
DOES NOT VERIFY. - تدوير المفتاح يحتاج سلسلة نسب يوقع فيها القديم على الجديد، والجهاز بنظام Android 15 استخدم المفتاح الجديد من كتلة v3.1، والأجهزة الأقدم من Android 13 تبقى على القديم.
- حزمة AAB بحجم 2.2 MB أصبحت لجوال عربي بمعالج arm64 ثلاثة ملفات بحجم تنزيل مقدر 781,753 بايت.
- Google Play في 2026: targetSdk 36 منذ 31 أغسطس، واختبار مغلق 14 يوماً مع 12 مختبراً للحسابات الشخصية الجديدة، وصفحات 16 KB، والتحقق من هوية المطور يبدأ في دول محددة ويتوسع في 2027.
- Apple رفضت في 2024 نحو ربع الطلبات، وقسم الأداء الذي يضم اكتمال التطبيق هو الأكبر، وأغلب أسبابه تمنع بقائمة تحقق قبل الإرسال.
ومقالات هذه المرحلة الأخرى، ومعها مقالات الجوال المنشورة:
دليلتطبيق الجوال: أصيل Native أم Flutter أم React Native؟بنينا نفس تطبيق الطلبات ثلاث مرات بلغة Kotlin وبإطاري Flutter وReact Native، وقسنا الحجم وزمن البناء والإقلاع والذاكرة لنعرف من أين يأتي كل فرق وكيف تختار.
دليلدورة حياة تطبيق الجوال Lifecycle والمهام الخلفيةلماذا يجد المستخدم السلة فارغة بعد مكالمة، وما الذي يبقى فعلاً بعد تدوير الشاشة وموت العملية والسبات Doze، بتجربة حقيقية على Android وشرح ما يقابلها في iOS.
دليلتطبيق يعمل دون إنترنت Offline-first: التخزين والمزامنةنبني سيرفراً وجوالين بقواعد SQLite ونرى بأرقام حقيقية كيف يضيع التسليم أو يسجل المبلغ مرتين أو يعود الطلب المحذوف، وكيف يمنع ذلك Outbox ومفتاح منع التكرار ودمج الحقول.
دليلالروابط العميقة Deep Links والإشعارات والشراء داخل التطبيقلماذا يفتح رابط الطلب المتصفح بدلاً من تطبيقك، وكيف يثبت نطاقك ملكيته للتطبيق في Android وiOS، وكيف يفتح الإشعار الشاشة الصحيحة، ومتى يلزمك الشراء داخل التطبيق.
دليلتقارير الأعطال Crash Reporting في تطبيقات الجوالكيف يصلك عطل حدث في جوال مستخدم لا تعرفه، وكيف تقرأ ال Stack Trace المشفر، ومتى توقف إصداراً جديداً قبل أن يصل إلى الجميع.
دليلتقارير الأعطال Crash Reports لتطبيق Android مع GlitchTipتطبيق Android يتعطل عمداً على المحاكي ويرسل أعطاله إلى GlitchTip على سيرفرك، وما الذي يعمل فعلاً عند رفع ملف ال Mapping وما الذي لا يعمل.
دليلاختبار تطبيقات الجوال مع Appium وMaestroكيف تختار أداة اختبار تطبيقات الجوال، وأين تشغل الاختبارات، ولماذا تفشل بعض الاختبارات عندما تصبح الواجهة عربية.
دليلأسئلة مقابلة مطور تطبيقات الجوال من المبتدئ إلى الخبيربنك أسئلة لمقابلة مطور تطبيقات الجوال على Android وiOS، لكل سؤال إجابة المبتدئ وإجابة الخبير، مع تمارين عملية شغلناها.وإذا كنت تستعد لمقابلة عمل، فراجع أسئلة رفض Apple للتطبيق والنشر الآمن دون تراجع في مقال أسئلة المقابلة.
سجل التحديثات
- أكتوبر 2026: كتابة المقال، وتنفيذ تجربة التوقيع والتغليف على تطبيق Kotlin وCompose بإصدار Android Gradle Plugin 9.4.1، مع keytool وjarsigner من JDK 21.0.5، وapksigner وzipalign وaapt2 من Build-Tools 37.0.0، وbundletool 1.18.3، على محاكي Android 15 (API 35). متطلبات المتجرين مقتبسة من صفحاتها الرسمية كما كانت في أكتوبر 2026، وإرشادات Apple بتحديث 16 أكتوبر 2026.