圖片快取(源於SDK文件)

yangxi_001發表於2013-11-28

大家都知道,現在的手機螢幕解析度是越來越大了,雖然之前我們介紹了非同步載入圖片的方法。但要知道,一個應用可用的記憶體是有限的。我們不可能將所有的記憶體都用來儲存圖片,也不可能為了記憶體而每次取圖片時都上網下載(流量費是很貴滴,而且下載也很耗電啊)。

因此,對於已下載的圖片,我們需要在本地維持一個快取。

記憶體快取

LurCache是一個記憶體快取類(Android3.1引入,通過v4的支援包,可以在API Level 4以上使用)。它使用一個強連線的LinkedHashMap,將使用頻率最高的圖片快取在記憶體中。

PS:在這之前,最流行的快取方式是使用SoftReference與WeakReference。但從Android2.3開始,垃圾回收對於軟引用與弱引用來說,變得越來越積極了。這也就造成了軟引用與弱引用的效率變得很低(沒幾下就被回收了,然後又得再建立,和沒快取沒太大區別)。同時,在Android3.0之前,Bitmap的資料是儲存在所謂的native記憶體區中(可以想象成是用C語言申請的記憶體,很難被垃圾回收自動釋放)。這也造成了應用非常容易發生記憶體溢位。

當然,要想使用LurCache,我們需要給定一個快取大小。而要想確定快取佔多少記憶體,需要考慮以下條件:

  • 應用的其他地方對記憶體的佔用有多緊張?
  • 有多少圖片會同時在螢幕上顯示?在它們顯示時,要確保多少的其餘圖片可以馬上顯示而無明顯延遲?
  • 螢幕大小與密度如何?xhdpi的機子明顯要比hdpi的機子需要更多的快取。
  • 每張圖片大概有多大?
  • 圖片訪問的頻率是多少?有沒有部分圖片的訪問頻率遠高於其他圖片?如果是的話,你可能需要將這些圖片進行快取,甚至需要多個LurCache物件來快取不同等級的圖片。
  • 你可以平衡質量與數量嗎?有的時候,儲存大量低解析度的圖片更有用。你可以在後臺任務中載入高解析度的。

沒有絕對合適的大小或計算方法,你必須根據自身應用的特點來確定相應的解決方案。快取太小,反而會由於頻繁的重新建立而降低效率。快取太大,自然也同樣會造成記憶體溢位了。

以下是一個使用LurCache的例子,使用八分之一的可用記憶體作為快取。在一個普通的hdpi的機子上,大概是4MB以上。

在一個800x480的螢幕上的全屏GridView顯示的圖片,大概需要1.5MB (800*480*4 bytes)的記憶體。也就是說,這個例子裡的快取,可以儲存至少2.5屏的圖片。

Java程式碼  收藏程式碼
  1. private LruCache<String, Bitmap> mMemoryCache;  
  2.   
  3. @Override  
  4. protected void onCreate(Bundle savedInstanceState) {  
  5.     ...  
  6.     // 獲取可用記憶體的最大值,超過這個數,就會產生OutOfMemory.  
  7.     // 將其以KB為單位儲存.  
  8.     final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);  
  9.     // 使用八分之一的可用記憶體作為快取.  
  10.     final int cacheSize = maxMemory / 8;  
  11.     mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {  
  12.         @Override  
  13.         protected int sizeOf(String key, Bitmap bitmap) {  
  14.             // 快取大小以KB為單位進行計算.  
  15.             return bitmap.getByteCount() / 1024;  
  16.         }  
  17.     };  
  18.     ...  
  19. }  
  20.   
  21. public void addBitmapToMemoryCache(String key, Bitmap bitmap) {  
  22.     if (getBitmapFromMemCache(key) == null) {  
  23.         mMemoryCache.put(key, bitmap);  
  24.     }  
  25. }  
  26.   
  27. public Bitmap getBitmapFromMemCache(String key) {  
  28.     return mMemoryCache.get(key);  
  29. }  

接著《一種非同步載入資源的方法(源於SDK文件)》的例子,在載入圖片時,先去快取裡查一下。如果快取裡有,直接設上去就行了。

Java程式碼  收藏程式碼
  1. public void loadBitmap(int resId, ImageView imageView) {  
  2.     final String imageKey = String.valueOf(resId);  
  3.     final Bitmap bitmap = getBitmapFromMemCache(imageKey);  
  4.     if (bitmap != null) {  
  5.         mImageView.setImageBitmap(bitmap);  
  6.     } else {  
  7.         mImageView.setImageResource(R.drawable.image_placeholder);  
  8.         BitmapWorkerTask task = new BitmapWorkerTask(mImageView);  
  9.         task.execute(resId);  
  10.     }  
  11. }  

 

當然,BitmapWorkerTask也需要更新一下,當圖片獲取到後,將其加入快取。

Java程式碼  收藏程式碼
  1. class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {     
  2.     ...     
  3.     // Decode image in background.  
  4.     @Override  
  5.     protected Bitmap doInBackground(Integer... params) {  
  6.         final Bitmap bitmap = decodeSampledBitmapFromResource(  
  7.                 getResources(), params[0], 100100);  
  8.         addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);  
  9.         return bitmap;  
  10.     }  
  11.     ...  
  12. }  

 磁碟快取

雖然記憶體快取很有用,但光靠它還是不夠的。像一些專門看圖片的應用,裡面的照片多了去了。很容易快取就滿了。或者當應用在後臺被殺死時,記憶體快取也會立刻清空。你還是得重新建立。

在這種情況下,就需要磁碟快取來將圖片儲存到本地。這樣,當記憶體快取被清空時,可以通過磁碟快取加快圖片的載入。

以下的例子就是使用原始碼中提供的DiskLurCache來完成磁碟快取的功能。

它並不是替代記憶體快取,而是在記憶體快取之外再額外備份了一次。

Java程式碼  收藏程式碼
  1. private DiskLruCache mDiskLruCache;  
  2. private final Object mDiskCacheLock = new Object();  
  3. private boolean mDiskCacheStarting = true;  
  4. private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10// 10MB  
  5. private static final String DISK_CACHE_SUBDIR = "thumbnails";  
  6.   
  7. @Override  
  8. protected void onCreate(Bundle savedInstanceState) {  
  9.     ...  
  10.     // Initialize memory cache  
  11.   
  12.     ...  
  13.     // Initialize disk cache on background thread  
  14.     File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);  
  15.     new InitDiskCacheTask().execute(cacheDir);  
  16.     ...  
  17. }  
  18.   
  19. class InitDiskCacheTask extends AsyncTask<File, Void, Void> {  
  20.     @Override  
  21.     protected Void doInBackground(File... params) {  
  22.         synchronized (mDiskCacheLock) {  
  23.             File cacheDir = params[0];  
  24.             mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);  
  25.             mDiskCacheStarting = false// Finished initialization  
  26.             mDiskCacheLock.notifyAll(); // Wake any waiting threads  
  27.         }  
  28.         return null;  
  29.     }  
  30. }  
  31.   
  32. class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {  
  33.     ...  
  34.     // Decode image in background.  
  35.     @Override  
  36.     protected Bitmap doInBackground(Integer... params) {  
  37.         final String imageKey = String.valueOf(params[0]);  
  38.         // Check disk cache in background thread  
  39.         Bitmap bitmap = getBitmapFromDiskCache(imageKey);  
  40.         if (bitmap == null) {  
  41.             // Not found in disk cache  
  42.             // Process as normal  
  43.             final Bitmap bitmap = decodeSampledBitmapFromResource(  
  44.                     getResources(), params[0], 100100);  
  45.         }  
  46.         // Add final bitmap to caches  
  47.         addBitmapToCache(imageKey, bitmap);  
  48.         return bitmap;  
  49.     }  
  50.     ...  
  51. }  
  52.   
  53. public void addBitmapToCache(String key, Bitmap bitmap) {  
  54.     // Add to memory cache as before  
  55.     if (getBitmapFromMemCache(key) == null) {  
  56.         mMemoryCache.put(key, bitmap);  
  57.     }  
  58.     // Also add to disk cache  
  59.     synchronized (mDiskCacheLock) {  
  60.         if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {  
  61.             mDiskLruCache.put(key, bitmap);  
  62.         }  
  63.     }  
  64. }  
  65.   
  66. public Bitmap getBitmapFromDiskCache(String key) {  
  67.     synchronized (mDiskCacheLock) {  
  68.         // Wait while disk cache is started from background thread  
  69.         while (mDiskCacheStarting) {  
  70.             try {  
  71.                 mDiskCacheLock.wait();  
  72.             } catch (InterruptedException e) {  
  73.             }  
  74.         }  
  75.         if (mDiskLruCache != null) {  
  76.             return mDiskLruCache.get(key);  
  77.         }  
  78.     }  
  79.     return null;  
  80. }  
  81.   
  82. // Creates a unique subdirectory of the designated app cache directory.  
  83. // Tries to use external  
  84. // but if not mounted, falls back on internal storage.  
  85. public static File getDiskCacheDir(Context context, String uniqueName) {  
  86.     // Check if media is mounted or storage is built-in, if so, try and use  
  87.     // external cache dir  
  88.     // otherwise use internal cache dir  
  89.   
  90.     final String cachePath = Environment.MEDIA_MOUNTED.equals(Environment  
  91.             .getExternalStorageState()) || !isExternalStorageRemovable() ? getExternalCacheDir(  
  92.             context).getPath() : context.getCacheDir().getPath();  
  93.     return new File(cachePath + File.separator + uniqueName);  
  94. }  

 

PS:即使是初始化磁碟快取,也需要相應的磁碟操作。因此,使用了一個非同步任務InitDiskCacheTask來進行。另外,為了防止在磁碟快取建立成功前就去訪問,在getBitmapFromDiskCache方法中,進行了wait操作。當磁碟快取初始化後,如果提前訪問了,相應的執行緒將被notifyAll喚醒。

處理Configuration Change

在程式執行時,我們經常會遇到Configuration Change,像橫豎屏切換、語言改變等等。

而這些情況,往往會使得當前執行的Activity被銷燬並重新建立。

為了防止在銷燬與建立過程中重新建立快取(耗時太久且影響效率,要是能直接儲存就好了),我們可以通過Fragment。只要setRetainInstance(true)就行了。

如下圖所示,在Activity的onCreate裡,先判斷相應的Fragment在不在FragmentManager裡,要是在的話,直接獲取相應的快取物件。並且在Fragment的onCreate中setRetainInstance(true)。

Java程式碼  收藏程式碼
  1. private LruCache<String, Bitmap> mMemoryCache;  
  2.   
  3. @Override  
  4. protected void onCreate(Bundle savedInstanceState) {  
  5.     ...  
  6.     RetainFragment mRetainFragment =  
  7.             RetainFragment.findOrCreateRetainFragment(getFragmentManager());  
  8.     mMemoryCache = RetainFragment.mRetainedCache;  
  9.     if (mMemoryCache == null) {  
  10.         mMemoryCache = new LruCache<String, Bitmap>(cacheSize);  
  11.         ... // Initialize cache here as usual  
  12.   
  13.         mRetainFragment.mRetainedCache = mMemoryCache;  
  14.     }  
  15.     ...  
  16. }  
  17.   
  18. class RetainFragment extends Fragment {  
  19.     private static final String TAG = "RetainFragment";  
  20.     public LruCache<String, Bitmap> mRetainedCache;  
  21.   
  22.     public RetainFragment() {  
  23.     }  
  24.   
  25.     public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {  
  26.         RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);  
  27.         if (fragment == null) {  
  28.             fragment = new RetainFragment();  
  29.         }  
  30.         return fragment;  
  31.     }  
  32.   
  33.     @Override  
  34.     public void onCreate(Bundle savedInstanceState) {  
  35.         super.onCreate(savedInstanceState);  
  36.         setRetainInstance(true);  
  37.     }  
  38. }  

 

相關文章