تخزين الصور النقطية مؤقتًا

ملاحظة: في معظم الحالات، ننصح التي تستخدم فيها ميزة التمرير لجلب الصور النقطية وفك ترميزها وعرضها في التطبيق. يستبعد الوضع "التمرير السريع" معظم تعقيد التعامل مع هذه مهام أخرى تتعلق باستخدام الصور النقطية والصور الأخرى على 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 أو بدونه. من المفترض أن تلاحظ تأخيرًا قليلاً أو معدومًا لأن الصور تشغل النشاط تقريبًا من الذاكرة على الفور عند الاحتفاظ بذاكرة التخزين المؤقت. لن يتم العثور على أي صور في ذاكرة التخزين المؤقت نأمل أن تكون متاحة في ذاكرة التخزين المؤقت على القرص، وإلا، فستتم معالجتها كالمعتاد.