หมายเหตุ: ในกรณีส่วนใหญ่ เราจะแนะนำ ที่คุณใช้การเลื่อนผ่าน ไลบรารีเพื่อดึงข้อมูล ถอดรหัส และแสดงบิตแมปในแอปของคุณ เลื่อนแสดงนามธรรมส่วนใหญ่ ความซับซ้อนในการจัดการกับสิ่งเหล่านี้ งานอื่นๆ ที่เกี่ยวข้องกับการทำงานกับบิตแมปและรูปภาพอื่นๆ ใน Android สำหรับข้อมูลเกี่ยวกับการใช้และการดาวน์โหลดแบบเลื่อนผ่าน โปรดไปที่ ที่เก็บแบบเลื่อนผ่านใน GitHub
การโหลดบิตแมปเดียวในอินเทอร์เฟซผู้ใช้ (UI) สามารถทำได้โดยง่าย แต่จะได้รับมากกว่า
ซับซ้อนหากคุณต้องโหลดชุดรูปภาพขนาดใหญ่พร้อมกัน ในหลายกรณี (เช่น
เช่น ListView
, GridView
หรือ ViewPager
) จำนวนรูปภาพทั้งหมดบนหน้าจอที่รวมกับรูปภาพ
อาจเลื่อนหน้าจอไปได้อย่างไม่จำกัด
คอมโพเนนต์ลักษณะนี้จะช่วยประหยัดหน่วยความจําด้วยการนําวิวย่อยกลับมาใช้ใหม่เมื่อออกจากหน้าจอ นอกจากนี้ ตัวเก็บขยะยังช่วยเพิ่มพื้นที่ว่างสำหรับบิตแมปที่โหลดไว้ด้วย ในกรณีที่คุณไม่ได้เก็บข้อมูลอ้างอิงที่มีอายุการใช้งานยาวนานไว้ การดำเนินการนี้เป็นสิ่งที่ดี แต่คุณควรหลีกเลี่ยงการประมวลผลรูปภาพเหล่านี้อย่างต่อเนื่องทุกครั้งที่ปรากฏบนหน้าจอเพื่อให้ UI โหลดได้อย่างรวดเร็วและราบรื่น ความทรงจำ และดิสก์แคชมักมีประโยชน์ในส่วนนี้ โดยช่วยให้คอมโพเนนต์สามารถโหลดรูปภาพที่ประมวลผลแล้วซ้ำได้อย่างรวดเร็ว
บทเรียนนี้จะแนะนำวิธีใช้แคชบิตแมปของหน่วยความจำและดิสก์เพื่อปรับปรุงการตอบสนอง และความลื่นไหลของ UI เมื่อโหลดบิตแมปหลายรายการ
ใช้แคชหน่วยความจำ
แคชหน่วยความจำช่วยให้เข้าถึงบิตแมปได้อย่างรวดเร็วโดยไม่ต้องแลกกับการใช้งานแอปพลิเคชันอันมีค่า
ความทรงจำ คลาส LruCache
(ยังมีอยู่ในไลบรารีการสนับสนุนเพื่อนำมาใช้อีกครั้ง
API ระดับ 4) เหมาะสมอย่างยิ่งกับงานในการแคชบิตแมป เนื่องจากเพิ่ง
อ้างอิงออบเจ็กต์ใน LinkedHashMap
ที่มีการอ้างอิงอย่างเข้มงวดและนําออกน้อยที่สุด
สมาชิกที่ใช้ล่าสุดก่อนที่แคชจะมีขนาดเกินที่ระบุไว้
หมายเหตุ: ก่อนหน้านี้ การใช้งานแคชหน่วยความจำยอดนิยมคือ
อย่างไรก็ตาม SoftReference
หรือ WeakReference
แคชบิตแมป
เราไม่แนะนำให้ทำเช่นนั้น ตั้งแต่ Android 2.3 (API ระดับ 9) เป็นต้นไป ตัวเก็บขยะจะเก็บข้อมูลอ้างอิงแบบอ่อน/แบบอ่อนแออย่างหนักหน่วงมากขึ้น ซึ่งทำให้ข้อมูลอ้างอิงดังกล่าวมีประสิทธิภาพน้อยลง นอกจากนี้
รุ่นก่อนหน้า Android 3.0 (API ระดับ 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); }
หมายเหตุ: ในตัวอย่างนี้ 1 ใน 8 ของหน่วยความจำของแอปพลิเคชัน
ที่จัดสรรสำหรับแคชของเรา สำหรับอุปกรณ์ปกติ/hdpi มีขนาดอย่างน้อยประมาณ 4 MB (32/8) เต็ม
หน้าจอ GridView
ที่เต็มไปด้วยรูปภาพในอุปกรณ์ที่มีความละเอียด 800x480
ใช้ประมาณ 1.5 MB (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); }
หมายเหตุ: แม้แต่การเริ่มต้นแคชดิสก์ก็ต้องอาศัยการดำเนินการของดิสก์ ดังนั้นจึงไม่ควรเกิดขึ้นในเทรดหลัก แต่ก็หมายความว่ามีโอกาส มีการเข้าถึงแคชก่อนการเริ่มต้น ในการแก้ปัญหานี้ ในการใช้งานด้านบน การล็อก ทำให้มั่นใจได้ว่าแอปจะไม่อ่านจากดิสก์แคชจนกว่าแคชจะเสร็จสิ้น ได้เริ่มต้นแล้ว
ขณะตรวจสอบแคชหน่วยความจำในเทรด UI ระบบจะตรวจสอบดิสก์แคชในเบื้องหลัง ชุดข้อความ การดำเนินการกับดิสก์ไม่ควรเกิดขึ้นในเธรด UI เมื่อประมวลผลรูปภาพคือ เสร็จสมบูรณ์ จะมีการเพิ่มบิตแมปสุดท้ายลงในทั้งแคชหน่วยความจำและดิสก์แคชเพื่อการใช้งานในอนาคต
จัดการการเปลี่ยนแปลงการกำหนดค่า
การเปลี่ยนแปลงการกำหนดค่ารันไทม์ เช่น การเปลี่ยนการวางแนวหน้าจอ จะทำให้ 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
ไว้ คุณจะสังเกตเห็นว่ามีการหน่วงเวลาเพียงเล็กน้อยหรือไม่มีเลยเนื่องจากรูปภาพจะแสดงกิจกรรมเกือบทั้งหมด
จากหน่วยความจำได้ทันทีเมื่อเก็บแคชไว้ รูปภาพที่ไม่พบในแคชหน่วยความจำจะอยู่ในแคชดิสก์หรือไม่นั้นขึ้นอยู่กับสถานการณ์ หากไม่ ระบบจะประมวลผลรูปภาพดังกล่าวตามปกติ