הערה: ברוב המקרים, אנחנו ממליצים שמשתמשים בהחלקה כדי לאחזר, לפענח ולהציג מפות סיביות באפליקציה שלך. בהחלקה מופשטים רוב התוכן את המורכבות בטיפול משימות אחרות שקשורות לעבודה עם מפות סיביות ותמונות אחרות ב-Android. לקבלת מידע על השימוש בהחלקה וההורדה שלה, אפשר להיכנס אל מאגר גלישה ב-GitHub.
הטעינה של מפת סיביות יחידה לממשק המשתמש (UI) היא פשוטה, אבל הפעולות נעשים יותר
מורכבת אם צריך לטעון קבוצה גדולה יותר של תמונות בבת אחת. במקרים רבים (למשל עם רכיבים כמו ListView
, GridView
או ViewPager
), אין הגבלה על המספר הכולל של התמונות במסך בשילוב עם תמונות שעשויות לגלול למסך בקרוב.
השימוש בזיכרון נשמר ברכיבים כמו זה על ידי מיחזור של תצוגות הצאצא בזמן שהן זזות אל מחוץ למסך. אספן האשפה גם משחרר את מפות הביטים הטעונות, בהנחה שלא שמרת הפניות לטווח ארוך. כל זה טוב, אבל כדי לשמור על ממשק משתמש יציב שנטען במהירות כדאי להימנע מעיבוד מתמשך של התמונות האלה בכל פעם שהן מוצגות במסך. במקרים כאלה, מטמון בזיכרון ובדיסק יכול לעזור, ולאפשר לרכיבים לטעון מחדש במהירות תמונות שעברו עיבוד.
בשיעור הזה תלמדו איך להשתמש במטמון של מפת סיביות (bitmap) בזיכרון ובדיסק כדי לשפר את הרספונסיביות והגמישות של ממשק המשתמש כשטוענים מספר מפות סיביות.
שימוש במטמון זיכרון
מטמון זיכרון מאפשר גישה מהירה למפות ביטים בתשלום של שימוש באפליקציות חשובות
זיכרון. הכיתה LruCache
(שזמינה גם ב-Support Library לשימוש עד לרמה 4 של API) מתאימה במיוחד למשימות של שמירת מטמון של בימפטים, שמירה של אובייקטים שהופנו אליהם לאחרונה ב-LinkedHashMap
עם הפניה חזקה והוצאה של המשתנה שלא נעשה בו שימוש לאחרונה לפני שהמטמון חורג מהגודל הייעודי שלו.
הערה: בעבר, יישום פופולרי של מטמון זיכרון היה
עם זאת, מטמון מפת סיביות של SoftReference
או WeakReference
מומלץ לא לעשות זאת. החל מ-Android 2.3 (רמת API 9), איסוף האשפה יעיל יותר
אגרסיבית באיסוף הפניות רכות/חלשות, ולכן הן לא יעילות למדי. In addition,
לפני 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); }
הערה: בדוגמה הזו, שמונה חלקים מזיכרון האפליקציה הוקצו למטמון שלנו. במכשיר רגיל/hdpi, הגודל המינימלי הוא כ-4MB (32/8). A מלא
מסך GridView
מלא בתמונות במכשיר ברזולוציה של 800x480
היא בגודל 1.5MB (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); }
הערה: גם לאינטליקציה של מטמון הדיסק נדרשות פעולות דיסק, ולכן אסור לבצע אותה ב-thread הראשי. עם זאת, המשמעות היא שיש סיכוי שתתבצע גישה למטמון לפני האתחול. כדי לטפל בבעיה, בהטמעה שצוינה למעלה, אובייקט מבטיח שהאפליקציה לא תקרא ממטמון הדיסק עד שהמטמון אותחל.
כשמטמון הזיכרון נבדק ב-thread של ממשק המשתמש, מטמון הדיסק נבדק ברקע של שרשור. אסור לבצע פעולות בדיסק בשרשור של ממשק המשתמש. כאשר עיבוד התמונה הוא מפת הביטים הסופית מתווספת גם לזיכרון וגם למטמון הדיסק לשימוש עתידי.
טיפול בשינויים בהגדרות
שינויים בתצורה של זמן הריצה, כגון שינוי בכיוון המסך, גורמים ל-Android להרוס מפעילים מחדש את פעילות הריצה עם ההגדרה החדשה (למידע נוסף על ההתנהגות הזו, כדאי לעיין במאמר טיפול בשינויים בזמן ריצה. אתם לא רוצים שתצטרכו לעבד שוב את כל התמונות, כדי שהמשתמשים ייהנו מחוויית שימוש חלקה ומהירה כשמתרחש שינוי בהגדרות.
למזלכם, יש לכם מטמון זיכרון נחמד של קובצי bitmap שיצרתם בקטע שימוש במטמון זיכרון. ניתן להעביר את המטמון החדש
מופע פעילות באמצעות 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
וגם בלי להשאיר אותו. אמור להיות עיכוב קטן או אפסי, כי התמונות מאכלסות את הפעילות כמעט
באופן מיידי מהזיכרון, כששומרים את המטמון. אם יש תמונות שלא נמצאו במטמון הזיכרון
הם זמינים במטמון הדיסק. אם לא, הם מעובדים כרגיל.