ذخیره نقشه های بیتی

توجه: در بیشتر موارد، توصیه می کنیم از کتابخانه Glide برای واکشی، رمزگشایی و نمایش بیت مپ ها در برنامه خود استفاده کنید. Glide بیشتر پیچیدگی های مربوط به انجام این وظایف و کارهای دیگر مربوط به کار با بیت مپ و سایر تصاویر را در اندروید خلاصه می کند. برای اطلاعات در مورد استفاده و دانلود Glide، از مخزن Glide در GitHub دیدن کنید.

بارگذاری یک بیت مپ در رابط کاربری (UI) ساده است، با این حال اگر نیاز به بارگیری مجموعه بزرگ‌تری از تصاویر به طور همزمان داشته باشید، همه چیز پیچیده‌تر می‌شود. در بسیاری از موارد (مانند اجزایی مانند ListView ، GridView یا ViewPager )، تعداد کل تصاویر روی صفحه همراه با تصاویری که ممکن است به زودی روی صفحه اسکرول شوند اساساً نامحدود است.

با بازیافت نماهای کودک در حین جابجایی آنها از صفحه نمایش، استفاده از حافظه با قطعاتی مانند این کاهش می یابد. جمع‌آورنده زباله همچنین نقشه‌های بیت بارگذاری شده شما را آزاد می‌کند، با این فرض که هیچ مرجعی با عمر طولانی نگه ندارید. همه اینها خوب و خوب است، اما برای حفظ یک رابط کاربری روان و بارگذاری سریع، می‌خواهید از پردازش مداوم این تصاویر هر بار که به صفحه نمایش بازمی‌گردند اجتناب کنید. حافظه پنهان و حافظه پنهان دیسک اغلب می تواند در اینجا کمک کند و به اجزا اجازه می دهد تا به سرعت تصاویر پردازش شده را بارگیری مجدد کنند.

این درس شما را با استفاده از حافظه پنهان و بیت مپ دیسک راهنمایی می کند تا پاسخگویی و روان بودن رابط کاربری خود را هنگام بارگیری چند بیت مپ بهبود بخشد.

از حافظه کش استفاده کنید

یک حافظه کش دسترسی سریع به بیت مپ ها را به قیمت اشغال حافظه کاربردی برنامه ارائه می دهد. کلاس LruCache (همچنین در کتابخانه پشتیبانی برای استفاده در API سطح 4 موجود است) به ویژه برای وظیفه ذخیره بیت مپ ها، نگهداری اشیاء اخیراً ارجاع داده شده در یک LinkedHashMap قوی و خارج کردن عضوی که اخیراً استفاده شده است قبل از اینکه حافظه پنهان از حد خود فراتر رود، مناسب است. اندازه تعیین شده

توجه: در گذشته، یک اجرای کش حافظه رایج، یک کش بیت مپ SoftReference یا WeakReference بود، اما این توصیه نمی شود. از Android 2.3 (API Level 9) شروع به جمع‌آوری زباله با جمع‌آوری مراجع نرم/ضعیف می‌کند که آنها را نسبتاً بی‌اثر می‌کند. علاوه بر این، قبل از اندروید 3.0 (سطح API 11)، داده‌های پشتیبان یک بیت مپ در حافظه اصلی ذخیره می‌شد که به روشی قابل پیش‌بینی منتشر نمی‌شد، و به طور بالقوه باعث می‌شد که برنامه برای مدت کوتاهی از محدودیت‌های حافظه خود فراتر رفته و از کار بیفتد.

برای انتخاب اندازه مناسب برای LruCache ، باید تعدادی از عوامل را در نظر گرفت، به عنوان مثال:

  • بقیه فعالیت ها و/یا برنامه شما چقدر حافظه فشرده است؟
  • چند تصویر به طور همزمان روی صفحه نمایش داده می شود؟ چه تعداد باید آماده باشند تا روی صفحه نمایش داده شوند؟
  • اندازه و تراکم صفحه نمایش دستگاه چقدر است؟ یک دستگاه صفحه نمایش با چگالی بالا (xhdpi) مانند Galaxy Nexus در مقایسه با دستگاهی مانند Nexus S (hdpi) به حافظه پنهان بزرگتری برای نگهداری همان تعداد تصویر در حافظه نیاز دارد.
  • بیت مپ ها چه ابعاد و پیکربندی دارند و بنابراین هر کدام چقدر حافظه اشغال می کنند؟
  • هر چند وقت یکبار به تصاویر دسترسی خواهید داشت؟ آیا دسترسی به برخی از آنها بیشتر از دیگران خواهد بود؟ اگر چنین است، شاید بخواهید موارد خاصی را همیشه در حافظه نگه دارید یا حتی چندین شی LruCache برای گروه های مختلف بیت مپ داشته باشید.
  • آیا می توانید بین کیفیت و کمیت تعادل ایجاد کنید؟ گاهی اوقات ذخیره تعداد بیشتری بیت مپ با کیفیت پایین تر می تواند مفیدتر باشد و به طور بالقوه یک نسخه با کیفیت بالاتر را در کار پس زمینه دیگری بارگیری می کند.

هیچ اندازه یا فرمول خاصی وجود ندارد که برای همه برنامه ها مناسب باشد، این شما هستید که استفاده خود را تجزیه و تحلیل کنید و راه حل مناسبی ارائه دهید. حافظه نهان خیلی کوچک باعث سربار اضافی بدون هیچ مزیتی می شود، حافظه نهان بیش از حد بزرگ می تواند یک بار دیگر باعث استثناهای java.lang.OutOfMemory شود و بقیه حافظه برنامه شما را برای کار با کمی باقی بگذارد.

در اینجا نمونه ای از راه اندازی LruCache برای بیت مپ آورده شده است:

کاتلین

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
        }
    }
    ...
}

جاوا

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 تمام صفحه پر از تصاویر در دستگاهی با رزولوشن 800x480 حدود 1.5 مگابایت (800*480*4 بایت) استفاده می کند، بنابراین حداقل حدود 2.5 صفحه از تصاویر را در حافظه پنهان می کند.

هنگام بارگذاری یک بیت مپ در ImageView ، ابتدا LruCache بررسی می شود. اگر ورودی پیدا شد، بلافاصله برای به روز رسانی ImageView استفاده می شود، در غیر این صورت یک رشته پس زمینه برای پردازش تصویر ایجاد می شود:

کاتلین

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
    }
}

جاوا

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 همچنین برای افزودن ورودی ها به حافظه نهان باید به روز شود:

کاتلین

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)
            }
        }
    }
    ...
}

جاوا

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 استخراج شده است. در اینجا کد نمونه به روز شده ای وجود دارد که یک کش دیسک را علاوه بر حافظه نهان موجود اضافه می کند:

کاتلین

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)
}

جاوا

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);
}

نکته: حتی مقداردهی اولیه کش دیسک نیز به عملیات دیسک نیاز دارد و بنابراین نباید در رشته اصلی انجام شود. با این حال، این به این معنی است که این احتمال وجود دارد که قبل از مقداردهی اولیه به حافظه پنهان دسترسی پیدا کند. برای رفع این مشکل، در پیاده سازی فوق، یک شی قفل تضمین می کند که برنامه از حافظه پنهان دیسک تا زمانی که کش اولیه نشده است، نمی خواند.

در حالی که کش حافظه در رشته UI بررسی می شود، کش دیسک در رشته پس زمینه بررسی می شود. عملیات دیسک هرگز نباید روی رشته UI انجام شود. هنگامی که پردازش تصویر کامل شد، بیت مپ نهایی برای استفاده در آینده هم به حافظه و هم به کش دیسک اضافه می شود.

کنترل تغییرات پیکربندی

تغییرات پیکربندی زمان اجرا، مانند تغییر جهت صفحه، باعث می شود Android فعالیت در حال اجرا را با پیکربندی جدید از بین ببرد و دوباره راه اندازی کند (برای اطلاعات بیشتر در مورد این رفتار، به مدیریت تغییرات زمان اجرا مراجعه کنید). شما می خواهید از پردازش مجدد همه تصاویر خود اجتناب کنید تا کاربر تجربه ای روان و سریع در هنگام تغییر پیکربندی داشته باشد.

خوشبختانه، شما یک حافظه کش خوب از بیت مپ ها دارید که در بخش Use a Memory Cache ساخته اید. این کش را می توان با استفاده از یک Fragment که با فراخوانی setRetainInstance(true) حفظ می شود، به نمونه اکتیویتی جدید منتقل شود. پس از ایجاد مجدد فعالیت، این Fragment حفظ شده مجدداً متصل می‌شود و شما به شیء حافظه پنهان موجود دسترسی پیدا می‌کنید، که اجازه می‌دهد تصاویر به سرعت واکشی شوند و دوباره در اشیاء ImageView جمع شوند.

در اینجا مثالی از حفظ یک شی LruCache در تغییرات پیکربندی با استفاده از یک Fragment آورده شده است:

کاتلین

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
    }
}

جاوا

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 بچرخانید. شما باید متوجه تاخیر کمی یا بدون تاخیر باشید زیرا تصاویر تقریباً بلافاصله فعالیت را از حافظه پر می کنند وقتی حافظه پنهان را حفظ می کنید. هر تصویری که در حافظه پنهان یافت نشد، امیدواریم در حافظه پنهان دیسک موجود باشد، اگر نه، طبق معمول پردازش می شود.