編輯:關於Android編程
原文地址:http://android.xsoftlab.net/training/displaying-bitmaps/cache-bitmap.html
往UI界面中加載單張圖片的過程是很簡單的,然而如果需要在某個時刻同時加載大量的圖片,那麼這事情就有些復雜了。在很多情況下,比如使用了ListView、GridView或者是ViewPager來展示一定數量的圖片,在本質上這些情況下,屏幕的快速滑動會導致大量的圖片被集中展示在屏幕上。
類似這樣通過回收移除到屏幕之外的子View的組件會抑制內存的使用(也就是說它們本身不會濫用內存)。垃圾回收器還會釋放你所加載的位圖,假設你沒有使用任何持久化引用的話。這真是極好的,但是為了保持流暢的UI效果,你可能需要在它們每次重新返回到屏幕的時候,對它們按照常規的方式重新處理。內存緩存及磁盤緩存可以在這裡提供幫助,可以使這些組件快速的重新加載已經處理過的圖片。
這節課將會討論在加載多張圖片的時候,如何通過使用內存緩存以及磁盤緩存來使UI界面更加流暢,響應速度更快。
內存緩存提供了一種快速訪問位圖的能力,不過這會花費寶貴的內存空間。類LruCache極其適合用來處理緩存圖片的任務,它會將最近使用到的位圖的引用存放在一個LinkedHashMap對象上,並會在超過內存設計大小之前將最後一個沒有用到的成員給驅除。
Note: 在過去,使用SoftReference或者是WeakReference來緩存圖片是最受歡迎的一種緩存方式,然而卻並不推薦這麼用。在Android 2.3之後,垃圾回收器對soft/weak引用的回收更加強制,這會使得這些引用幾乎無效。此外,在Android 3.0之前,位圖的字節數據被存儲在本地內存中,可以預見這些數據是不會被釋放的,這會導致程序很容易超過自身的內存限制,然後崩潰。
為了給LruCache選擇合適的尺寸,有幾個因素應該被考慮在內:
Activity或者程序在常規狀態下的內存使用量是多少? 在同一時間最多會有多少圖片集中顯示在屏幕上?有多少內存需要為准備顯示到屏幕上的圖片所用? 設備屏幕的大小和尺寸分別是多少?在加載相同圖片數量的情況下,像Galaxy Nexus這種超高的密度(xhdpi)的設備與Nexus S(hdpi)相比則需要更大的內存。 圖片的尺寸多大?配置是什麼?加載這個位圖的時候需要花費的內存是多少? 圖片的訪問有多頻繁?會比其它位圖訪問更頻繁嗎?如果是這樣,可能你需要將它們永遠保持在內存中了,或者甚至是有多個LruCache對象來為圖片分組。 你可以在數量與質量之間取得平衡嗎?某些時候存儲大量的低質圖片是很有用處的,可能會潛在的存在一些後台任務來加載一些高質量的版本。這裡特別沒有指定尺寸或者配置,不過這適用所有的應用程序,這取決於對內存使用情況的分析,並需要找到一個適合的解決方案。緩存設置的太小會導致無意義的額外開銷,緩存設置的太大會再次引起java.lang.OutOfMemory異常,應該將大小設置為應用的常規內存使用量之外的剩余內存之間。
下面是使用LruCache緩存位圖的一個例子:
private LruCache 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(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) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
Note: 在這個例子中,有八分之一的內存被分配給了緩存。在正常的設備上(hdpi)這大概是4MB(32/8)左右。一個鋪滿了圖片的GridView在全屏狀態下的800*480的設備上所占的內存大概是1.5MB(800*480*4個字節),所以這可以在內存中存儲大概2.5頁的圖像。
當加載一個位圖到ImageView上的時候,首先要檢查LruCache。如果發現了與之相匹配的,則會被用來立即更新到ImageView上,否則就會觸發一個後台線程來處理圖片:
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中也需要對內存緩存進行添加或更新:
class BitmapWorkerTask extends AsyncTask {
...
// 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這樣的組件在加載大數據集的時候可以輕易的將內存緩存填滿。程序在運行的過程中可能會被其它任務打斷,比如一個來電,這時,在後台的任務可能就會被殺死,內存緩存也會被銷毀。一旦用戶返回了界面,那麼程序就需要再次重新處理每張圖片。
那麼磁盤緩存在這些情況下就很有幫助了,它可以存儲處理過的圖片,並會輔助提升圖片的加載時間,在圖片不再在內存緩存中存在的時候。當然,在磁盤上獲取一張圖片要比內存中要慢,並且還需要開啟單獨的工作線程,這和從磁盤上讀取數據的時間一樣,都不可預估。
Note:ContentProvider可能更適合用來存放被緩存過的圖像,如果這些圖像的訪問更加頻繁的話,就像在相冊應用中的情況一樣。
從Android Source中更新的示例代碼使用了一個DiskLruCache的實現。下面是個更新後的版本,它對已有的內存緩存增加了磁盤緩存:
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 thread
File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
new InitDiskCacheTask().execute(cacheDir);
...
}
class InitDiskCacheTask extends AsyncTask {
@Override
protected Void doInBackground(File... params) {
synchronized (mDiskCacheLock) {
File cacheDir = params[0];
mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
mDiskCacheStarting = false; // Finished initialization
mDiskCacheLock.notifyAll(); // Wake any waiting threads
}
return null;
}
}
class BitmapWorkerTask extends AsyncTask {
...
// 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) {
mMemoryCache.put(key, bitmap);
}
// Also add to disk cache
synchronized (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 thread
while (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 dir
final String cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}
Note:因為磁盤緩存的初始化需要磁盤操作,所以這個過程不應該放在UI線程中執行。然而,這也意味著在緩存初始化之前這是個訪問的機會。為了做到這一點,需要有個lock對象來保證在緩存被初始化之前APP沒有從磁盤緩存中讀取數據。
內存緩存在UI線程中執行檢查,磁盤緩存在後台線程中執行檢查。磁盤操作絕不應該放入UI線程。當圖像處理完畢後,最終被處理過的圖片應當被添加到內存緩存及磁盤緩存中以便備用。
如果在運行時發生了變更,比如屏幕的方向發生了改變,會引起Android銷毀並重啟運行中的Activity,你可能想要避免再一次處理圖像,這樣一旦配置發生了改變,可以使用戶有一個流暢快速的用戶體驗。
幸運的是,你有一個非常贊的內存緩存方案:可以使用設置了setRetainInstance(true)的Fragment,它可以將緩存傳入新的Activity實例。在activity重新創建的時候,這個被保留存在的Fragment會被重新附加在Activity上,你可以獲得原先內存緩存的訪問能力,這使得圖像可以快速的被獲得並被重新填充在ImageView對象中。
下面這個例子使用了引用LruCache的Fragment,並通過了配置更改的問題:
private LruCache mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
RetainFragment retainFragment =
RetainFragment.findOrCreateRetainFragment(getFragmentManager());
mMemoryCache = retainFragment.mRetainedCache;
if (mMemoryCache == null) {
mMemoryCache = new LruCache(cacheSize) {
... // Initialize cache here as usual
}
retainFragment.mRetainedCache = mMemoryCache;
}
...
}
class RetainFragment extends Fragment {
private static final String TAG = "RetainFragment";
public LruCache 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;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
}
為了測試這項輸出,試著在有和沒有Fragment的情況下旋轉設備。你應該會注意到這個過程幾乎沒有延遲。任何圖像如果沒有在內存緩存中找到,那麼這就為磁盤緩存提供了用武之地,如果都沒有的話,那麼常規的處理方法就會出場。
最近心情比較浮躁,項目初步已經完成一直沒有心情來更新博客,基本功能已經實現了包括添加城市,刪除城市,獲取城市部分天氣預報信息,已經詳細的天氣預報信息,還集成了ShareS
之前一直都是看別人寫的啟動模式,發現網上大多數的內容都是抄襲來抄襲去,直到最近看了開發藝術這本書,發現之前對啟動模式的理解過於簡單,很多東西都沒有考慮到,為了加深理解,於
在使用studio開發的項目過程中有時候我們想將項目發布到github上,以前都是用一種比較麻煩的方式(cmd)進行提交,最近發現studio其實是自帶這種功能的,終於可
本文實例講述了Android編程單選項框RadioGroup用法。分享給大家供大家參考,具體如下:今天介紹的是RadioGroup 的組事件.RadioGroup 可將各