الأدوات والأداء

استراتيجيات قابلة للتنفيذ لتقليل استهلاك الصور النقطية للذاكرة في تطبيقك

سماعات

لمحة

لمحة عن المحاضرة

تُعدّ إدارة ذاكرة الصور النقطية أمرًا بالغ الأهمية للحدّ من مخاطر نفاد الذاكرة (OOM) في تطبيقات Android الغنية بالمرئيات. على الرغم من أنّ المطوّرين يدركون أهمية تقليل أحجام الصور، فإنّ تحقيق استقرار التطبيق على المدى الطويل يتطلّب إتقان ركيزتَين أساسيتَين أخريَين، وهما إعادة الاستخدام وإعادة التدوير. تقدّم هذه الجلسة دليلاً عمليًا لتحسين أداء الذاكرة باستخدام أدوات حديثة ومكتبات مفتوحة المصدر، مثل Glide وCoil، وواجهات برمجة تطبيقات على مستوى النظام.

في هذه الجلسة، ستتعرّف على كيفية تشخيص حالات تراجُع الذاكرة في الواقع باستخدام مؤشرات Android الحيوية، وتنفيذ أنماط التخزين المؤقت المتقدّمة (إعادة الاستخدام) باستخدام Glide وCoil، وتحرير الذاكرة بشكل استباقي (إعادة التدوير) باستخدام دالة الاستدعاء onTrimMemory.

ملخّص

  • سيقدّم Google Play الحدّ الأدنى من متطلبات أداء الذاكرة التي سيتم تطبيقها في فبراير 2027. يؤثر تجاوز الحدّ المئوي الـ 90 بشكل مباشر في تقييم حالة تطبيقك، ويمكن أن يقلّل من مستوى ظهوره على "متجر Google Play".
  • تتوسّع الصور النقطية حسب الأبعاد وعمق اللون عند فك ترميزها، ما يجعلها أكبر مستهلك للذاكرة في معظم التطبيقات. فبضع صور بالدقة الكاملة يمكن أن تستهلك مئات الميغابايت.
  • يمكنك تقليل دقة الصور وفك ترميز المناطق المرئية فقط لتقليل استهلاك الذاكرة. اطلب أبعاد صور مطابقة من خلال شبكة توصيل المحتوى (CDN) أو استخدِم واجهات برمجة تطبيقات مثل BitmapRegionDecoder بدلاً من تحميل صور كبيرة الحجم بدقة كاملة.
  • تؤدي أدوات تحميل الصور المتعددة إلى مضاعفة استخدام ذاكرة التخزين المؤقت التلقائية في الذاكرة المؤقتة وتكرار الصور النقطية المخزّنة. استخدِم أداة تحميل واحدة أو اضبط ميزانيات ذاكرة صريحة في جميع أدوات التحميل الحالية.
  • يمكنك تحرير ذاكرة الصور عندما ينتقل تطبيقك إلى الخلفية من خلال معالجة onTrimMemory. إيقاف الاستطلاع والصور المتحركة في الخلفية في "onStop" لمنع زيادة استخدام الذاكرة في الخلفية

الموارد

Gemini Notebook

المواضيع

  • bitmap-optimization
  • android-memory
  • android-vitals
  • image-caching
  • memory-profiler

النص

تنزيل النص

مقدمة ونظرة عامة على استخدام الصور النقطية للذاكرة

  • Fung Lam

    نعم، لدينا حوالي ساعة قبل الغداء، لذا يُرجى البقاء معي.

  • اسمي "فونغ لام"، وأنا مهندس علاقات المطوّرين في Android، وأقيم هنا في لندن.

  • نعم، شكرًا لك.

  • قبل أن أبدأ، هل يمكنني طرح سؤال؟

  • كم منكم اشترى هاتفًا جديدًا هذا العام؟

  • يا للروعة

  • يا للروعة

  • أريد إجابة صادقة، ولن ألومك إذا لم تشترِ هاتف Pixel.

  • حسنًا، الهاتف الجديد أغلى ثمنًا، أليس كذلك؟

  • نعم، لهذا السبب لم يشترِ بعضكم هاتفًا جديدًا.

  • لماذا السعر أعلى؟

  • هل تعلم؟

  • لا.

  • ذاكرة الوصول العشوائي (RAM).

  • ‫ذاكرة الوصول العشوائي، نعم

  • نعم، تمثّل ذاكرة الوصول العشوائي (RAM) تحديًا كبيرًا لنظام Android ولنا كمطوّرين.

  • لهذا السبب، أنا هنا اليوم لأخبركم كيف يمكننا التغلّب على التحدي المتعلّق بالذاكرة، أي بذاكرة الوصول العشوائي (RAM).

  • في وقت سابق من مؤتمر Google I/O وGoogle I/O Connect، عقدنا بعض الجلسات حول الذاكرة، بدءًا من كيفية تخصيص نظام التشغيل للذاكرة، وكيفية تحليل أداء تطبيقك، وكيفية تحسين الرمز البرمجي باستخدام R8، وبالطبع، تناولتُ بعض المعلومات عن Bitmap في ذلك الوقت.

  • يمكنك رؤية صوري في لقطة الشاشة.

  • تحسين الذاكرة هو أحد المجالات الكبيرة التي تشمل جوانب مختلفة، مثل وقت تشغيل Android، وعمليات جمع المهملات، وأدوات الأداء، وتخصيص ذاكرة العناصر، وبرنامج التجميع، وما إلى ذلك. وفي هذه الجلسة التي تستغرق 30 دقيقة، سأقدّم لك بعض المعلومات حول كيفية تحسين ذاكرة الصور النقطية.

  • قد تتساءل، لماذا، لماذا Bitmap؟

  • لماذا يجب التركيز على ذاكرة الصور النقطية؟

  • لأنّ الصورة تساوي ألف كلمة.

  • هذا صحيح، وسأشرح لك السبب.

  • على شاشة الهاتف، لنفترض أنّ دقة الشاشة على هاتف Pixel 7 أو Pixel 9 أو Pixel 11 Pro تبلغ حوالي 2,500 × 1,200 أو 1,400.

  • لذلك، لكل بكسل، نحمل 32 بت من عمق اللون.

  • بالنسبة إلى صورة ملء الشاشة، ليس لديّ ما يكفي من الأصابع، ولكنّها ستستهلك حوالي 15 ميغابايت من الذاكرة، أو 50 ميغابايت.

  • لذا، فإنّ الكلمة في نظام التشغيل تكون مثلاً 64 بت، ما يعني أنّها 8 بايت.

  • ألف كلمة هي ثمانية فقط، 8 ميغابايت.

  • نعم، 8K.

  • لذا، بالتأكيد صورة واحدة تساوي أكثر من ألف كلمة.

  • نعم، هذا مهم.

  • يبلغ حجم صورة واحدة 15 ميغابايت.

  • ماذا لو كان تطبيقك يحتوي على 10 أو 100 صورة؟

  • حسنًا، إنّه عدد كبير.

  • لذا، فإنّ Bitmap هي دائمًا أكبر مستهلك للذاكرة في تطبيقك.

  • عند فك ترميز الصورة في الذاكرة، كما ذكرت، يتم توسيعها كالبُعد وعمق اللون.

  • لا يهمّ حجم الملف أو تنسيقه، سواء كان JPEG أو PNG.

  • لا، سيتم فك ترميزه واستخدام الذاكرة.

  • لذلك، فإنّ بضع صور بالدقة الكاملة ستشغل بسهولة مئات الميغابايت من الذاكرة على جهازك.

  • ثانيًا، عندما تكون الذاكرة ممتلئة، سيضطر نظام التشغيل إلى استهلاك المزيد من الطاقة للتعامل مع استهلاك الذاكرة.

  • سيؤدي ذلك إلى ظهور واجهة المستخدم بشكل غير سلس.

  • أو قد يوقف نظام التشغيل تطبيقك لإخلاء بعض المساحة في الذاكرة.

  • أو سيواجه تطبيقك أيضًا خطأ "نفاد الذاكرة" أو استثناء "نفاد الذاكرة".

  • لحسن الحظ، الخبر السار هو أنّ هذا الوجه عادةً ما يحقّق نجاحًا كبيرًا بجهد قليل.

  • تحدث معظم الارتفاعات المفاجئة في استخدام الذاكرة بسبب خطأ شائع يمكن تجنّبه، ويمكننا إصلاحه بدون أي تأثير في جودة الصورة.

  • أصبحت الآن على دراية بأهمية ذاكرة الصور النقطية لتطبيقك، ليس فقط لجعله سلسًا، ولكن أيضًا لضمان ثباته والحفاظ على المستخدمين.

  • لذلك، سنستعرض اليوم خمسة أقسام بسيطة لضمان استمرار تشغيل تطبيقك بسلاسة.

  • أولاً، سنلقي نظرة على الجودة الفنية في Google Play، ومتطلبات الجودة الفنية الجديدة، بما في ذلك الحدّ الأقصى للذاكرة القادم ومكان التحقّق من الحدّ الأقصى بسهولة.

  • ثانيًا، سأشارك دراسة حالة حول 500 تطبيق Android، وكيفية استخدامها في الذاكرة، وهل يتم ذلك بطريقة صحيحة، أم أنّ هناك شيئًا يمكننا تعلّمه؟

  • بعد ذلك، سأوضّح لك مبدأ تحسين استخدام الذاكرة للصور النقطية.

  • أخيرًا، سنشارك بعض الأدوات التي يمكن أن تساعدك في التحقّق من تطبيقك.

معايير الجودة والمشاكل الشائعة في Google Play

  • Fung Lam

    أولاً، متطلبات الجودة الجديدة في Google Play.

  • كم مرة تزور Google Play Console؟

  • كل شهر؟

  • كل أسبوع؟

  • أبدًا؟

  • هذه الضحكات تعني نعم، أليس كذلك؟

  • لا؟

  • لا.

  • هل اطّلعت على قسم "مؤشرات Android الحيوية"؟

  • نعم، نعم.

  • نعم، لذا أنت بالتأكيد، تعرف شيئًا قبل أن أصنع هذا.

  • لذلك، في Google Play، هناك متطلبات جودة جديدة بشأن الحد الأدنى للأداء.

  • نحرص في Android وGoogle Play على أن يستخدم المستخدمون تطبيقات عالية الجودة.

  • لذا، هذا الحدّ هو أيضًا أمر نحاول من خلاله تقديم أفضل تجربة للمستخدم النهائي.

  • ببساطة، هذا يعني أنّه عندما يكون تطبيقك قيد التشغيل في الخلفية أو في المقدّمة، يجب ألا يتجاوز استخدام الذاكرة في الشريحة المئوية الـ 90 هذا الحدّ.

  • وإلا، قد يقل ظهور تطبيقك على "متجر Google Play" أو ما شابه".

  • وسيبدأ تطبيق هذه السياسة في فبراير 2027 على التطبيقات والألعاب.

  • ويؤثر تجاوز هذا الحدّ بشكل مباشر في تقييم حالة تطبيقك.

  • لذا، تأكَّد من الانتقال إلى Google Play Console والتحقّق من "النسبة المئوية 90 اليوم".

  • وإجراء التحسينات قبل الموعد النهائي

  • هذه هي الصورة المتحركة.

  • "إذا انتقلت إلى Google Play Console، يمكنك الاطّلاع على مؤشرات Android الحيوية.

  • هناك جزء من الذاكرة.

  • ويمكنك الاطّلاع على الشريحة المئوية التسعين.

  • وإذا نقرت عليه، نعم، يمكنك أن ترى بمرور الوقت متوسط استهلاك الذاكرة.

  • وهناك أيضًا فئات مختلفة حسب ذاكرة الوصول العشوائي (RAM) وعامل الشكل وكثافة الشاشة وإصدار نظام التشغيل، ما يساعدك في عزل مشكلة بطء الذاكرة على جهاز معيّن من الفئة الأساسية أو في إصدارات معيّنة من التطبيق.

  • قد تقولون: "شكرًا على إعلامنا بأنّ تطبيقاتنا تجاوزت الحدّ الأقصى.

  • لكن في الحقيقة، لا أعرف ما المشكلة.

  • هل يمكنك المساعدة؟"

  • نعم، لهذا السبب أنا هنا لمساعدتك.

  • سأحاول تسهيل الأمر عليك، مثلاً، إذا كنت تشاهد التلفزيون، ثم لم يعرض التلفزيون أي فيديو أو صوت، ماذا ستفعل؟

  • تُخرج دليل التلفزيون، وتنتقل إلى الصفحات القليلة الأخيرة، والتي عادةً ما تكون دليل تحديد المشاكل وحلّها، وتقول، "مرحبًا، إذا لم يكن التلفزيون الخاص بك به صوت، فجرّب هذا الزر، أو جرّب ذلك الزر".

  • نعم، وبالمثل، اليوم، حددت بعض المشكلات الشائعة.

  • نأمل أن يكون هذا الدليل مفيدًا لك في تحديد المشاكل وحلّها.

  • عادةً، عندما نقرأ دليل التلفزيون، يجب أن ننتقل من الصفحة الأولى، والتي عادةً ما تكون مجموعات أدوات التشخيص لمساعدتك حقًا في فهم كيفية عمل التلفزيون.

  • ولكن الآن، أعلم أنّ هذه الأمور أقل أهمية، لذا يُرجى السماح لي بالبدء بتحديد المشاكل وحلّها أولاً، وسنعود إليها لاحقًا.

  • ولأكون صادقًا، هذه هي الطريقة التي أقرأ بها دليل التلفزيون.

  • للكشف عن مشكلة ذاكرة الصور النقطية الأكثر شيوعًا في تخصيص الصور النقطية في منظومة Android المتكاملة، بحثتُ في حوالي 500 تطبيق من التطبيقات الرائجة في Android.

  • أقوم بتشغيل التطبيق فعليًا، وبدء التطبيق، وتسجيل الدخول، والانتقال إلى صفحات مختلفة، والتعمق، والعودة.

  • وأختبر التطبيق على ذاكرة وصول عشوائي مختلفة، وأجهزة مختلفة أيضًا، مثل جهاز Android Go بذاكرة بسعة 2 غيغابايت، وبعض الأجهزة المنخفضة المواصفات بسعة 4 غيغابايت، وبعض الأجهزة المتوسطة المواصفات بسعة 4 غيغابايت، و6 غيغابايت إلى 8 غيغابايت، أو بعض الأجهزة القابلة للطي المميّزة بشاشة كبيرة جدًا وجودة شاشة جيدة جدًا وذاكرة بسعة 16 غيغابايت.

  • قِسنا استخدام الذاكرة على هذا الجهاز في حالات مختلفة للتطبيق، والتقطنا أجزاء من الذاكرة، واستخدمنا أدوات مختلفة، بما في ذلك Perfetto AI Skill، لتحليل عملية التخصيص، وتحديد الأنماط المضادة الشائعة، أي الأخطاء الشائعة التي نرتكبها على نطاق واسع.

  • وهكذا تغيّرت الذاكرة في حالات النشاط المختلفة.

  • ومن خلال هذا التحليل، نحدّد أكثر من 15 مشكلة شائعة، تتراوح بين الصور الكبيرة الحجم وزيادة ذاكرة التخزين المؤقت في الخلفية.

  • هذه هي دراسة الحالة الأساسية التي سنشاركها ونستكشفها اليوم.

دراسات حالة: تغيير حجم الصور النقطية وفك ترميزها

  • Fung Lam

    نعم، كل دراسة الحالة التي أشاركها الآن تدور حول البحث الذي شمل 500 تطبيق.

  • بما أنّكم هنا، أعتقد أنّنا جميعًا نحب Android، أليس كذلك؟

  • جميعنا نحب Android.

  • لنفترض أنّ أحدنا يحب Android أيضًا، ولكنه ليس خبيرًا في تطوير Android.

  • لذلك، أنشأ تطبيق Android لطلب تماثيل Android.

  • ولكن هناك بعض الأخطاء.

  • لنتحدّث عن ذلك، ربما يكون هذا هو الخطأ الأول.

  • "مرحبًا، في صفحة المنتج، لدينا تنسيق من عمودين يعرض تماثيل Android مختلفة.

  • بالنسبة إلى كل عمود، يبلغ حجم الصورة حوالي 400 × 400 بكسل.

  • ومع ذلك، فإن التطبيق، في الواقع، يقوم بتنزيل صورة واحدة من الخادم، بغض النظر عن حجم شاشتي.

  • يتم تنزيل صورة بدقة 3000 × 3000.

  • نعم، لأنّ المطوّرين يقولون: "إنّ Android رائع ومذهل للغاية.

  • أريد أن أقدّم أفضل تجربة للمستخدم".

  • ولكن في الواقع، لا، لأنّه، كما تعلمون، يتم تحديد الحد الأقصى من خلال سمة عرض الصورة.

  • لذا، حتى إذا نزّلت الصورة الكبيرة، لن تظهر للمستخدم بأفضل شكل.

  • بالإضافة إلى ذلك، تسبّبت هذه الصورة الكبيرة في استهلاك ذاكرة إضافية.

  • وحتى إذا خفّضنا معدّل العيّنات، ستستهلك هذه العملية أيضًا ذاكرة إضافية.

  • كيف يمكننا مساعدة هذا المطوّر في حلّها؟

  • إذا كنتم على دراية بشبكة توصيل المحتوى (CDN)، يمكن لهذه الشبكة معالجة بعض عمليات الصور، مثل الاقتصاص وتغيير الحجم والتدوير وتطبيق الفلاتر وتحويل الصورة من مربّعة إلى دائرية.

  • من خلال طلب HTTP، يمكن لشبكة توصيل المحتوى معالجة بعض الأمور.

  • لذا، حتى إذا كانت جهة الخادم تستضيف صورة واحدة فقط، يمكنك استخدام شبكة توصيل المحتوى لإرسال صورة تم تغيير حجمها إلى جهازك.

  • لذا، رقم 1، نعم عندما تطلب الصورة، حاول استخدام شبكة توصيل المحتوى، لذلك ستطلب الصورة بحجم ImageView.

  • ولكن إذا لم يكن ذلك ممكنًا، مثل أنه قد يكون في بعض الأحيان أكبر قليلاً من ImageView، فتذكر تقليل العينة لتغيير حجمها في الكود.

  • لذلك، عندما يتم تحميلها في الذاكرة، يتم تحميل صورة أصغر، وليس تحميل الدقة الكاملة للصورة.

  • في ما يلي عينة تعليمات برمجية لمكتبة Coil.

    // 1. Ask CDN for the exact UI card dimensions (e.g. 540x675)
    val sizedUrl = "$cdnBaseUrl?w=$cardWidthPx&h=$cardHeightPx&fmt=webp"
    
    // 2. Enforce exact downsampling + Hardware Bitmap config
    AsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data(sizedUrl)
            .size(cardWidthPx, cardHeightPx) // Never use Size.ORIGINAL!
            .precision(Precision.EXACT)      // Downsample to exact card size
            .bitmapConfig(Bitmap.Config.HARDWARE)
            .build(),
        contentDescription = product.title
    )
  • يمكنك تمرير الحجم كصياغة باللون الأصفر.

  • نعم، يمكنك تمرير الحجم كـ HTTP في طلب HTTP.

  • بعد ذلك، في الرمز المميّز هنا، هل يمكنك رؤية الجزء المميّز؟ نعم، لا تستخدِم Size.ORIGINAL.

  • نعم، استخدِم العرض والارتفاع، ليتم تمريرهما كمعلَمة.

  • استخدِم الدقة لتقليل دقة الصورة إلى الحجم الدقيق.

  • وإذا أمكن، استخدِم Bitmap.Config.HARDWARE، الذي يستخدم أيضًا ذاكرة أقل لأنّ الصورة سيتم تخزينها في ذاكرة VRAM مباشرةً.

  • وبالمثل، في هذا التطبيق، لنفترض أنهم أصدروا إصدارًا جديدًا من مقتنيات Android، لذلك هناك صورة طويلة يمكن للمستخدم التمرير سريعًا لليسار واليمين لرؤية تصميم مختلف لشكل Android الجديد.

  • ولكن هناك مشكلة أخرى هنا.

  • بالنسبة إلى الصورة الطويلة، يكون جزء كبير منها خارج الشاشة، ما يعني أنّ المستخدم لا يمكنه رؤيتها، ولكن يتم تحميل الصورة في الذاكرة.

  • هل يمكننا تحسينه؟

  • نعم.

  • يمكننا تقسيمها إلى صورة أصغر وأصغر وعرض الصورة المعروضة على الشاشة فقط.

  • ولن يتم تحميل العناصر التي تقع خارج الشاشة، أو سيتم إلغاء تحميلها.

  • وبالتالي، سيستخدم مقدارًا أقل من الذاكرة.

  • وبالمثل، في هذه الحالة، تم توفير حوالي %60 من الذاكرة.

  • بالطبع، إذا قمت بالقص إلى مستطيل أصغر، فسيؤدي ذلك إلى توفير المزيد.

  • ولكن بالمثل، إذا أراد المستخدم توفير ميزة للمستخدم، فيمكنك التكبير والتكبير لرؤية كل تفاصيل تمثال Android، وبعض الأخطاء الشائعة هي أنها يتم تحميلها حقًا، مرة أخرى، صورة ضخمة جدًا، لذلك يمكن تكبيرها وتكبيرها مرة أخرى.

  • ولكن، لا، ليس عليك إجراء ذلك.

  • الطريقة الأفضل هي عندما يكبّر المستخدم الصورة الأصلية ذات الدقة المنخفضة التي تم تحميلها، وعندما يتوقف المستخدم عن التصغير، يتم حساب الإحداثيات ثم تحميل الدقة الكاملة، أي تحميل تلك المنطقة فقط، وهي منطقة محسوبة من الصورة ذات الدقة العالية.

  • لذلك، سيتم تحميل هذه المنطقة المرئية فقط في الذاكرة.

  • كيف يمكنني إجراء ذلك؟

  • هناك واجهة برمجة تطبيقات تُسمى BitmapRegionDecoder، تساعدك في تحميل منطقة معيّنة من الصورة.

  • وبالطبع، هناك أيضًا بعض الأدوات والمكتبات التي يمكن أن تساعدك.

  • يمكنك البحث عنه على GitHub.

  • يستخدم بعض تطبيقات القصص المصوّرة أو بعض الخرائط هذه الميزة حاليًا.

  • ويمكنك استخدامها كمرجع.

  • بالإضافة إلى ذلك، نحن بشر، ونرتكب الأخطاء.

  • قد يخطئ الذكاء الاصطناعي أحيانًا.

  • لذا، لتجنّب ارتكاب الأخطاء، يمكننا أيضًا إضافة بعض القيود عند تحميل الصورة.

  • على سبيل المثال، في هذه الحالة، يمكنني أن أقول، "مرحبًا، دعنا نستخدم حجم النافذة أو بُعد الشاشة كحد أقصى ونضعه في دالة min، أو الحد الأدنى لعرض الطلب أو العرض المرتبط".

    // Query active window bounds instead of hardcoding 1080 / 1920
    val bounds = WindowMetricsCalculator.getOrCreate()
        .computeCurrentWindowMetrics(activity).bounds
    
    // Clamp target dimensions so Bitmap NEVER exceeds the active window
    val safeWidth  = minOf(requestedWidthPx,  bounds.width())
    val safeHeight = minOf(requestedHeightPx, bounds.height())
    
    val bitmap = Bitmap.createBitmap(safeWidth, safeHeight, config)
  • لذلك، باستخدام هذه الدالة، كلما قمت بتحميل صورة، لن تكون موجودة على الإطلاق في الحدود.

  • لذلك، لن يتم تحميل صورة أكبر من دقة الشاشة.

  • تذكَّر أنّه عليك فك ترميز المنطقة المرئية فقط من الصورة التي ستحفظ ذاكرتك.

إدارة الذاكرة والملفات المكرّرة وذاكرة التخزين المؤقت

  • Fung Lam

    ومرة أخرى، لنفترض أنّ هذا التطبيق ليس من مطوّر واحد.

  • في الواقع، إنّها مخصّصة لعدة مساهمين وقد يستخدمون أدوات مختلفة.

  • على سبيل المثال، جزء منه يستخدم عذرًا.

  • اسمحوا لي أن أبدأ من جديد.

  • وينطبق الأمر نفسه على هذا التطبيق.

  • مرحبًا، أنا في صفحة المنتج، لدي قائمة بالمنتجات ورأيت هذه الصورة، أضفتها إلى سلة التسوق.

  • بعد بضع دقائق، أذهب إلى الدفع، مهلاً، تظهر نفس الصورة مرة أخرى.

  • ولكن هل هذه هي الصورة نفسها أم أنّها في الواقع عنصران مختلفان في الذاكرة؟

  • نعم، من الأخطاء الشائعة التي رصدناها مثلاً أنّ التطبيق يحمّل الكثير من الصور.

  • ومع ذلك، لديهم أيضًا الكثير من الصور المكرّرة في الذاكرة.

  • إذا كنت تستخدم أداة تحميل الصور، يمكنك في الواقع تمرير مفتاح لإجراء التخزين المؤقت.

  • لذلك، عند استخدام الصورة نفسها، يمكنك استخدام كائن Bitmap الذي تم تخصيصه مسبقًا بدلاً من إنشاء كائن جديد.

  • في استوديو Android، يمكنك استخدام "محلّل الذاكرة" لمعرفة ما إذا كان تطبيقك يحتوي على صورة نقطية مكرّرة.

  • لقد تناولت هذا الموضوع في جلستي في مؤتمر Google I/O، لذا يمكنك الرجوع إلى YouTube لمشاهدة الفيديو.

  • وإذا لم أكن أستخدم استوديو Android، فهناك أيضًا أمر adb لالتقاط لقطة لأجزاء من الذاكرة، وبعد ذلك يمكنني استخدام لغة SQL للرسم البياني لأجزاء من الذاكرة في Perfetto للاستعلام عما إذا كان تطبيقي يحتوي على نسخة مكررة من Bitmap.

  • هذا مثال على رمز في Coil، يوضّح كيفية إضافة مفتاح لتخزين الصور النقطية مؤقتًا.

    // Strip volatile query tokens (?from=list / ?sig=...) for cache key
    val canonicalKey = MemoryCache.Key(productUrl.substringBefore("?"))
    
    // On Checkout Screen: reuse the 200x200 Bitmap already loaded in the List!
    AsyncImage(
        model = ImageRequest.Builder(context)
            .data(productUrl)
            .placeholderMemoryCacheKey(canonicalKey) // Instant zero-decode reuse!
            .memoryCacheKey(canonicalKey)
            .diskCacheKey(productUrl.substringBefore("?"))
            .build(),
        contentDescription = product.title
    )
  • والآن، نعم.

  • أعود إلى المثال السابق.

  • مرحبًا، تطبيقي له مطوّر مختلف.

  • يستخدم بعضها Views، لذا يتم استخدام Glide كأداة تحميل صور.

  • ويستخدم بعضهم Coil لأنّهم يستخدمون Compose.

  • لقد ذكرت للتو أنّ أداة تحميل الصور تساعدنا في التخزين المؤقت، فهل هناك أي مشكلة إذا كان لديّ أكثر من أداة تحميل صور واحدة؟

  • بالتأكيد، هناك خطأ ما.

  • بالنسبة إلى أداة تحميل الصور، سيتم تلقائيًا استخدام نسبة مئوية معيّنة من حجم ذاكرة الـ heap كحجم تلقائي لذاكرة التخزين المؤقت.

  • لذا، إذا كان لديك أداة تحميل صور واحدة، فسيتم استخدام نسبة مئوية من الذاكرة.

  • وإذا كان لديك برنامجا تحميل صور، ستتضاعف الذاكرة.

  • إذا كان لديك ثلاثة برامج تحميل صور، فستكون 3X.

  • وإذا لم يكن محمّل الصور يشارك المجموعة نفسها، سيحدث الشيء نفسه.

  • على سبيل المثال، في صفحة المنتج، يتم تحميل صورة باستخدام Glide، ثم في صفحة الدفع، يتم تحميل الصورة نفسها باستخدام Coil.

  • ومع ذلك، ستكون هناك صورتان في الذاكرة.

  • هذا ليس جيدًا.

  • نعم، لحلّ هذه المشكلة، من الأفضل استخدام أداة تحميل صور واحدة أو مجموعة مشتركة واحدة للتخزين المؤقت.

  • ولكن ماذا لو كان هذا مثل، أوه، الكثير من الأعمال الهندسية، لم أستطع القيام بذلك.

  • هناك حلّ أسهل، وهو قياس النسبة المئوية للصورة التي ستتم معالجتها بواسطة أداة تحميل الصور هذه، والنسبة المئوية للصورة التي ستتم معالجتها بواسطة أداة تحميل صور أخرى، وتحديد وزن لأدوات تحميل الصور المختلفة، تمامًا مثل هذا الرمز.

    val totalBudgetBytes = computeAdaptiveCacheBytes(context)
    val numberOfLoaders = 2 // e.g., Team A (Glide) + Team B (Coil)
    
    // Approach A (With Stats): Weight by image traffic (70% Team A / 30% Team B)
    val loaderABudget = (totalBudgetBytes * 0.70).toInt()
    val loaderBBudget = (totalBudgetBytes * 0.30).toInt()
    
    // Approach B (Simple Fix without stats): Divide by number of loaders!
    val simpleSplitBudget = totalBudgetBytes / numberOfLoaders
    
    MemoryCache.Builder()
        .maxSizeBytes(simpleSplitBudget)
        .build()
  • يمكنني ضبط loaderABudget على %70 وloaderBBudget على %30، وهذه طريقة أخرى.

  • إذا كنت لا تعرف حقًا مقدار الصورة التي سيتم التعامل معها بواسطة كل محمل، فلا يزال من الأفضل تقسيمها.

  • لديّ اثنان من أدوات تحميل الصور، لذا سأقسم على 2، وثلاثة من أدوات تحميل الصور، لذا سأقسم على 3 بدلاً من استخدام القيمة التلقائية لأنّ ذلك سيؤدي إلى الإفراط في استخدام الذاكرة.

  • ولنفترض أنّه في صفحة المنتج، نتوقع أن ينتقل المستخدم إلى الأسفل للاطّلاع على المزيد من المعلومات عن المنتج.

  • لذا، عادةً ما نقوم بتخزين حوالي 50 صورة أو 50 ميغابايت من الصور مؤقتًا.

  • ولكن ماذا لو تم نقل التطبيق إلى الخلفية؟

  • إذا كان التطبيق في الخلفية، قد لا يكون المستخدم يتصفّحه.

  • إذا لم يكن المستخدم يتصفح، فهل يجب أن أحتفظ بهذا المقدار من ذاكرة التخزين المؤقت في الذاكرة؟

  • لا، علينا إطلاقه.

  • نعم، يجب أن نلتزم بعملية الاستدعاء onTrimMemory.

  • نعم، في الواقع، لا تنفّذ العديد من التطبيقات حاليًا دالة الاستدعاء onTrimMemory أو لا تلتزم بها.

  • بالنسبة إلى دالة الاستدعاء onTrimMemory، هناك إشارتان يجب الانتباه إليهما.

  • الأول هو UI_HIDDEN، وهو يشبه ضغط المستخدم على زر الشاشة الرئيسية، وليس مباشرةً بعد الضغط على زر الشاشة الرئيسية، بل بعد فترة من الوقت بعد الضغط على زر الشاشة الرئيسية، ما يعني، "أوه، تطبيقك في حالة عدم وجود واجهة مستخدم مرئية للمستخدم".

  • ويمكننا أن نتوقع، "مرحبًا، المستخدم لا يستخدم تطبيقك.

  • يجب تحرير الموارد لنظام التشغيل".

  • لذلك يجب تقليص حجم ذاكرة التخزين المؤقت.

  • والآخر هو TRIM_MEMORY_BACKGROUND، ما يعني أنّ التطبيق يعمل في الخلفية منذ فترة طويلة، ويحتاج نظام التشغيل الآن إلى بعض الموارد.

  • في هذه المرحلة، يجب أن تفرغ ذاكرتك قدر الإمكان، بنسبة 100٪ تقريبًا.

  • من خلال القيام بذلك، نعم، يمكنك أيضًا تقليل فرصتك، وتقليل مخاطر قتل تطبيقك بواسطة نظام التشغيل.

  • لأنّه مثلاً، "إذا أرادوا ذاكرة، يمكنك منحهم إياها، ثم يتعامل نظام التشغيل معها".

  • إذا لم يكن الأمر كذلك، فلن يكون أمام نظام التشغيل خيار آخر، وعليه اتخاذ إجراء ما.

منع زيادة استخدام الذاكرة في الخلفية

  • Fung Lam

    "هناك خطأ كبير آخر ألاحظه، وهو مثلاً: "لقد ذكرتُ للتو أنّ تطبيقك يجب أن يستخدم ذاكرة أقل إذا كان يعمل في الخلفية".

  • ومع ذلك، رأيت تطبيقات تستخدم بالفعل المزيد من الذاكرة في الخلفية، والمزيد من الذاكرة.

  • لماذا؟

  • لهذا السبب أُجري البحث.

  • حاولتُ معرفة السبب.

  • وأحد الأسباب هو أنّ تطبيقهم يتضمّن بعض الصور المتحركة أو لوحة عرض دوّارة.

  • على سبيل المثال، يتم تنزيل صورة جديدة كل 15 ثانية، وعرضها للمستخدم".

  • ومع ذلك، لم يوقف المطوّر الصورة المتحركة عندما ينتقل التطبيق إلى الخلفية.

  • لذلك، حتى إذا كان التطبيق في الخلفية، سيستمر الرمز في العمل، وسيستمر في الحصول على صورة جديدة، وفك ترميز الصورة الجديدة، واستخدام المزيد من الذاكرة.

  • هذا الإجراء لا يُفترض أن يتّخذه التطبيق.

  • نعم

  • يجب إيقاف جميع الصور المتحركة أو التخزين المؤقت أو الأعمال غير ذات الصلة عندما ينتقل التطبيق إلى الخلفية.

  • لذا، إذا كنت على دراية بدورة حياة النشاط، فهناك حدث onPause وonStop، تأكَّد من تنفيذ onStop وonPause وonDestroyView لإيقاف الصورة المتحركة، نعم، لإيقاف طلبات سحب الصور أو أشياء أخرى تتعلق بالعمل الذي لا يحتاج إلى انتباه المستخدم.

    override fun onStop() {
        super.onStop()
        // 1. Stop animations so they don't decode next frames in background!
        bannerAnimationView.cancelAnimation()
        animatedImageDrawable?.stop()
    
        // 2. Cancel next-image buffering / prefetch queues!
        nextImagesBufferingJob?.cancel()
        Glide.with(this).pauseRequests()
    }
    
    override fun onDestroyView() {
        super.onDestroyView()
        bannerAnimationView.clearAnimation() // Release resources!
    }

إطار عمل 3R لصحة الذاكرة

  • Fung Lam

    أعلم أنّني ناقشت معك العديد من الطلبات، ولكنّني قلت: "لديّ أكثر من 15 طلبًا".

  • أعلم أنني إذا شاركت جميع الحالات معك، فقد تنسى كل شيء.

  • ستعود إلى المنزل وتبحث في Google عن "كيف يمكنني حلّ مشاكل الذاكرة؟"

  • لذا، أسهّل عليك تذكُّرها.

  • نعم

  • سأربطها بإطار عمل 3R المعروف جدًا لمساعدتك على حفظها.

  • أعتقد أنّ الجميع سمعوا عن إطار العمل هذا الذي يرتكز على 3 مبادئ.

  • الأول هو تقليل، كيفية تقليل الذاكرة عند تحميل صورة.

  • بالطبع، كما ذكرت، يمكننا تقليل دقة الصورة والحصول على الأبعاد الصحيحة لها عند استلامها من الخادم.

  • نحسّن عمق اللون أو نستخدم صورة نقطية للأجهزة إن أمكن.

  • وإذا لم تكن تستخدم شبكة توصيل محتوى، جرِّب استخدامها.

  • لذلك، عندما نحصل على الصورة من الخادم، فإننا نحصل فقط على الصورة التي تناسب الحجم بشكل أفضل.

  • إعادة الاستخدام: نستخدم ذاكرة التخزين المؤقت لإعادة استخدام الصورة التي تم تحميلها في الذاكرة، وبذلك لا نحصل على صورة نقطية مكرّرة في الذاكرة.

  • وبالطبع، حاول ألا يكون لديك عدة أدوات تحميل صور في الرمز الخاص بك، لأن ذلك أقل كفاءة.

  • التقليل، وإعادة الاستخدام، وما هو المبدأ المتبقي؟

  • هل تعلم؟

  • إعادة التدوير

  • هل تريد إعادة المحاولة؟

  • إعادة التدوير، نعم، إعادة التدوير.

  • نعم، لقد تعلمنا تقليل الاستخدام وإعادة الاستخدام وإعادة التدوير في المدرسة الابتدائية، على الرغم من أننا لا نتعلم البرمجة في المدرسة الابتدائية.

  • نعم، إعادة التدوير، نعم.

  • نعم، يتعلّق الأمر بكيفية تحرير الذاكرة لكي يتمكّن تطبيق آخر أو نظام التشغيل من استخدامها.

  • هناك أشياء مثل إعادة التدوير.

  • أولاً، ذكرتُ أنّ هناك حدث onTrimMemory، أي أنّ هناك إشارتَين سيرسلهما نظام التشغيل إليك عندما تكون واجهة المستخدم مخفية أو عندما يحتاج نظام التشغيل إلى الموارد.

  • نعم، حاوِل معالجة هذا الطلب.

  • وبالطبع، إذا كان لديك صورة متحركة أو أداة تحميل صور أو بعض الأعمال الأخرى التي تحدث في المقدّمة، تأكَّد من إيقافها عندما يكون هذا مثل حدث onPause، onStop.

  • لذا، قلِّل من استخدام الموارد، وأعِد استخدامها، وأعِد تدويرها.

  • من السهل تذكُّر ذلك، أليس كذلك؟

مجموعة أدوات التشخيص للمطوّرين والتحليل المستند إلى الذكاء الاصطناعي

  • Fung Lam

    سنعود الآن إلى الجزء الأقل إثارة للاهتمام في دليل التلفزيون.

  • كيف نستخدم مجموعة أدوات مختلفة للكشف عن مشكلة الذاكرة هذه وقياسها بفعالية؟

  • الأولى هي Google Play Console و"مؤشرات Android الحيوية" وReporting API.

  • نعم، يمكنك الاطّلاع على الشريحة المئوية الـ 90 في Google Play Console.

  • أين تتواجد الآن؟

  • أو يمكنك استخدام Reporting API إذا لم تكن تزور صفحة الويب.

  • سيظل بإمكانك استخدام واجهة برمجة التطبيقات للحصول على القيمة.

  • نعم، تأكَّد من أنّ الحدّ الأدنى هو 200 ميغابايت و400 ميغابايت.

  • تأكَّد من عدم تجاوز الحدّ الأقصى.

  • هناك أيضًا أمر ADB لمساعدتك في التقاط لقطة لأجزاء من الذاكرة أو إرسال حدث لمحاكاة حالة التطبيق، مثلاً، تطبيقي الآن في مرحلة UI_HIDDEN.

    adb shell dumpsys meminfo <pkg>  # watch Native Heap!
    adb shell am send-trim-memory <pkg> UI_HIDDEN
  • إذا كنت تستخدم "استوديو Android"، فهذا أمر جيد.

  • ومع ذلك، فإن هذا دائمًا صديق جيد لك.

  • يمكنك استخدام محلّل استوديو Android لالتقاط لقطة لأجزاء من الذاكرة، والحصول على واجهة مستخدم لمعرفة استخدام الذاكرة الفعلي وتخصيص الذاكرة الفعلي.

  • وبالطبع، نتحدث عن الذكاء الاصطناعي مع Gemini في "استوديو Android" أو مهارات Android الأخرى المتاحة.

  • تساعدك هذه الأداة في تحليل لقطة لأجزاء من الذاكرة واستخدام تتبُّعات Perfetto SQL للعثور على الصورة النقطية، وتحديد مالك ذاكرة التخزين المؤقت عادةً.

  • هل يتم تحميل صورتي من خلال Glide أو Coil؟

  • هل هناك أي تسرّب آخر للذاكرة في تطبيقي؟

  • أعلم أنّ بعضكم يقول: "أنا لا أستخدم بيئة التطوير المتكاملة (IDE) بعد الآن.

  • أنا مهندس طلبات.

  • أستخدمه، نعم، أرسل طلبًا إلى وكيل الذكاء الاصطناعي لكتابة الرمز البرمجي".

  • نعم، أعلم ذلك.

  • أقوم أيضًا ببعض الترميز البسيط خلال وقت فراغي.

  • لذا، أشارك هذا معك كنموذج فقط.

  • نعم، هذا مجرد جزء من الطلب الذي أستخدمه في بحثي عن 500 تطبيق.

  • نعم، جزء منه فقط.

  • أنت تعلم أن الشاشة ليست كبيرة.

  • إذا وضعت المزيد، فلن تتمكن من رؤيته.

  • ولكنّها تمنحك فكرة عن كيفية استخدام الذكاء الاصطناعي لإجراء عمليات التشغيل الآلي، وتسجيل استخدام الذاكرة، بما في ذلك لقطة لأجزاء من الذاكرة، وتحليلها وتقديم النتائج لك.

  • ويمكنني أن أضيف هنا جملة أخرى، "يُرجى أيضًا تزويدي بالرمز البرمجي الذي يحلّ المشكلة".

  • نعم، آمل أن يقلل هذا من مخاوفك، لنقل، "مرحبًا، الذاكرة مجال كبير.

  • لم أدرس نظام التشغيل أو بنية نظام الكمبيوتر في علوم الكمبيوتر في جامعتي.

  • لكن في الواقع، هذا ليس مخيفًا وليس صعبًا".

  • نعم، يسهّل ذلك تحليل تطبيقك.

  • يُرجى تجربتها.

  • ولكن بالطبع، سيرتكب الذكاء الاصطناعي أخطاء، لذا فإنّ المعرفة الأساسية حول كيفية استخدام الذاكرة وكيفية تخصيص نظام التشغيل للذاكرة لا تزال مهمة.

  • لهذا السبب، أدعوكم لحضور محاضرتي ومشاهدة الفيديو الخاص بنا على YouTube.

الخلاصات الرئيسية والملخّص

  • Fung Lam

    للتلخيص، لدينا بعض النقاط الرئيسية.

  • أولاً، التقليل.

  • تأكَّد من استخدام الحجم الصحيح للصورة.

  • تقسّم الصورة إلى صورة أصغر حتى لا يتم تحميل كل شيء على الذاكرة، نعم، فقط لتحميل الصورة المرئية للمستخدم في الذاكرة.

  • استخدِم bitmapConfig أو RGB_565 للأجهزة كلما أمكن ذلك.

  • إعادة الاستخدام، لذا تأكَّد من أنّ لديك استراتيجية للتخزين المؤقت، واستخدِم أداة تحميل الصور لمساعدتك في تقليل عدد الصور النقطية المكرّرة في الذاكرة.

  • وبالطبع، لا تستخدم صور Bitmap متعددة، حتى إذا حاولت استبدال حجم ذاكرة التخزين المؤقت التلقائي لأدوات تحميل الصور هذه.

  • ولا نعيد استخدامها أو إعادة تدويرها.

  • نعم

  • يجب أن تتكيّف إدارة الذاكرة في تطبيقك مع حالة التطبيق.

  • إذا كانت واجهة المستخدم مخفية، نعم، يجب استخدام ذاكرة أقل.

  • إذا طلب منك نظام التشغيل حقًا تحرير الذاكرة، فحرر أكبر قدر ممكن.

  • وإيقاف استنزاف الموارد، سواء كانت Bitmap أو طاقة أو موارد حوسبة.

  • نعم، أوقِف موقّت البانر والصورة المتحركة وتجميع الصور.

  • وأخيرًا، احرص على مراقبة الأداء.

  • انتقِل إلى Google Play Console بانتظام.

  • تأكَّد من أنّ هذه المقابلة لأغراض التدريب فقط.

  • نعم، سيساعدك هذا في فهم الحالة الحالية للشريحة المئوية الـ 90 من استخدام الذاكرة في تطبيقك لدى جميع المستخدمين.

  • سيساعدك ذلك أيضًا في تحديد المشكلة مبكرًا.

  • نعم

  • لإنشاء ملفات تعريف محليًا، يمكنك استخدام "محلّل الذاكرة" في "استوديو Android".

  • يمكننا أيضًا استخدام بعض أوامر ADB.

  • لدينا أيضًا مهارة Android.

  • ولكن تأكَّد من رغبتك في الاطّلاع على إحصاءات المستخدمين الحقيقيين، وستجدها في Google Play Console.

  • وهناك أيضًا واجهة برمجة التطبيقات Google Play Reporting API التي تتيح لك الحصول على هذه البيانات بدون الحاجة إلى تسجيل الدخول إلى Play Console.

  • كالعادة، لمزيد من المعلومات، يُرجى الانتقال إلى developer.android.com/memory.

  • يمكنك تقليل النفايات وإعادة استخدامها وتدويرها باستخدام هذا المنتج.

  • يمكننا إنشاء منظومة Android متكاملة أفضل، وآمل أن نتمكّن معًا من إنشاء منظومة Android متكاملة أفضل للتغلّب على التحديات التي نواجهها.

  • شكرًا لك، ومع أطيب التحيّات.

  • سيكون لديّ ساعات عمل قريبًا في الخارج، لذا إذا كانت لديك أسئلة، فلا تتردد في الحضور.

  • والمتحدث التالي حسنًا، لديه بعض الوقت، لذا يرجى انتظار المتحدث التالي على المسرح وشكرًا جزيلاً لك.

الرمز

مقتطفات الرمز البرمجي

مقتطف Kotlin عند الدقيقة 11:50 في "دراسات الحالة: تغيير حجم الصور النقطية وفك ترميزها"
// 1. Ask CDN for the exact UI card dimensions (e.g. 540x675)
val sizedUrl = "$cdnBaseUrl?w=$cardWidthPx&h=$cardHeightPx&fmt=webp"

// 2. Enforce exact downsampling + Hardware Bitmap config
AsyncImage(
    model = ImageRequest.Builder(LocalContext.current)
        .data(sizedUrl)
        .size(cardWidthPx, cardHeightPx) // Never use Size.ORIGINAL!
        .precision(Precision.EXACT)      // Downsample to exact card size
        .bitmapConfig(Bitmap.Config.HARDWARE)
        .build(),
    contentDescription = product.title
)
مقتطف Kotlin في الدقيقة 14:45 في "دراسات الحالة: تغيير حجم الصور النقطية وفك ترميزها"
// Query active window bounds instead of hardcoding 1080 / 1920
val bounds = WindowMetricsCalculator.getOrCreate()
    .computeCurrentWindowMetrics(activity).bounds

// Clamp target dimensions so Bitmap NEVER exceeds the active window
val safeWidth  = minOf(requestedWidthPx,  bounds.width())
val safeHeight = minOf(requestedHeightPx, bounds.height())

val bitmap = Bitmap.createBitmap(safeWidth, safeHeight, config)
مقتطف Kotlin في الدقيقة 16:51 في فيديو "إدارة الذاكرة والنسخ المكرّرة وذاكرة التخزين المؤقت"
// Strip volatile query tokens (?from=list / ?sig=...) for cache key
val canonicalKey = MemoryCache.Key(productUrl.substringBefore("?"))

// On Checkout Screen: reuse the 200x200 Bitmap already loaded in the List!
AsyncImage(
    model = ImageRequest.Builder(context)
        .data(productUrl)
        .placeholderMemoryCacheKey(canonicalKey) // Instant zero-decode reuse!
        .memoryCacheKey(canonicalKey)
        .diskCacheKey(productUrl.substringBefore("?"))
        .build(),
    contentDescription = product.title
)
مقتطف Kotlin في الدقيقة 18:17 في "إدارة الذاكرة والتكرارات وذاكرة التخزين المؤقت"
val totalBudgetBytes = computeAdaptiveCacheBytes(context)
val numberOfLoaders = 2 // e.g., Team A (Glide) + Team B (Coil)

// Approach A (With Stats): Weight by image traffic (70% Team A / 30% Team B)
val loaderABudget = (totalBudgetBytes * 0.70).toInt()
val loaderBBudget = (totalBudgetBytes * 0.30).toInt()

// Approach B (Simple Fix without stats): Divide by number of loaders!
val simpleSplitBudget = totalBudgetBytes / numberOfLoaders

MemoryCache.Builder()
    .maxSizeBytes(simpleSplitBudget)
    .build()
مقتطف Kotlin في الدقيقة 22:05 في "منع زيادة استخدام الذاكرة في الخلفية"
override fun onStop() {
    super.onStop()
    // 1. Stop animations so they don't decode next frames in background!
    bannerAnimationView.cancelAnimation()
    animatedImageDrawable?.stop()

    // 2. Cancel next-image buffering / prefetch queues!
    nextImagesBufferingJob?.cancel()
    Glide.with(this).pauseRequests()
}

override fun onDestroyView() {
    super.onDestroyView()
    bannerAnimationView.clearAnimation() // Release resources!
}
مقتطف Shell في الدقيقة 25:17 في فيديو "مجموعة أدوات التشخيص للمطوّرين والتحليل المستند إلى الذكاء الاصطناعي"
adb shell dumpsys meminfo <pkg>  # watch Native Heap!
adb shell am send-trim-memory <pkg> UI_HIDDEN