Android 圖片緩存機制的深入理解
Android 圖片緩存機制的深入理解
Android加載一張圖片到用戶界面是很簡單的,但是當一次加載多張圖片時,情況就變得復雜起來。很多情況下(像ListView、GridView或ViewPager等組件),屏幕上已顯示的圖片和即將滑動到當前屏幕上的圖片數(shù)量基本上是沒有限制的。
這些組件通過重用已經(jīng)移除屏幕的子視圖來將降低內存的使用,垃圾回收器也會及時釋放那些已經(jīng)不再使用的已下載的圖片,這些都是很好的方法,但是為了保持一個流暢的、快速加載的用戶界面,就應該避免當再次回到某個頁面時而重新處理圖片。內存緩存和磁盤緩存可以幫我們做到這些,它們允許組件快速地重新加載已處理好的圖片。
使用內存緩存
內存緩存允許快速地訪問圖片,但它以占用App寶貴的內存為代價。LruCache類(API Level 4的Support Library也支持)特別適合來做圖片緩存,它使用一個強引用的LinkedHashMap來保存最近使用的對象,并且會在緩存數(shù)量超出預設的大小之前移除最近最少使用的對象。
說明:以前流行的內存緩存方案是使用軟引用或弱引用來緩存圖片,然而現(xiàn)在不推薦這樣做了,因為從android 2.3(API Level 9)起,垃圾收集器更傾向于先回收軟引用或弱引用,這樣就使它們變得低效。另外在Android 3.0(API Level 11)之前,圖片的像素數(shù)據(jù)是存儲在本地內存(native memory)中的,它以一種不可預測的方式釋放,因此可能會導致App超過內存限制甚至崩潰。
為了給LruCache設置一個合適的大小,以下是應該考慮的一些因素:
1.你的Activity或App的可用內存是多少?
2.一次展示到屏幕上的圖片是多少?有多少圖片需要預先準備好以便隨時加載到屏幕?
3.設備的屏幕尺寸和密度是多少?像Galaxy Nexus這樣的高分辨率(xhdpi)設備比Nexus S這樣分辨率(hdpi)的設備在緩存相同數(shù)量的圖片時需要更大的緩存空間。
4.圖片的尺寸和配置是怎樣的?每張圖片會占用多少內存?
5.圖片的訪問頻率如何?是否有一些圖片比另一些訪問更加頻繁?如果這樣的話,或許可以將某些圖片一直保存在內存里或者針對不同的圖片分組設置不同的LruCache對象。
6.你能否平衡圖片質量和數(shù)量之間的關系?有時候存儲更多低質量的圖片更加有用,當在需要的時候,再通過后臺任務下載高質量的圖片。
這里沒有一個具體的大小和計算公式適用于所有的App,你需要分析你的使用情況并得到一個合適的方案。當一個緩存太小時會導致無益的額外的開銷,而緩存太大時也可能會引起Java.lang.OutOfMemory異常,另外緩存越大,留給App其他部分的內存相應就越小。
這里是一個為圖片設置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) { @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); }
說明:在上述例子中,我們分配了應用內存的1/8作為緩存大小,在一個normal/hdpi的設備上最少也有4MB(32/8)的大小。一個800*480分辨率的屏幕上的一個填滿圖片的GridView大概占用1.5MB(800*480*4byte)的內存,因此該Cache至少可以緩存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); } }
上述線程中,在解碼圖片之后,也需要把它添加到內存緩存中:
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這樣展示大量數(shù)據(jù)的組件可以很輕易地就占滿內存緩存。你的App也可能被類似電話這樣的任務打斷,當App被切換到后臺后也可能被殺死,內存緩存也可能被銷毀,一旦用戶回到之前的界面,你的App依然要重新處理每個圖片。
磁盤緩存可以用來輔助存儲處理過的圖片,當內存緩存中圖片不可用時,可以從磁盤緩存中查找,從而減少加載次數(shù)。當然,從磁盤讀取圖片要比從內存讀取慢并且讀取時間是不可預期的,因此需要使用后臺線程來讀取。
說明:ContentProvider 可能是一個合適的存儲頻繁訪問的圖片的地方,比如在Image Gallery應用中。
這里的示例代碼是從Android源代碼中剝離出來的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<File, Void, Void> { @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<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) { 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); }
說明:初始化磁盤緩存需要磁盤操作因此它不應在主線程進行,然而這意味著有可能在磁盤緩存尚未初始化之前就有訪問操作發(fā)生,為了解決這個問題,在上面的實現(xiàn)中,使用一個鎖對象來確保只有在磁盤緩存初始化之后才會從磁盤緩存讀取數(shù)據(jù)。
內存緩存可以直接在UI線程讀取,然而磁盤緩存必須在后臺線程檢查,磁盤操作不應該在UI線程發(fā)生。當圖片處理完畢后,務必將最終的圖片添加到內存緩存和磁盤緩存以備后續(xù)使用。
以上就是對Android 圖片緩存機制的詳解,如有疑問請留言或者到本站社區(qū)交流討論,感謝閱讀,希望能幫助到大家,謝謝大家對本站的支持!
相關文章
Android網(wǎng)絡數(shù)據(jù)開關用法簡單示例
這篇文章主要介紹了Android網(wǎng)絡數(shù)據(jù)開關用法,通過自定義函數(shù)調用系統(tǒng)服務實現(xiàn)開關功能,具有一定參考借鑒價值,需要的朋友可以參考下2016-06-06Flutter文本Text和輸入框TextField組件使用示例
這篇文章主要為大家介紹了Flutter文本Text和輸入文本框TextField組件使用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08Flutter插件開發(fā)之HmsScanKit實現(xiàn)示例詳解
這篇文章主要為大家介紹了Flutter插件開發(fā)之HmsScanKit實現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11Android 使用AsyncTask實現(xiàn)多任務多線程斷點續(xù)傳下載
這篇文章主要介紹了Android 使用AsyncTask實現(xiàn)多任務多線程斷點續(xù)傳下載的相關資料,需要的朋友可以參考下2018-05-05