মনে রাখবেন: বেশিরভাগ ক্ষেত্রে, আমরা সাজেস্ট করি যে আপনি নিজের অ্যাপে বিটম্যাপ ফেচ, ডিকোড ও দেখানোর জন্য Glide লাইব্রেরি ব্যবহার করুন। Glide, Android-এ বিটম্যাপ ও অন্যান্য ছবি নিয়ে কাজ করার সাথে সম্পর্কিত এইসব ও অন্যান্য টাস্ক ম্যানেজ করার বেশিরভাগ জটিলতা সংক্ষিপ্ত করে দেয়। Glide ব্যবহার ও ডাউনলোড করা সম্পর্কে তথ্য পেতে, GitHub-এ Glide রিপোজিটরি দেখুন।
আপনার ইউজার ইন্টারফেসে (UI) একটি বিটম্যাপ লোড করা সহজ, তবে আপনাকে একসাথে অনেক ছবি লোড করতে হলে
জিনিসগুলি আরও জটিল হয়ে ওঠে। অনেক ক্ষেত্রে (যেমন,
ListView, GridView বা ViewPager-এর মতো কম্পোনেন্টের ক্ষেত্রে), স্ক্রিনে দেখানো মোট ছবির সংখ্যা এবং স্ক্রিনে শীঘ্রই স্ক্রল করে আসতে পারে এমন ছবির সংখ্যা
আসলে সীমাহীন।
এই ধরনের কম্পোনেন্টের মাধ্যমে মেমরির ব্যবহার কম রাখা হয়। এর জন্য চাইল্ড ভিউ অফ-স্ক্রিনে চলে গেলে সেগুলিকে রিসাইকেল করা হয়। এছাড়াও, গার্বেজ কালেক্টর আপনার লোড করা বিটম্যাপ খালি করে দেয়, ধরে নেওয়া হয় যে আপনি কোনও দীর্ঘস্থায়ী রেফারেন্স রাখেননি। This is all good and well, but in order to keep a fluid and fast-loading UI you want to avoid continually processing these images each time they come back on-screen. মেমরি এবং ডিস্ক ক্যাশে প্রায়ই এক্ষেত্রে সাহায্য করতে পারে, যা কম্পোনেন্টকে প্রসেস করা ছবি দ্রুত রিলোড করার অনুমতি দেয়।
এই পাঠে আপনি শিখবেন কীভাবে মেমরি এবং ডিস্ক বিটম্যাপ ক্যাশে ব্যবহার করে একাধিক বিটম্যাপ লোড করার সময় আপনার UI-এর রেসপন্সিভনেস এবং ফ্লুইডিটি উন্নত করা যায়।
মেমরি ক্যাশে ব্যবহার করা
মেমরি ক্যাশে মূল্যবান অ্যাপ্লিকেশন মেমরি ব্যবহার করে বিটম্যাপে দ্রুত অ্যাক্সেস অফার করে। LruCache ক্লাস (API লেভেল ৪-এ ফিরে যাওয়ার জন্য সাপোর্ট লাইব্রেরিতেও উপলভ্য)
বিটম্যাপ ক্যাশে করার জন্য বিশেষভাবে উপযুক্ত, সম্প্রতি রেফারেন্স করা অবজেক্টকে একটি শক্তিশালী রেফারেন্স করা LinkedHashMap-এ রাখে এবং ক্যাশে তার নির্ধারিত সাইজ অতিক্রম করার আগে সবচেয়ে কম
সম্প্রতি ব্যবহৃত মেম্বারকে সরিয়ে দেয়।
মনে রাখবেন: অতীতে, একটি জনপ্রিয় মেমরি ক্যাশে বাস্তবায়ন ছিল একটি
SoftReference বা WeakReference বিটম্যাপ ক্যাশে, তবে
এটি সাজেস্ট করা হয় না। Android 2.3 (API লেভেল ৯) থেকে শুরু করে, গার্বেজ কালেক্টর সফট/উইক রেফারেন্স সংগ্রহ করার ব্যাপারে আরও
আগ্রাসী হয়ে উঠেছে, যার ফলে সেগুলি প্রায় অকার্যকর হয়ে পড়েছে। এছাড়াও,
Android 3.0 (API লেভেল ১১)-এর আগে, বিটম্যাপের ব্যাকআপ ডেটা নেটিভ মেমরিতে স্টোর করা হত যা
অনুমানযোগ্য উপায়ে রিলিজ করা হয় না, এর ফলে সম্ভাব্যভাবে কোনও অ্যাপ্লিকেশন মেমরির সীমা
সংক্ষিপ্তভাবে অতিক্রম করে ক্র্যাশ করতে পারে।
LruCache-এর জন্য উপযুক্ত সাইজ বেছে নিতে, বেশ কিছু বিষয়
বিবেচনা করা উচিত, যেমন:
- আপনার বাকি অ্যাক্টিভিটি এবং/অথবা অ্যাপ্লিকেশন কতটা মেমরি ইনটেনসিভ?
- একসাথে কতগুলি ছবি স্ক্রিনে দেখানো হবে? কতজনকে অন-স্ক্রিনে আসার জন্য উপলভ্য থাকতে হবে?
- ডিভাইসের স্ক্রিনের সাইজ ও ডেনসিটি কত? Galaxy Nexus-এর মতো অতিরিক্ত হাই ডেনসিটি স্ক্রিন (xhdpi) ডিভাইসের ক্ষেত্রে 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 } } ... }
জাভা
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 ডিভাইসে এটি ন্যূনতম প্রায় ৪ এমবি (৩২/৮)। ৮০০x৪৮০ রেজোলিউশন সহ ডিভাইসে ছবিতে ভর্তি একটি ফুল
স্ক্রিন GridView
প্রায় ১.৫ এমবি (৮০০*৪৮০*৪ বাইট) ব্যবহার করবে, তাই এটি মেমরিতে ছবির প্রায় ২.৫ পৃষ্ঠা
ক্যাশে করবে।
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 } }
জাভা
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) } } } ... }
জাভা
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) }
জাভা
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-কে নতুন কনফিগারেশনের সাথে চলমান অ্যাক্টিভিটি বন্ধ ও রিস্টার্ট করতে বাধ্য করে (এই আচরণ সম্পর্কে আরও তথ্যের জন্য, রানটাইম পরিবর্তন ম্যানেজ করা দেখুন)। আপনি আপনার সব ছবি আবার প্রসেস করা এড়াতে চান যাতে কনফিগারেশন পরিবর্তন হলে ব্যবহারকারী একটি মসৃণ এবং দ্রুত অভিজ্ঞতা পান।
ভাগ্যক্রমে, আপনার কাছে বিটম্যাপের একটি সুন্দর মেমরি ক্যাশে আছে যা আপনি মেমরি ক্যাশে ব্যবহার করুন বিভাগে তৈরি করেছেন। এই ক্যাশে Fragment ব্যবহার করে নতুন অ্যাক্টিভিটি ইনস্ট্যান্সে পাস করা যেতে পারে, যা setRetainInstance(true) কল করার মাধ্যমে সংরক্ষিত হয়। অ্যাক্টিভিটি আবার তৈরি করার পরে, এই রেখে দেওয়া Fragment আবার অ্যাটাচ করা হয় এবং আপনি আগে থেকে থাকা ক্যাশে অবজেক্টে অ্যাক্সেস পান, যার ফলে ছবি দ্রুত ফেচ করা এবং ImageView অবজেক্টে আবার পপুলেট করা যায়।
Fragment ব্যবহার করে কনফিগারেশন পরিবর্তন জুড়ে LruCache অবজেক্ট ধরে রাখার একটি উদাহরণ এখানে দেওয়া হল:
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 } }
জাভা
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 রেখে এবং না রেখে ডিভাইসটি ঘুরিয়ে দেখুন। আপনি ক্যাশে রেখে দিলে, মেমরি থেকে প্রায় সঙ্গে সঙ্গেই অ্যাক্টিভিটিতে ছবি লোড হয়ে যায়, তাই আপনি কোনও ল্যাগ দেখতে পাবেন না বা খুব সামান্য ল্যাগ দেখতে পাবেন। মেমরি ক্যাশেতে খুঁজে পাওয়া যায়নি এমন যেকোনও ছবি
ডিস্ক ক্যাশেতে উপলভ্য থাকে, যদি না থাকে, তাহলে সেগুলি স্বাভাবিকভাবে প্রসেস করা হয়।