يقوم Android 9 Pie وAndroid 10 بإرسال تحذيرات أو حظر الوصول تمامًا إلى واجهات برمجة التطبيقات المخفية. إليك كيف يمكن للمطورين التغلب على قيود واجهة برمجة التطبيقات المخفية.
ارجع بالذاكرة إلى ما مضى عليه أكثر من عام، ونحن جميعًا متحمسون لرؤية ما سيأتي في الإصدارات التجريبية من Android P. يتطلع المستخدمون إلى ميزات جديدة، ويتطلع المطورون إلى بعض الأدوات الجديدة لتحسين تطبيقاتهم. لسوء الحظ بالنسبة لبعض هؤلاء المطورين، فإن الإصدار التجريبي الأول من Android P جاء مع قليل من المفاجأة السيئة: قيود واجهة برمجة التطبيقات المخفية. قبل أن أتعمق في ما يعنيه ذلك بالضبط، اسمحوا لي أن أشرح قليلاً عن سياقه.
لماذا كل هذا؟
لا يتعين على مطوري تطبيقات Android البدء من الصفر عند إنشاء تطبيق ما. توفر Google أدوات لجعل تطوير التطبيقات أسهل وأقل تكرارًا. إحدى هذه الأدوات هي Android SDK. تعد SDK في الأساس مرجعًا لجميع الوظائف التي يمكن للمطورين استخدامها بأمان في تطبيقاتهم. تأتي هذه الوظائف بشكل قياسي في جميع إصدارات Android التي توافق عليها Google. ومع ذلك، فإن SDK ليست شاملة. هناك عدد لا بأس به من الأجزاء "المخفية" من إطار عمل Android والتي لا تشكل جزءًا من SDK.
يمكن أن تكون هذه الأجزاء "المخفية" مفيدة بشكل لا يصدق للأشياء الأكثر اختراقًا أو ذات المستوى المنخفض. على سبيل المثال، بلدي تطبيق درج القطعة يستخدم وظيفة غير تابعة لـ SDK لاكتشاف متى يقوم المستخدم بتشغيل تطبيق من عنصر واجهة مستخدم حتى يتمكن الدرج من الإغلاق تلقائيًا. قد تفكر: "لماذا لا تجعل هذه الوظائف غير التابعة لـ SDK جزءًا من SDK فقط؟" حسنًا، المشكلة هي أن عمليتهم لا يمكن التنبؤ بها بشكل كامل. لا تستطيع Google التأكد من أن كل جزء من إطار العمل يعمل على كل جهاز توافق عليه، لذلك يتم تحديد طرق أكثر أهمية للتحقق منها. ولا تضمن Google بقاء بقية إطار العمل متسقًا. يمكن للمصنعين تغيير هذه الوظائف المخفية أو إزالتها بالكامل. حتى في الإصدارات المختلفة من AOSP، فإنك لا تعرف أبدًا ما إذا كانت الوظيفة المخفية ستظل موجودة أو تعمل كما كانت في السابق.
إذا كنت تتساءل عن سبب استخدامي لكلمة "مخفي"، فذلك لأن هذه الوظائف ليست حتى جزءًا من SDK. عادةً، إذا حاولت استخدام طريقة أو فئة مخفية في أحد التطبيقات، فسوف يفشل تجميعها. استخدامها يتطلب انعكاس أو SDK المعدلة.
ومع نظام التشغيل Android P، قررت Google أن مجرد إخفاء هذه العناصر ليس كافيًا. عندما تم إصدار النسخة التجريبية الأولى، تم الإعلان عن ذلك لم تعد معظم الوظائف المخفية (وليس كلها) متاحة للاستخدام لمطوري التطبيقات. سيحذرك الإصدار التجريبي الأول عندما يستخدم تطبيقك وظيفة مدرجة في القائمة السوداء. لقد أدت الإصدارات التجريبية التالية إلى تعطل تطبيقك. على الأقل بالنسبة لي، كانت هذه القائمة السوداء مزعجة للغاية. لم يقتصر الأمر على كسر قدر كبير من إيماءات التنقل، ولكن بما أن الوظائف المخفية يتم إدراجها في القائمة السوداء افتراضيًا (يتعين على Google إدراج بعض الوظائف في القائمة البيضاء يدويًا لكل إصدار)، فقد واجهت الكثير من المشاكل في تشغيل Widget Drawer.
الآن، هناك عدة طرق للتغلب على القائمة السوداء. الأول كان ببساطة هو إبقاء تطبيقك يستهدف API 27 (Android 8.1)، نظرًا لأن القائمة السوداء تنطبق فقط على التطبيقات التي تستهدف أحدث واجهة برمجة التطبيقات. لسوء الحظ، مع جوجل الحد الأدنى من متطلبات API للنشر على متجر Play، سيكون من الممكن فقط استهداف API 27 لفترة طويلة. اعتبارًا من 1 نوفمبر 2019، يجب أن تستهدف جميع تحديثات التطبيقات في متجر Play واجهة برمجة التطبيقات 28 أو الإصدارات الأحدث.
الحل الثاني هو في الواقع شيء مدمج من Google في Android. من الممكن تشغيل أمر ADB لتعطيل القائمة السوداء بالكامل. يعد هذا أمرًا رائعًا للاستخدام والاختبار الشخصي، لكن يمكنني أن أخبرك بشكل مباشر أن محاولة إقناع المستخدمين النهائيين بإعداد ADB وتشغيله يعد بمثابة كابوس.
ما موقفنا من ذلك؟ لا يمكننا استهداف API 27 بعد الآن إذا أردنا التحميل إلى متجر Play، وطريقة ADB غير قابلة للتطبيق. لكن هذا لا يعني أن الخيارات قد نفدت.
تنطبق القائمة السوداء لواجهة برمجة التطبيقات المخفية فقط على تطبيقات المستخدم غير المدرجة في القائمة البيضاء. تطبيقات النظام والتطبيقات الموقعة بتوقيع النظام الأساسي والتطبيقات المحددة في ملف التكوين كلها معفاة من القائمة السوداء. ومن المضحك أن مجموعة خدمات Google Play كلها محددة في ملف التكوين هذا. أعتقد أن Google أفضل من بقيتنا.
على أية حال، دعونا نواصل الحديث عن القائمة السوداء. الجزء الذي يهمنا اليوم هو أن تطبيقات النظام معفاة. وهذا يعني، على سبيل المثال، أن تطبيق System UI يمكنه استخدام جميع الوظائف المخفية التي يريدها نظرًا لوجودها في قسم النظام. من الواضح أننا لا نستطيع فقط دفع التطبيق إلى قسم النظام. وهذا يحتاج إلى جذر، ومدير ملفات جيد، ومعرفة بالأذونات... سيكون من الأسهل استخدام بنك التنمية الآسيوي. هذه ليست الطريقة الوحيدة التي يمكننا من خلالها أن نكون تطبيقًا للنظام، على الأقل فيما يتعلق بالقائمة السوداء لواجهة برمجة التطبيقات المخفية.
ارجع بعقلك إلى سبع فقرات مضت عندما ذكرت التأمل. إذا كنت لا تعرف ما هو الانعكاس، أنصحك بالقراءة هذه الصفحة، ولكن هنا ملخص سريع. في Java، يعد الانعكاس طريقة للوصول إلى الفئات والأساليب والحقول التي يتعذر الوصول إليها عادةً. إنها أداة قوية بشكل لا يصدق. كما قلت في تلك الفقرة، كان الانعكاس وسيلة للوصول إلى وظائف غير تابعة لـ SDK. تحظر القائمة السوداء لواجهة برمجة التطبيقات (API) استخدام الانعكاس، ولكنها لا تمنع استخدامه مزدوج-انعكاس.
الآن، هنا يصبح الأمر غريبًا بعض الشيء. عادةً، إذا أردت استدعاء طريقة مخفية، فستفعل شيئًا كهذا (هذا موجود في Kotlin، لكن Java مشابه):
val someHiddenClass = Class.forName("android.some.hidden.Class")
val someHiddenMethod = someHiddenClass.getMethod("someHiddenMethod", String::class.java)
someHiddenMethod.invoke(null, "some important string")
وبفضل القائمة السوداء لواجهة برمجة التطبيقات (API)، ستحصل فقط على ClassNotFoundException. ومع ذلك، إذا فكرت مرتين، فسيعمل بشكل جيد:
val forName = Class::class.java.getMethod("forName", String:: class.java)
val getMethod = Class::class.java.getMethod("getMethod", String:: class.java, arrayOf>()::class.java)
val someHiddenClass = forName.invoke(null, "android.some.hidden.Class") asClass
val someHiddenMethod = getMethod.invoke(someHiddenClass, "someHiddenMethod", String::class.java)
someHiddenMethod.invoke(null, "some important string")
الحق غريب؟ حسنا، نعم، ولكن أيضا لا. تتعقب القائمة السوداء لواجهة برمجة التطبيقات (API) من يتصل بوظيفة ما. إذا لم يكن المصدر مستثنى، فإنه يتعطل. في المثال الأول، المصدر هو التطبيق. ومع ذلك، في المثال الثاني، المصدر هو النظام نفسه. بدلاً من استخدام الانعكاس للحصول على ما نريده بشكل مباشر، فإننا نستخدمه لإخبار النظام بالحصول على ما نريد. وبما أن مصدر استدعاء الوظيفة المخفية هو النظام، فإن القائمة السوداء لم تعد تؤثر علينا.
لذلك انتهينا. لدينا طريقة لتجاوز القائمة السوداء لواجهة برمجة التطبيقات (API) الآن. إنها قديمة بعض الشيء، ولكن يمكننا كتابة دالة مجمعة حتى لا نضطر إلى التفكير المزدوج في كل مرة. إنها ليست رائعة، لكنها أفضل من لا شيء. لكن في الواقع، لم ننته بعد. هناك طريقة أفضل للقيام بذلك تتيح لنا استخدام الانعكاس العادي أو SDK المعدل، كما كان الحال في الأيام الخوالي.
نظرًا لأنه يتم تقييم تنفيذ القائمة السوداء لكل عملية (وهو نفس التقييم لكل تطبيق في معظم الحالات)، فقد تكون هناك طريقة ما يمكن للنظام من خلالها تسجيل ما إذا كان التطبيق المعني معفيًا أم لا. لحسن الحظ، هناك، ويمكن الوصول إليها بالنسبة لنا. باستخدام اختراق الانعكاس المزدوج المكتشف حديثًا، أصبح لدينا كتلة تعليمات برمجية تستخدم لمرة واحدة:
val forName = Class::class.java.getDeclaredMethod("forName", String:: class.java)
val getDeclaredMethod = Class::class.java.getDeclaredMethod("getDeclaredMethod", String:: class.java, arrayOf>()::class.java) val vmRuntimeClass = forName.invoke(null, "dalvik.system.VMRuntime") asClass
val getRuntime = getDeclaredMethod.invoke(vmRuntimeClass, "getRuntime", null) as Method
val setHiddenApiExemptions = getDeclaredMethod.invoke(vmRuntimeClass, "setHiddenApiExemptions", arrayOf(arrayOf<String>()::class.java)) asMethodval vmRuntime = getRuntime.invoke(null)
setHiddenApiExemptions.invoke(vmRuntime, arrayOf("L"))
حسنًا، من الناحية الفنية، هذا لا يخبر النظام بأن تطبيقنا معفى من القائمة السوداء لواجهة برمجة التطبيقات. يوجد بالفعل أمر ADB آخر يمكنك تشغيله لتحديد الوظائف التي لا ينبغي إدراجها في القائمة السوداء. وهذا ما نستفيد منه أعلاه. يتجاوز الكود بشكل أساسي كل ما يعتقد النظام أنه معفى لتطبيقنا. تمرير "L" في النهاية يعني أن جميع الطرق معفاة. إذا كنت تريد استثناء أساليب معينة، فاستخدم صيغة Smali: Landroid/some/hidden/Class; Landroid/some/other/hidden/Class;
.
الآن انتهينا فعلا. أنشئ فئة تطبيق مخصصة، ثم ضع هذا الرمز في ملف onCreate()
الطريقة، وبام، لا مزيد من القيود.
شكرًا لعضو XDA weishu، مطور VirtualXpose وTaichi، لاكتشافه هذه الطريقة في الأصل. نود أيضًا أن نشكر مطور XDA المعترف به topjohnwu لتوضيح هذا الحل البديل لي. وإليك المزيد حول كيفية عمله، على الرغم من أنه باللغة الصينية. أنا أيضاً كتب عن هذا على Stack Overflow، مع مثال في JNI أيضًا.