【Android Developers Training】 58. 缓存位图
注:本文翻譯自Google官方的Android Developers Training文檔,譯者技術(shù)一般,由于喜愛安卓而產(chǎn)生了翻譯的念頭,純屬個(gè)人興趣愛好。
原文鏈接:http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
向你的應(yīng)用中加載一個(gè)單一的位圖是很直接的行為,然而當(dāng)你需要一次性加載一組圖像的大集合時(shí),事情會變得更加復(fù)雜。在很多情況下(比如對于ListView,GridView或者ViewPager),屏幕上顯示的圖片以及會因加載動(dòng)作而進(jìn)入屏幕的圖片,這兩者的總數(shù)加起來是無法限制的。
通過對移除屏幕區(qū)域的子View進(jìn)行回收,可以讓這類組件內(nèi)存使用降低下來。垃圾回收器也會對那些假定你將不再需要的引用對象進(jìn)行回收和釋放。這些措施都很好,但是為了保持流暢地和快速地加載UI,你會希望避免多次連續(xù)地處理這些圖片,當(dāng)它們回到屏幕區(qū)域中來時(shí)。一個(gè)存儲或磁盤緩存可以在這方面提供幫助,它可以讓組件迅速的重新加載處理過的圖片。
這節(jié)課將會教你使用一個(gè)存儲和磁盤緩存,來提升你的UI加載多個(gè)圖片時(shí)的響應(yīng)和流暢性。
一). 使用一個(gè)內(nèi)存緩存
一個(gè)內(nèi)存緩存提供了快速訪問位圖的方法,但它的代價(jià)是需要消耗掉珍貴的應(yīng)用內(nèi)存。LruCache類(在Support Library也有,可以支持到API Level 4及以上的平臺)對于緩存圖片來說尤其適合,它能將最近引用的對象存儲在一個(gè)基于強(qiáng)引用的LinkedHashMap中,并且在緩存超出它的特定大小后,將最近最遲被引用的對象去除。
Note:
在過去,一個(gè)流行的內(nèi)存緩存實(shí)現(xiàn)是SoftReference或者WeakReference的位圖緩存,然而,這并不是推薦的實(shí)現(xiàn)方法。從Android 2.3(API Level 9)開始,垃圾回收器對于軟引用和弱引用的回收變得更加地激進(jìn),從而使得它們的效用正在下降。從Android 3.0(API Level 11)開始,存儲于本機(jī)內(nèi)存的位圖數(shù)據(jù)并不是以一個(gè)可預(yù)測的形式釋放的,這就有潛在的可能性導(dǎo)致一個(gè)應(yīng)用超出它的內(nèi)存限制進(jìn)而崩潰。
為了為一個(gè)LruCache選擇合適的大小,一些因素需要考量,例如:
- 你的activity或應(yīng)用剩余的存儲壓力是如何的?
- 同一時(shí)間有多少應(yīng)用顯示在屏幕上?有多少需要準(zhǔn)備就緒顯示到屏幕上?
- 設(shè)備的屏幕的尺寸和密度的大小是多少?一個(gè)極高密度的屏幕(xhdpi)的設(shè)備(比如Galaxy Nexus)可能相對于其他比如hdpi的設(shè)備(比如Nexus S)需要更大的緩存來容納同樣數(shù)量的照片。
- 位圖文件的尺寸和屬性是怎樣的,需要消耗多少大的內(nèi)存空間?
- 圖片被訪問的頻率高不高?有沒有一些圖片被訪問你的頻率比其它的要高?如果有,也許你會期望讓這些項(xiàng)目一直保留在內(nèi)存或者為不同被訪問頻率的圖片設(shè)置多組LruCache對象。
- 能否做到數(shù)量和質(zhì)量間的平衡?有些時(shí)候存儲大量低質(zhì)量的圖片時(shí)很有用的,而將更高質(zhì)量的圖片加載任務(wù)放在后臺執(zhí)行。
沒有什么特定的大小或者公式能夠適合所有的應(yīng)用,你應(yīng)該自己分析并決定你的用法和解決方案。一個(gè)過小的緩存會導(dǎo)致大量無益處的執(zhí)行操作,而太大的緩存會導(dǎo)致java.lang.OutOfMemory異常,或者讓你剩下的應(yīng)用只有有限的存儲來工作。
下面是一個(gè)LruCache配置的樣例代碼:
private LruCache<String, Bitmap> mMemoryCache;@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;mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {@Overrideprotected 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) {mMemoryCache.put(key, bitmap);} }public Bitmap getBitmapFromMemCache(String key) {return mMemoryCache.get(key); }Note:
在這個(gè)例子中,八分之一的應(yīng)用內(nèi)存被分配給了我們的緩存。在一個(gè)標(biāo)準(zhǔn)或hdpi的設(shè)備上,這大約為4MB左右(32/8)。一個(gè)全屏的GridView,在一個(gè)分辨率為800x480的設(shè)備上,充滿圖片之后,會使用掉大約1.5MB(800*480*4字節(jié)),所以這個(gè)緩存至少大約能放下2.5個(gè)頁面數(shù)量的圖片在內(nèi)存中。
當(dāng)把一個(gè)圖片加載到ImageView時(shí),LruCache會先進(jìn)行檢查。如果找到了一個(gè)對應(yīng)的條目,那么它將會立即用來更新ImageView,否則的話一個(gè)后臺線程會啟動(dòng)并處理該圖像:
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也需要更新,并將相應(yīng)字段添加到內(nèi)存緩存中:
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {...// Decode image in background. @Overrideprotected Bitmap doInBackground(Integer... params) {final Bitmap bitmap = decodeSampledBitmapFromResource(getResources(), params[0], 100, 100));addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);return bitmap;}... }二). 使用磁盤緩存
一個(gè)內(nèi)存緩存對于加速訪問最近查看的位圖是很有效果的,然而你不能依賴于它,因?yàn)闊o法做到所有圖片都放置在該緩存中。如GridView這樣的組件其較大的數(shù)據(jù)集可以迅速填充內(nèi)存緩存。同時(shí),你的應(yīng)用可能會被另一個(gè)事務(wù)打斷,如一個(gè)來電,此時(shí)在后臺中,它可能會被殺掉,這樣的話內(nèi)存緩存就被銷毀了。一旦這個(gè)用戶恢復(fù)了,你的應(yīng)用不得不重新處理這些圖片。
一個(gè)磁盤緩存可以在這種情況下發(fā)揮效用,它能保持處理過的位圖文件,并減少在內(nèi)存緩存中不再可以獲得的加載時(shí)間。當(dāng)然,從磁盤獲取圖片比從內(nèi)存獲取圖片要慢,由于磁盤讀寫的速度有很多不確定性,故應(yīng)該在后臺線程中執(zhí)行。
Note:
一個(gè)ContentProvider是一個(gè)比較合適的存儲緩存圖片的地方,對于那些訪問頻率較高的圖片來說,例如在圖庫的應(yīng)用中。
下面的代碼使用了DiskLruCache的實(shí)現(xiàn),它來自于Android source。并且添加到內(nèi)存緩存的代碼中,更新其功能:
private DiskLruCache mDiskLruCache; private final Object mDiskCacheLock = new Object(); private boolean mDiskCacheStarting = 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 threadFile cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);new InitDiskCacheTask().execute(cacheDir);... }class InitDiskCacheTask extends AsyncTask<File, Void, Void> {@Overrideprotected Void doInBackground(File... params) {synchronized (mDiskCacheLock) {File cacheDir = params[0];mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);mDiskCacheStarting = false; // Finished initializationmDiskCacheLock.notifyAll(); // Wake any waiting threads }return null;} }class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {...// Decode image in background. @Overrideprotected Bitmap doInBackground(Integer... params) {final String imageKey = String.valueOf(params[0]);// Check disk cache in background threadBitmap bitmap = getBitmapFromDiskCache(imageKey);if (bitmap == null) { // Not found in disk cache// Process as normalfinal 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 beforeif (getBitmapFromMemCache(key) == null) {mMemoryCache.put(key, bitmap);}// Also add to disk cachesynchronized (mDiskCacheLock) {if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {mDiskLruCache.put(key, bitmap);}} }public Bitmap getBitmapFromDiskCache(String key) {synchronized (mDiskCacheLock) {// Wait while disk cache is started from background threadwhile (mDiskCacheStarting) {try {mDiskCacheLock.wait();} catch (InterruptedException e) {}}if (mDiskLruCache != null) {return mDiskLruCache.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 dirfinal String cachePath =Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :context.getCacheDir().getPath();return new File(cachePath + File.separator + uniqueName); }Note:
因?yàn)槌跏蓟疟P緩存也需要磁盤操作所以它也不能再主線程中執(zhí)行。然而,這其實(shí)意味著緩存有可能在還未初始化的時(shí)候就被訪問了。為了解決這個(gè)問題,在上面的代碼實(shí)現(xiàn)中,一個(gè)信號量(lock)保證了應(yīng)用會在初始化完成之后才去讀取緩存。
雖然內(nèi)存緩存在UI線程中檢查,磁盤緩存是在后臺線程中檢查。磁盤操作不應(yīng)該發(fā)生在UI線程中執(zhí)行。當(dāng)圖片處理完成了,最后位圖將會同時(shí)添加到內(nèi)存和磁盤緩存中,以備將來使用。
三). 處理配置變更
運(yùn)行時(shí)的配置變更,如屏幕方向變化,會導(dǎo)致Android銷毀當(dāng)前activity,并以新的配置重啟activity(可以閱讀:Handling Runtime Changes)。你一定希望避免重復(fù)處理圖像,這樣的話用戶就能在配置改變時(shí),擁有平滑快速地使用體驗(yàn)。
幸運(yùn)的是,你在之前的章節(jié)中,已經(jīng)擁有了一個(gè)很出色的圖片內(nèi)存緩存了。這個(gè)緩存可以通過使用一個(gè)Fragment(該Fragment通過調(diào)用setRetainInstance(true)將其自身保留),傳遞給新的activity實(shí)例。在activity重新創(chuàng)建之后,這個(gè)保留的Fragment就完成了重新依附(reattach),同時(shí)你獲得了現(xiàn)有緩存對象的訪問,允許圖片快速提取并填充到ImageView對象中。
下面是一個(gè)使用Fragment,在配置變更發(fā)生時(shí)保留LruCache對象的例子:
private LruCache<String, Bitmap> mMemoryCache;@Override protected void onCreate(Bundle savedInstanceState) {...RetainFragment retainFragment =RetainFragment.findOrCreateRetainFragment(getFragmentManager());mMemoryCache = retainFragment.mRetainedCache;if (mMemoryCache == null) {mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {... // Initialize cache here as usual }retainFragment.mRetainedCache = mMemoryCache;}... }class RetainFragment extends Fragment {private static final String TAG = "RetainFragment";public LruCache<String, Bitmap> mRetainedCache;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;}@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setRetainInstance(true);} }要測試這段代碼,嘗試分別在保留Fragment和不保留Fragment的情況下旋轉(zhuǎn)設(shè)備。你應(yīng)該能注意到當(dāng)保留了緩存時(shí),圖片填充到activity時(shí)幾乎沒有延遲。那些在內(nèi)存緩存中找不到的圖片一般都會在磁盤緩存中找到,如果找不到,這些圖片就會像平常一樣處理。
轉(zhuǎn)載于:https://www.cnblogs.com/jdneo/p/3522538.html
總結(jié)
以上是生活随笔為你收集整理的【Android Developers Training】 58. 缓存位图的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python临床数据_从临床试验中获取数
- 下一篇: 【转】Android加密算法:AES、B