يقدم هذا الدرس التعليمي شرحًا مفصلاً وموسعًا لعملية بناء تطبيق أندرويد أصلي (NDK) باستخدام مجموعة من أدوات سطر الأوامر، بدلاً من الاعتماد على بيئات التطوير المتكاملة مثل Android Studio. يستند الشرح على تحليل السكربت (Bash script) الذي قدمته، مع إضافة تفاصيل إضافية حول كل خطوة، أمثلة عملية، وشرح للخيارات الشائعة للأدوات، مما يجعله دليلاً عمليًا أكثر شمولاً لفهم العملية اليدوية. سنغطي أيضًا بعض النصائح لتصحيح الأخطاء الشائعة وتحسين الأداء.
على الرغم من أن أدوات مثل Android Studio و Gradle تبسط عملية البناء إلى حد كبير، فإن فهم العملية اليدوية يوفر رؤية أعمق لمكونات نظام أندرويد وكيفية تفاعلها. هذا الفهم ضروري لتصحيح الأخطاء المعقدة، تحسين أداء البناء، وتخصيص العملية لتناسب احتياجات متقدمة. على سبيل المثال، في مشاريع كبيرة، قد تحتاج إلى بناء مخصص لمعماريات معينة مثل ARM أو x86، أو دمج مكتبات خارجية دون الاعتماد على Gradle. بالإضافة إلى ذلك، يساعد هذا النهج في فهم كيفية عمل أدوات مثل ndk-build (التي هي في جوهرها غلاف حول أدوات مثل clang++) و CMake. إذا كنت تواجه مشكلات في البناء الآلي، مثل أخطاء الربط (linker errors) الغامضة، فإن الرجوع إلى العملية اليدوية يمكن أن يكشف عن أسباب مثل مشاكل في التبعيات أو عدم توافق المعماريات.
pkg install clang aapt2 d8 zipalign keytool apksigner openjdk libglvnd-dev
ما هو Android NDK؟
الـ NDK (مجموعة تطوير التطبيقات الأصلية) هي مجموعة من الأدوات التي تسمح للمطورين باستخدام كود مكتوب بلغات C و C++ داخل تطبيقات أندرويد. يُستخدم هذا النهج غالبًا لتحقيق أداء أعلى في المهام الحاسوبية المكثفة مثل الألعاب، معالجة الصوت والصورة، ومحاكاة الفيزياء، أو لإعادة استخدام مكتبات برمجية مكتوبة بالفعل بلغة C/C++. على سبيل المثال، في تطبيقات الألعاب، يمكن استخدام NDK لكتابة محركات الرسومات باستخدام واجهات برمجة التطبيقات الرسومية الأصلية مثل OpenGL ES أو Vulkan، مما يوفر أداءً أفضل بكثير من Java. NDK يدعم أيضًا بناء تطبيقات كاملة أصلية دون Java، باستخدام NativeActivity، وهو ما يتطلب فهمًا لدورة حياة التطبيق (لایف سايكل) مثل onCreate و onPause على المستوى الأصلي.
أدوات البناء الأساسية: السكربت المقدم يستخدم مجموعة من الأدوات المستقلة الموجودة ضمن حزمة أدوات بناء أندرويد (Android SDK Build Tools) و NDK:
clang++: مترجم (Compiler) لغة C++ المستخدم لتحويل الكود الأصلي إلى مكتبات مشتركة (.so). هو جزء من NDK ويدعم خيارات مثل--targetلتحديد المعمارية (مثلaarch64-linux-androidلـ ARM64). يمكن تخصيصه بفلاغات مثل-O3لتحسين الأداء (optimization)، أو-gلإضافة معلومات التصحيح (debugging symbols)، أو-march=armv8-aلدعم ميزات معالجات محددة.aapt2: أداة أندرويد لتغليف الأصول (Android Asset Packaging Tool)، وتقوم بتجميع موارد التطبيق (مثل الواجهات، الصور، السلاسل النصية) وربطها في ملف APK. تتم عمليتها في مرحلتين:compileلتحويل الموارد إلى صيغة ثنائية (.flat)، وlinkلربطها معAndroidManifest.xml. هذا يحسن الأداء مقارنة بـaaptالقديم، حيث يسمح ببناء تدريجي (incremental build).d8: أداة تقوم بتحويل ملفات Java bytecode (.class) إلى صيغة Dalvik Executable (.dex) التي تعمل على نظام أندرويد. هي بديل أسرع وأكثر كفاءة لأداةdxالقديمة، وتدعم ميزات Java 8+ عبر عملية تسمى "desugaring" للتوافق مع إصدارات أندرويد القديمة. في حالة تجاوز حد 65 ألف دالة (multidex)، يمكن استخدام خيار--multi-dex.zipalign: أداة لتحسين ملفات APK عن طريق محاذاة البيانات غير المضغوطة، مما يقلل من استهلاك الذاكرة عند تشغيل التطبيق. يجب تنفيذها دائمًا قبل التوقيع، وتستخدم خيار-v 4لمحاذاة 4 بايت، مما يسرع الوصول إلى الموارد.keytool: أداة Java لإنشاء وإدارة شهادات الأمان ومفاتيح التوقيع (Keystore). تستخدم لإنشاء keystore باستخدام-genkeypair، مع تحديد الاسم المستعار (alias) وكلمة المرور. يجب الحفاظ على أمان هذا الملف، حيث أنه هويتك كمطور ويُستخدم لنشر تحديثات التطبيق في Google Play.apksigner: أداة لتوقيع ملفات APK باستخدام شهادة الأمان، وهي خطوة إلزامية لتثبيت التطبيق على الأجهزة. تدعم مخططات توقيع أحدث مثل v2 و v3 التي توفر أمانًا أعلى وحماية ضد التلاعب بالملف بعد توقيعه. يمكن التحقق من صحة التوقيع باستخدام الأمرapksigner verify.libglvnd-dev: مكتبة libglvnd-dev و ليس libglvnd اى الملفات الخاصة بتطويرها و ليس المكتبة نفسها نحتاج هذه المكتبة لربط egl, gles الخاصة بالنظام الاولى ستساعد فى تشغيل التطبيق و الثانيه ستتسبب فى خطأ not fond libEGL.so.1 أو ما شابه
دعنا الآن نحلل كل جزء من السكربت لفهم دوره في عملية البناء، مع إضافة أمثلة ونصائح للتعامل مع الأخطاء الشائعة.
# ---------- CONFIG ----------
APP_NAME="MyNDKApp"
P_DIR="$(pwd)"
ANDROID_JAR="$P_DIR/android.jar"
KEYSTORE="$P_DIR/mykey.keystore"
# ... (بقية المتغيرات)
يقوم هذا الجزء بتعريف متغيرات أساسية لتسهيل الوصول إلى الملفات والمجلدات. يتضمن ذلك اسم التطبيق، مسار ملف android.jar (الذي يحتوي على واجهات برمجة تطبيقات أندرويد)، وموقع ملف التوقيع (Keystore).
نصيحة لتصحيح الأخطاء: تأكد من أن ANDROID_JAR يشير إلى الإصدار الصحيح لمنصة أندرويد التي تستهدفها (مثل platforms/android-33/android.jar) لتجنب أخطاء في التوافق أثناء الترجمة. إذا واجهت خطأ "file not found" أو "cannot find symbol"، تحقق من صحة المسارات عن طريق طباعتها باستخدام echo $ANDROID_JAR.
# ---------- [تنظيف المجلدات السابقة] ----------
rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR/lib/arm64-v8a" ...
echo "[*] Compiling native code..."
"$CLANG" -shared -fPIC \
--target=aarch64-linux-android21 \
--sysroot="$NDK_SYSROOT" \
"$CPP_DIR/native-lib.cpp" \
"$CPP_DIR/android_native_app_glue.c" \
-o "$BUILD_DIR/lib/arm64-v8a/libnative-lib.so" \
-llog -landroid -lEGL -lGLESv2
- التنظيف: يبدأ السكربت بإزالة مجلد البناء السابق لضمان بناء جديد ونظيف. هذا يمنع مشاكل مثل استخدام ملفات قديمة تسبب أخطاء غير متوقعة في التنفيذ.
- الترجمة (Compilation): يستخدم
clang++لترجمة ملفات C++ (native-lib.cppوandroid_native_app_glue.c).--target=aarch64-linux-android21: يحدد هذا الخيار أن الهدف هو بنية ARM 64-bit لأندرويد، مع تحديد الحد الأدنى لمستوى الـ API (هنا 21). هذا يضمن أن الكود المترجم لن يستخدم واجهات برمجية غير متوفرة في الإصدارات القديمة.--sysroot="$NDK_SYSROOT": يحدد المسار إلى ملفات النظام والمكتبات الخاصة بـ NDK، وهو ضروري للوصول إلى الهيدرات (headers) والمكتبات القياسية مثلEGLوGLES.-shared -fPIC: يُنتج مكتبة مشتركة (.so) يمكن تحميلها ديناميكيًا بواسطة التطبيق.-fPIC(Position-Independent Code) يجعل الكود قابلاً للنقل، وهو مطلب للمكتبات المشتركة.android_native_app_glue.c: هو عبارة عن كود مساعد يوفره الـ NDK لتبسيط إدارة أحداث دورة حياة التطبيق (مثل الإنشاء، الإيقاف المؤقت) والتفاعلات مع المستخدم في تطبيق يعتمد بالكامل على الكود الأصلي. يقوم بإنشاء خيط منفصل لمعالجة الإدخال والأحداث، مما يمنع انسداد الخيط الرئيسي للتطبيق.-l<name>: هذه الخيارات تربط المكتبات المشتركة الضرورية. على سبيل المثال,-llogلـ Android logging،-landroidللواجهات البرمجية الأساسية، و-lEGL/-lGLESv2لرسوميات OpenGL.- خطأ شائع: "undefined reference to..." يشير هذا الخطأ إلى أن المترجم لم يجد تعريف دالة معينة. تأكد من أنك قمت بربط جميع المكتبات المطلوبة باستخدام
-lأو أنك قمت بتضمين جميع ملفات المصدر (.cpp/.c) في أمر الترجمة.
echo "[*] Compiling resources and generating R.java..."
"$AAPT2" compile --dir "$RES_DIR" -o "$BUILD_DIR/compiled_res"
"$AAPT2" link \
-o "$APK_UNSIGNED" \
-I "$ANDROID_JAR" \
--manifest "$SRC_DIR/AndroidManifest.xml" \
--java "$GEN_JAVA_SRC_DIR" \
--min-sdk-version $MIN_SDK \
--target-sdk-version $TARGET_SDK \
-R "$BUILD_DIR/compiled_res"/*.flat
هنا، تتم معالجة موارد التطبيق على خطوتين باستخدام aapt2:
aapt2 compile: تقوم هذه الخطوة بتجميع كل ملف مورد (XML, PNG, etc.) على حدة إلى تنسيق ثنائي محسن (.flat). هذا يسمح ببناء تدريجي، حيث يعاد تجميع الملفات المتغيرة فقط. يتم وضع المخرجات في مجلد وسيط.- نصيحة: يدعم
aapt2مؤهلات الموارد (resource qualifiers) مثلdrawable-hdpiأوvalues-ar. سيقوم بتجميع كل نسخة من المورد بشكل منفصل، ويقرر النظام أي نسخة سيتم استخدامها وقت التشغيل بناءً على جهاز المستخدم.
- نصيحة: يدعم
aapt2 link: تربط هذه الخطوة جميع الملفات المجمّعة (.flat) مع ملفAndroidManifest.xmlلإنشاء ملف APK أولي غير موقّع.-I "$ANDROID_JAR": يربط مكتبة أندرويد للسماح باستخدام موارد النظام (مثل@android:color/white).--java "$GEN_JAVA_SRC_DIR": يقوم بإنشاء ملفR.java، وهو ملف يحتوي على معرفات فريدة لكل مورد في التطبيق، مما يسمح بالوصول إليها من كود جافا (أو عبر JNI من الكود الأصلي).-R .../*.flat: يحدد مسار الموارد المترجمة التي سيتم تضمينها في الـ APK.
echo "[*] Compiling R.java and creating DEX file..."
javac -d "$BUILD_CLASSES_DIR" -classpath "$ANDROID_JAR" --release 8 "$GEN_JAVA_SRC_DIR/R.java"
"$D8_TOOL" --release --output "$BUILD_DIR" --lib "$ANDROID_JAR" --min-api $MIN_SDK "$BUILD_CLASSES_DIR"/**/*.class
javac: يقوم مترجم جافا بترجمة ملفR.java(وأي ملفات جافا أخرى إن وجدت) إلى Java bytecode (.class). خيار--release 8يحدد أن الكود يجب أن يكون متوافقًا مع Java 8.d8: تأخذ أداةd8ملفات.classالناتجة وتحولها إلى ملفclasses.dex. ملف DEX هو الصيغة التنفيذية التي يفهمها ويشغلها Android Runtime (ART).--release: يشير إلى بناء مخصص للنشر، مما يمكّن تحسينات إضافية.- ملاحظة متقدمة: في المشاريع الحقيقية، غالبًا ما يتم استخدام أداة R8 بدلاً من d8 مباشرة. R8 هي أداة شاملة تقوم بالتحويل إلى DEX، تقليص الكود (shrinking) لإزالة الكود غير المستخدم، التعتيم (obfuscation) لحماية الكود، والتحسين (optimization).
echo "[*] Adding classes.dex and native libs to the APK..."
(cd "$BUILD_DIR" && zip -uj "$APK_UNSIGNED" "classes.dex")
(cd "$BUILD_DIR" && zip -ur "$APK_UNSIGNED" "lib")
يتم استخدام أداة zip القياسية لإضافة ملف classes.dex والمكتبات الأصلية (مثل lib/arm64-v8a/libnative-lib.so) إلى ملف APK الأولي الذي تم إنشاؤه بواسطة aapt2. ملف APK في جوهره هو عبارة عن أرشيف ZIP منظم.
echo "[*] Aligning the APK..."
"$ZIPALIGN" -v 4 "$APK_UNSIGNED" "$APK_UNSIGNED_ALIGNED"
تقوم أداة zipalign بتحسين ملف APK عن طريق ضمان أن جميع البيانات غير المضغوطة (مثل الصور والموارد) تبدأ عند حدود 4 بايت. هذه العملية مهمة جدًا لأنها تقلل من استهلاك الذاكرة العشوائية (RAM) وتزيد من سرعة تشغيل التطبيق، حيث يمكن للنظام الوصول إلى الموارد مباشرة عبر mmap() بدلاً من فك ضغطها أولاً.
echo "[*] Signing the aligned APK..."
if [ ! -f "$KEYSTORE" ]; then
echo "[*] Generating keystore..."
"$KEYTOOL" -genkeypair -v -keystore "$KEYSTORE" -alias "$KEY_ALIAS" -keyalg RSA -keysize 2048 -validity 10000
fi
"$APKSIGNER" sign \
--ks "$KEYSTORE" \
--ks-key-alias "$KEY_ALIAS" \
--ks-pass pass:"$KEY_PASS" \
--out "$APK_SIGNED" \
"$APK_UNSIGNED_ALIGNED"
هذه هي الخطوة النهائية والحاسمة:
- إنشاء Keystore (إذا لم يكن موجودًا): يستخدم السكربت
keytoolلإنشاء ملف توقيع جديد (.keystore). يحتوي هذا الملف على زوج من المفاتيح (عام وخاص) يستخدم لتحديد هوية المطور.- نصيحة أمان: لا تقم بتخزين كلمات المرور كنص عادي في السكربتات في المشاريع الحقيقية. استخدم متغيرات البيئة أو أدوات إدارة الأسرار. احتفظ بنسخة احتياطية آمنة من ملف Keystore؛ ففقدانه يعني أنك لن تتمكن من نشر تحديثات لتطبيقك.
- التوقيع: تستخدم أداة
apksignerملف الـ Keystore لتوقيع ملف APK الذي تمت محاذاته. التوقيع الرقمي يؤكد أن التطبيق لم يتم التلاعب به منذ توقيعه ويثبت ملكية المطور.- بشكل افتراضي، يستخدم
apksignerمخططات التوقيع الحديثة (v2, v3, v4) التي توفر أمانًا أفضل وسرعة تحقق أعلى على الأجهزة. التوقيع باستخدام هذه المخططات يحمي محتوى APK بالكامل.
- بشكل افتراضي، يستخدم
من الرائع فهم هذه العملية اليدوية، ولكن في التطوير اليومي، تُستخدم أدوات بناء عالية المستوى لأتمتة هذه الخطوات.
- CMake: هو نظام بناء مفتوح المصدر شائع لبناء الكود الأصلي. عند استخدامه مع أندرويد، يمكنك كتابة ملف
CMakeLists.txtبسيط لتحديد ملفات المصدر والمكتبات التي تريد ربطها. يتولى Gradle و NDK بعد ذلك استدعاءclang++بالأعلام والمسارات الصحيحة. - Gradle: هو نظام الأتمتة الرسمي لبناء تطبيقات أندرويد. يقوم بقراءة ملفات
build.gradleلتحديد كيفية بناء التطبيق. داخليًا، يقوم Gradle باستدعاء نفس الأدوات التي استخدمناها يدويًا (aapt2,d8/R8,apksigner)، لكنه يدير التبعيات المعقدة، متغيرات البناء (build variants)، والتكوينات الأخرى تلقائيًا.
معرفتك بالعملية اليدوية تمنحك القدرة على فهم ما يفعله Gradle "تحت الغطاء"، مما يساعدك على تشخيص المشاكل عندما تفشل عملية البناء الآلية.
بعد اكتمال السكربت، يتم إنتاج ملف MyNDKApp-signed.apk النهائي، وهو جاهز للتثبيت على أجهزة أندرويد (استخدم adb install MyNDKApp-signed.apk للتثبيت). من خلال اتباع هذه الخطوات اليدوية، نكون قد أتممنا بنجاح جميع مراحل بناء التطبيق: من ترجمة الكود الأصلي وموارد التطبيق، إلى تجميع وتحويل كود جافا، وأخيرًا تجميع كل المكونات في حزمة واحدة وتأمينها بالتوقيع الرقمي. هذا الفهم العميق ليس مجرد تمرين أكاديمي، بل هو أساس قوي يمكّنك من حل المشكلات المتقدمة وتحسين عمليات البناء في أي مشروع أندرويد.