ملاحظة: في معظم الحالات، ننصح التي تستخدم فيها ميزة التمرير لجلب الصور النقطية وفك ترميزها وعرضها في التطبيق. يستبعد الوضع "التمرير السريع" معظم تعقيد التعامل مع هذه مهام أخرى تتعلق باستخدام الصور النقطية والصور الأخرى على Android. للحصول على معلومات حول استخدام تطبيق Glide وتنزيله، انتقِل إلى مستودع بالتمرير على GitHub.
من السهل تحميل صورة نقطية واحدة في واجهة المستخدم، ولكن تزداد الأمور
إذا كنت تحتاج إلى تحميل مجموعة أكبر من الصور دفعة واحدة. في كثير من الحالات (مثل مع
المكونات مثل ListView
أو GridView
أو ViewPager
)، وإجمالي عدد الصور التي تظهر على الشاشة مع الصور التي
التمرير إلى الشاشة غير محدودة في الأساس.
يتم تقليل استخدام الذاكرة باستخدام مكونات مثل هذه من خلال إعادة تدوير طرق العرض الثانوية أثناء تحركها خارج الشاشة. وتعمل أداة تجميع البيانات المهملة أيضًا على تفريغ الصور النقطية التي تم تحميلها، بافتراض أنك لا تحتفظ بأي منها مراجع طويلة الأمد. كل هذه الميزات جيدة ورائعة، ولكن للحفاظ على واجهة مستخدم سلسة وسريعة التحميل وتريد تجنب المعالجة المستمرة لهذه الصور في كل مرة تظهر فيها على الشاشة. ذكرى وذاكرة التخزين المؤقت على القرص يمكن أن يساعد في ذلك غالبًا، مما يسمح للمكونات بإعادة تحميل الصور التي تمت معالجتها بسرعة.
يرشدك هذا الدرس إلى كيفية استخدام ذاكرة التخزين المؤقت للصور النقطية للقرص وذاكرة التخزين المؤقت على القرص لتحسين سرعة الاستجابة وسلاسة واجهة المستخدم عند تحميل عدة صور نقطية.
استخدام ذاكرة تخزين مؤقت
توفّر ذاكرة التخزين المؤقت إمكانية الوصول السريع إلى الصور النقطية مقابل استهلاك تطبيقات قيّمة.
الذاكرة. فئة LruCache
(متوفّرة أيضًا في مكتبة الدعم لإعادة الاستخدام
إلى المستوى 4 من واجهة برمجة التطبيقات) مناسبة بشكل خاص لمهمة التخزين المؤقت للصور النقطية، والحفاظ على
الكائنات المشار إليها في LinkedHashMap
مرجعي قوي وإخراج الأثر الأقل
العضو المستخدم مؤخرًا قبل أن تتجاوز ذاكرة التخزين المؤقت الحجم المحدد.
ملاحظة: في الماضي، كان تنفيذ ذاكرة التخزين المؤقت الشائعة
في المقابل، إنّ ذاكرة التخزين المؤقت للصور النقطية في SoftReference
أو WeakReference
إلا أننا لا ننصح بذلك. بدءًا من Android 2.3 (مستوى واجهة برمجة التطبيقات 9)، ستكون أداة تجميع البيانات المهملة أكثر
سريع في جمع الإشارات إلى شخصية ضعيفة/ضعيفة، مما يجعله غير فعالة إلى حد ما. بالإضافة إلى ذلك،
قبل الإصدار Android 3.0 (مستوى واجهة برمجة التطبيقات 11)، تم تخزين البيانات الاحتياطية للصورة النقطية في ذاكرة أصلية والتي
لم يتم إصدارها بطريقة يمكن التنبؤ بها، مما قد يتسبب في تجاوز أحد التطبيقات لفترة وجيزة
حدود الذاكرة والأعطال.
لاختيار حجم مناسب لـ LruCache
، هناك عدد من العوامل.
في الاعتبار، على سبيل المثال:
- إلى أي مدى تستهلك الذاكرة المتبقية في نشاطك و/أو تطبيقك؟
- كم عدد الصور التي ستظهر على الشاشة في وقت واحد؟ عدد الشركات التي يجب أن تكون متاحة للحضور على الشاشة؟
- ما هو حجم شاشة الجهاز وكثافته؟ جهاز شاشة عالية الكثافة (xhdpi) مثل Galaxy Nexus ذاكرة تخزين مؤقت أكبر للاحتفاظ بنفس عدد الصور في الذاكرة مقارنةً بجهاز مثل Nexus S (hdpi).
- ما هي الأبعاد والتكوينات في الصور النقطية، وبالتالي حجم الذاكرة الذي ستستهلكه كل صورة
- كم مرة سيتم الوصول إلى الصور؟ هل سيتم الوصول إلى بعضها بشكل متكرر أكثر من غيرها؟
في هذه الحالة، ننصحك بالاحتفاظ بعناصر معيّنة في الذاكرة دائمًا أو استخدام عناصر
LruCache
متعدّدة لمجموعات مختلفة من الصور النقطية. - هل يمكنك تحقيق التوازن بين الجودة والكمية؟ في بعض الأحيان، قد يكون من المفيد أكثر تخزين عدد الصور النقطية المنخفضة الجودة، ما قد يؤدي إلى تحميل نسخة ذات جودة أعلى في مهمة خلفية.
لا يوجد حجم أو صيغة محددة تناسب جميع التطبيقات، فالأمر متروك لك لتحليل
الاستخدام والتوصل إلى حل مناسب. تتسبب ذاكرة التخزين المؤقت الصغيرة جدًا في زيادة النفقات العامة
لا فائدة، لأنّ ذاكرة التخزين المؤقت الكبيرة جدًا يمكن أن تتسبب مرة أخرى في حدوث java.lang.OutOfMemory
استثناء
واترك بقية التطبيق في ذاكرة صغيرة للعمل عليه.
في ما يلي مثال على إعداد LruCache
للصور النقطية:
Kotlin
private lateinit var memoryCache: LruCache<String, Bitmap> override fun onCreate(savedInstanceState: Bundle?) { ... // Get max available VM memory, exceeding this amount will throw an // OutOfMemory exception. Stored in kilobytes as LruCache takes an // int in its constructor. val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt() // Use 1/8th of the available memory for this memory cache. val cacheSize = maxMemory / 8 memoryCache = object : LruCache<String, Bitmap>(cacheSize) { override fun sizeOf(key: String, bitmap: Bitmap): Int { // The cache size will be measured in kilobytes rather than // number of items. return bitmap.byteCount / 1024 } } ... }
Java
private LruCache<String, Bitmap> memoryCache; @Override protected void onCreate(Bundle savedInstanceState) { ... // Get max available VM memory, exceeding this amount will throw an // OutOfMemory exception. Stored in kilobytes as LruCache takes an // int in its constructor. final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // Use 1/8th of the available memory for this memory cache. final int cacheSize = maxMemory / 8; memoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // The cache size will be measured in kilobytes rather than // number of items. return bitmap.getByteCount() / 1024; } }; ... } public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null) { memoryCache.put(key, bitmap); } } public Bitmap getBitmapFromMemCache(String key) { return memoryCache.get(key); }
ملاحظة: في هذا المثال، يوجد ثُمن ذاكرة التطبيق
المخصصة لذاكرة التخزين المؤقت لدينا. ولا يزيد حجم هذا الحجم على أي جهاز عادي/hdpi عن 4 ميغابايت (32/8). ملف
الشاشة GridView
التي تحتوي على صور على جهاز بدرجة دقة 800×480
تستهلك حوالي 1.5 ميغابايت (800*480*4 بايت)، لذا سيتم تخزين حوالي 2.5 صفحة من الصور في ذاكرة التخزين المؤقت
الذاكرة.
عند تحميل صورة نقطية في ImageView
، يتم عرض LruCache
أولاً. إذا تم العثور على إدخال، سيتم استخدامه على الفور لتعديل ImageView
، وإلّا تظهر سلسلة محادثات في الخلفية لمعالجة الصورة:
Kotlin
fun loadBitmap(resId: Int, imageView: ImageView) { val imageKey: String = resId.toString() val bitmap: Bitmap? = getBitmapFromMemCache(imageKey)?.also { mImageView.setImageBitmap(it) } ?: run { mImageView.setImageResource(R.drawable.image_placeholder) val task = BitmapWorkerTask() task.execute(resId) null } }
Java
public void loadBitmap(int resId, ImageView imageView) { final String imageKey = String.valueOf(resId); final Bitmap bitmap = getBitmapFromMemCache(imageKey); if (bitmap != null) { mImageView.setImageBitmap(bitmap); } else { mImageView.setImageResource(R.drawable.image_placeholder); BitmapWorkerTask task = new BitmapWorkerTask(mImageView); task.execute(resId); } }
يجب أن يكون BitmapWorkerTask
أيضًا
لإضافة إدخالات إلى ذاكرة التخزين المؤقت:
Kotlin
private inner class BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() { ... // Decode image in background. override fun doInBackground(vararg params: Int?): Bitmap? { return params[0]?.let { imageId -> decodeSampledBitmapFromResource(resources, imageId, 100, 100)?.also { bitmap -> addBitmapToMemoryCache(imageId.toString(), bitmap) } } } ... }
Java
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { ... // Decode image in background. @Override protected Bitmap doInBackground(Integer... params) { final Bitmap bitmap = decodeSampledBitmapFromResource( getResources(), params[0], 100, 100)); addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); return bitmap; } ... }
استخدام ذاكرة تخزين مؤقت على القرص
تكون ذاكرة التخزين المؤقت مفيدة في تسريع الوصول إلى الصور النقطية التي تم عرضها مؤخرًا، ولكن لا يمكنك
على الصور المتوفرة في ذاكرة التخزين المؤقت هذه. مكونات مثل GridView
مع
يمكن لمجموعات البيانات الأكبر أن تملأ بسهولة ذاكرة التخزين المؤقت. قد تتم مقاطعة تطبيقك من قِبل مستخدم آخر
لمهمة مثل إجراء مكالمة هاتفية، وقد يتم إنهاؤها في الخلفية وذاكرة التخزين المؤقت
وتدميرها. وبعد استئناف المستخدم، سيتعين على طلبك معالجة كل صورة مرة أخرى.
يمكن استخدام ذاكرة التخزين المؤقت على القرص في هذه الحالات للاحتفاظ بالصور النقطية التي تمت معالجتها والمساعدة في تقليل التحميل الأوقات التي تصبح فيها الصور غير متوفرة في ذاكرة التخزين المؤقت. وبالطبع، يؤدي جلب الصور من القرص يكون أبطأ من التحميل من الذاكرة ويجب أن يتم في سلسلة محادثات في الخلفية، وذلك لأن مدة قراءة القرص يمكن غير متوقع.
ملاحظة: قد يكون ContentProvider
أكثر
مكانًا مناسبًا لتخزين الصور المخزّنة مؤقتًا إذا كان يتم الوصول إليها بشكل متكرر، مثلاً في
معرض الصور.
يستخدم الرمز النموذجي لهذه الفئة طريقة تنفيذ DiskLruCache
يتم الحصول عليها من
مصدر Android.
في ما يلي مثال على رمز محدَّث يضيف ذاكرة التخزين المؤقت على القرص بالإضافة إلى ذاكرة التخزين المؤقت الحالية:
Kotlin
private const val DISK_CACHE_SIZE = 1024 * 1024 * 10 // 10MB private const val DISK_CACHE_SUBDIR = "thumbnails" ... private var diskLruCache: DiskLruCache? = null private val diskCacheLock = ReentrantLock() private val diskCacheLockCondition: Condition = diskCacheLock.newCondition() private var diskCacheStarting = true override fun onCreate(savedInstanceState: Bundle?) { ... // Initialize memory cache ... // Initialize disk cache on background thread val cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR) InitDiskCacheTask().execute(cacheDir) ... } internal inner class InitDiskCacheTask : AsyncTask<File, Void, Void>() { override fun doInBackground(vararg params: File): Void? { diskCacheLock.withLock { val cacheDir = params[0] diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE) diskCacheStarting = false // Finished initialization diskCacheLockCondition.signalAll() // Wake any waiting threads } return null } } internal inner class BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() { ... // Decode image in background. override fun doInBackground(vararg params: Int?): Bitmap? { val imageKey = params[0].toString() // Check disk cache in background thread return getBitmapFromDiskCache(imageKey) ?: // Not found in disk cache decodeSampledBitmapFromResource(resources, params[0], 100, 100) ?.also { // Add final bitmap to caches addBitmapToCache(imageKey, it) } } } fun addBitmapToCache(key: String, bitmap: Bitmap) { // Add to memory cache as before if (getBitmapFromMemCache(key) == null) { memoryCache.put(key, bitmap) } // Also add to disk cache synchronized(diskCacheLock) { diskLruCache?.apply { if (!containsKey(key)) { put(key, bitmap) } } } } fun getBitmapFromDiskCache(key: String): Bitmap? = diskCacheLock.withLock { // Wait while disk cache is started from background thread while (diskCacheStarting) { try { diskCacheLockCondition.await() } catch (e: InterruptedException) { } } return diskLruCache?.get(key) } // Creates a unique subdirectory of the designated app cache directory. Tries to use external // but if not mounted, falls back on internal storage. fun getDiskCacheDir(context: Context, uniqueName: String): File { // Check if media is mounted or storage is built-in, if so, try and use external cache dir // otherwise use internal cache dir val cachePath = if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState() || !isExternalStorageRemovable()) { context.externalCacheDir.path } else { context.cacheDir.path } return File(cachePath + File.separator + uniqueName) }
Java
private DiskLruCache diskLruCache; private final Object diskCacheLock = new Object(); private boolean diskCacheStarting = true; private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB private static final String DISK_CACHE_SUBDIR = "thumbnails"; @Override protected void onCreate(Bundle savedInstanceState) { ... // Initialize memory cache ... // Initialize disk cache on background thread File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR); new InitDiskCacheTask().execute(cacheDir); ... } class InitDiskCacheTask extends AsyncTask<File, Void, Void> { @Override protected Void doInBackground(File... params) { synchronized (diskCacheLock) { File cacheDir = params[0]; diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE); diskCacheStarting = false; // Finished initialization diskCacheLock.notifyAll(); // Wake any waiting threads } return null; } } class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { ... // Decode image in background. @Override protected Bitmap doInBackground(Integer... params) { final String imageKey = String.valueOf(params[0]); // Check disk cache in background thread Bitmap bitmap = getBitmapFromDiskCache(imageKey); if (bitmap == null) { // Not found in disk cache // Process as normal final Bitmap bitmap = decodeSampledBitmapFromResource( getResources(), params[0], 100, 100)); } // Add final bitmap to caches addBitmapToCache(imageKey, bitmap); return bitmap; } ... } public void addBitmapToCache(String key, Bitmap bitmap) { // Add to memory cache as before if (getBitmapFromMemCache(key) == null) { memoryCache.put(key, bitmap); } // Also add to disk cache synchronized (diskCacheLock) { if (diskLruCache != null && diskLruCache.get(key) == null) { diskLruCache.put(key, bitmap); } } } public Bitmap getBitmapFromDiskCache(String key) { synchronized (diskCacheLock) { // Wait while disk cache is started from background thread while (diskCacheStarting) { try { diskCacheLock.wait(); } catch (InterruptedException e) {} } if (diskLruCache != null) { return diskLruCache.get(key); } } return null; } // Creates a unique subdirectory of the designated app cache directory. Tries to use external // but if not mounted, falls back on internal storage. public static File getDiskCacheDir(Context context, String uniqueName) { // Check if media is mounted or storage is built-in, if so, try and use external cache dir // otherwise use internal cache dir final String cachePath = Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() : context.getCacheDir().getPath(); return new File(cachePath + File.separator + uniqueName); }
ملاحظة: حتى تهيئة ذاكرة التخزين المؤقت على القرص يتطلب إجراء عمليات على القرص وبالتالي يجب ألا تتم في سلسلة التعليمات الرئيسية ومع ذلك، هذا يعني أن هناك فرصة يتم الوصول إلى ذاكرة التخزين المؤقت قبل الإعداد. لمعالجة هذا الأمر، في التنفيذ أعلاه، سيؤدي استخدام قفل بأن التطبيق لا يقرأ من ذاكرة التخزين المؤقت على القرص حتى يتم الاحتفاظ التهيئة.
أثناء التحقّق من ذاكرة التخزين المؤقت في سلسلة واجهة المستخدم، يتم التحقّق من ذاكرة التخزين المؤقت على القرص في الخلفية. . يجب ألا تحدث عمليات القرص مطلقًا في سلسلة واجهة المستخدم. عند معالجة الصور مكتملة، تتم إضافة الصورة النقطية النهائية إلى كل من الذاكرة وذاكرة التخزين المؤقت على القرص لاستخدامها مستقبلاً.
التعامل مع تغييرات الإعدادات
تؤدي التغييرات في إعدادات بيئة التشغيل، مثل تغيير اتجاه الشاشة، إلى تدمير جهاز Android أعِد تشغيل النشاط الجاري باستخدام الإعداد الجديد (لمزيد من المعلومات حول هذا السلوك، راجِع التعامل مع التغييرات في وقت التشغيل). كنت تريد تجنب الاضطرار إلى معالجة جميع صورك مرة أخرى حتى يحصل المستخدم على تجربة سلسة وسريعة تجربة المستخدم عند حدوث تغيير في التهيئة.
لحسن الحظ، لديك ذاكرة تخزين مؤقت رائعة للصور النقطية التي أنشأتها في قسم استخدام ذاكرة التخزين المؤقت للذاكرة. يمكن تمرير ذاكرة التخزين المؤقت هذه إلى
مثيل نشاط باستخدام Fragment
، يتم الاحتفاظ به من خلال استدعاء setRetainInstance(true)
. بعد أن يكون النشاط
تمت إعادة إنشاء هذا Fragment
الذي تم الاحتفاظ به، ويمكنك الوصول إلى
كائن ذاكرة التخزين المؤقت الحالي، ما يسمح بجلب الصور وإعادة تعبئتها بسرعة في كائنات ImageView
في ما يلي مثال على الاحتفاظ بعنصر LruCache
في جميع الإعدادات.
التغييرات باستخدام Fragment
:
Kotlin
private const val TAG = "RetainFragment" ... private lateinit var mMemoryCache: LruCache<String, Bitmap> override fun onCreate(savedInstanceState: Bundle?) { ... val retainFragment = RetainFragment.findOrCreateRetainFragment(supportFragmentManager) mMemoryCache = retainFragment.retainedCache ?: run { LruCache<String, Bitmap>(cacheSize).also { memoryCache -> ... // Initialize cache here as usual retainFragment.retainedCache = memoryCache } } ... } class RetainFragment : Fragment() { var retainedCache: LruCache<String, Bitmap>? = null companion object { fun findOrCreateRetainFragment(fm: FragmentManager): RetainFragment { return (fm.findFragmentByTag(TAG) as? RetainFragment) ?: run { RetainFragment().also { fm.beginTransaction().add(it, TAG).commit() } } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) retainInstance = true } }
Java
private LruCache<String, Bitmap> memoryCache; @Override protected void onCreate(Bundle savedInstanceState) { ... RetainFragment retainFragment = RetainFragment.findOrCreateRetainFragment(getFragmentManager()); memoryCache = retainFragment.retainedCache; if (memoryCache == null) { memoryCache = new LruCache<String, Bitmap>(cacheSize) { ... // Initialize cache here as usual } retainFragment.retainedCache = memoryCache; } ... } class RetainFragment extends Fragment { private static final String TAG = "RetainFragment"; public LruCache<String, Bitmap> retainedCache; public RetainFragment() {} public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) { RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG); if (fragment == null) { fragment = new RetainFragment(); fm.beginTransaction().add(fragment, TAG).commit(); } return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } }
لاختبار ذلك، جرّب تدوير الجهاز مع الاحتفاظ بـ Fragment
أو بدونه. من المفترض أن تلاحظ تأخيرًا قليلاً أو معدومًا لأن الصور تشغل النشاط تقريبًا
من الذاكرة على الفور عند الاحتفاظ بذاكرة التخزين المؤقت. لن يتم العثور على أي صور في ذاكرة التخزين المؤقت
نأمل أن تكون متاحة في ذاكرة التخزين المؤقت على القرص، وإلا، فستتم معالجتها كالمعتاد.