为什么要做缓存?

在UI界面加载一张图片时很简单,然而如果需要加载多张较大的图像,事情就会变得更加复杂。在许多情况下(如ListView、GridView或ViewPager等的组件),屏幕上的图片的总数伴随屏幕的滚动会大大增加,且基本上是无限的。

为了使内存使用保持在稳定范围内,防止出现OOM,这些组件会在子view画出屏幕后,对其进行资源回收,并重新显示新出现的图片,垃圾回收机制会释放掉不再显示的图片的内存空间。但是这样频繁地处理图片的加载和回收不利于操作的流畅性,而内存或者磁盘的Cache就会帮助解决这个问题,实现快速加载已加载的图片。 在缓存上,主要有两种级别的Cache:LruCache和DiskLruCache。 前者是基于内存的,后者是基于磁盘的。

如何在内存中做缓存?

通过内存缓存可以快速加载缓存图片,但会消耗应用的内存空间。LruCache类(通过兼容包可以支持到sdk4)很适合做图片缓存,它通过LinkedHashMap保持图片的强引用方式存储图片,当缓存空间超过设置定的限值时会释放掉早期的缓存。

注:在过去,常用的内存缓存实现是通过SoftReference或WeakReference,但不建议这样做。从Android2.3(API等级9)垃圾收集器开始更积极收集软/弱引用,这使得它们相当无效。此外,在Android 3.0(API等级11)之前,存储在native内存中的可见的bitmap不会被释放,可能会导致应用程序暂时地超过其内存限制并崩溃。


为了给LruCache设置合适的大小,需要考虑以下几点因素:


  • 你的应用中空闲内存是多大?
  • 你要在屏幕中一次显示多少图片? 你准备多少张图片用于显示?
  • 设备的屏幕大小与density 是多少?超高屏幕density的设备(xhdpi)像Galaxy Nexus 比 Nexus S (hdpi)这样的设备在缓存相同的图片时需要更大的Cache空间。
  • 图片的大小和属性及其需要占用多少内存空间?
  • 图片的访问频率是多少? 是否比其他的图片使用的频率高?如果这样你可能需要考虑将图片长期存放在内存中或者针对不同类型的图片使用不同的缓存策略。
  • 如何平衡质量与数量,有事你可能会存储一些常用的低质量的图片用户显示,然后通过异步线程加载高质量的图片。

图片缓存方案没有固定的模式使用所有的的应用,你需要根据应用的具体应用场景进行分析,选择合适的方案来做,缓存太小不能发挥缓存的优势,太大可能占用过多的内存,降低应用性能,或者发生内存溢出异常,

下面是一个使用LruCache的例子:

private LruCache mMemoryCache;                                                                            @Override protected void onCreate(Bundle savedInstanceState)
{      ...      // Get memory class of this device, exceeding this amount will throw an OutOfMemory exception.      final int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();                                                                                // Use 1/8th of the available memory for this memory cache.      final int cacheSize = 1024 * 1024 * memClass / 8;                                                                                mMemoryCache = new LruCache(cacheSize)
{          @Override         protected int sizeOf(String key, Bitmap bitmap)
{              // The cache size will be measured in bytes rather than number of items.              return bitmap.getByteCount();          }      };      ...  }                                                                            public void addBitmapToMemoryCache(String key, Bitmap bitmap)
{      if (getBitmapFromMemCache(key) == null)
{          mMemoryCache.put(key, bitmap);      }  }                                                                            public Bitmap getBitmapFromMemCache(String key)
{      return mMemoryCache.get(key);  }

注意:在这个例子中,应用八分之一的内存分配给图片缓存,在普通/hdpi设备中大约为4MB(32/8)。GirdView全屏时在800x480分辨率的设备中需要1.5M图片空间(800*480*4 bytes),这样就可以在内存中缓存2.5屏的图片。


运用LruCache向ImageView添加图片时首先先检查图片是否存在,如果在直接更行ImageView,否则通过后台线程加载图片:

public void loadBitmap(int resId, ImageView imageView)
{      final String imageKey = String.valueOf(resId);                                                                final Bitmap bitmap = getBitmapFromMemCache(imageKey);      if (bitmap != null)
{          imageView.setImageBitmap(bitmap);      }
else
{          imageView.setImageResource(R.drawable.image_placeholder);          BitmapWorkerTask task = new BitmapWorkerTask(imageView);          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;      }      ...  }


如何使用磁盘缓存?

内存缓存对访问最近使用的图片时很高效,但是你不能保证它一直会在缓存中。像GirdView这样大数据量的组件很容易充满内存缓存。你的应用可能会被“来电”打断,在后台时可能会被杀掉,内存缓存就会失效,一旦用户重新回到应用中时,你需要重新处理每个图片。

在这种情况下我们可以运用磁盘缓存存储已处理的图片,当图片不再内存中时,减少重新加载的时间,当然从磁盘加载图片时要比内存中慢,需要在后台线程中做,因为磁盘的读取时间是未知的。

注意:如果你经常访问图片,ContentProvider应该是存储图片的好地方,如:Gallery图片管理应用。

下面是一个简单的DiskLruCache实现。然而推荐的实现DiskLruCache方案请参考Android4.0中(libcore/luni/src/main/java/libcore/io/DiskLruCache.java)源码。本文使用的是之前版本中的简单实现(Quick Search中是另外的实现).

显示是简单实现DiskLruCache更新后的例子:

private DiskLruCache mDiskCache;  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      ...      File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR);      mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE);      ...  }                                      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(String.valueOf(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      if (!mDiskCache.containsKey(key))
{          mDiskCache.put(key, bitmap);      }  }                                      public Bitmap getBitmapFromDiskCache(String key)
{      return mDiskCache.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.  public static File getCacheDir(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.getExternalStorageState() == Environment.MEDIA_MOUNTED ||
!Environment.isExternalStorageRemovable()) ?                               context.getExternalCacheDir().getPath() :
context.getCacheDir().getPath();                                          return new File(cachePath + File.separator + uniqueName);  }

内存缓存检查在UI线程中做,磁盘缓存的检查在后台线程中。硬盘操作不应在UI线程中。图片处理完成后应将其加入正在使用的内存、磁盘缓存中。

如何处理配置的改变?

应用运行中配置改变时,如屏幕方向改变时为了应用新的配置Android会销毁重新运行当前的Activity,此时,为了给用户快速、平缓的用户体验你可能不想重新加载图片。

多亏你运行了缓存技术,缓存可以通过 setRetainInstance(true))传递给新的Activity,在Activity重启后,你可以通过附着的Fragment重新使用已存在的缓存,这样就可以快速加载到ImageView中了。

下面是一个当配置改变时用Fragment重用已有的缓存的例子:

private LruCache mMemoryCache;                     @Override protected void onCreate(Bundle savedInstanceState)
{      ...      RetainFragment mRetainFragment =              RetainFragment.findOrCreateRetainFragment(getFragmentManager());      mMemoryCache = RetainFragment.mRetainedCache;      if (mMemoryCache == null)
{          mMemoryCache = new LruCache(cacheSize)
{              ... // Initialize cache here as usual          }          mRetainFragment.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();          }          return fragment;      }                         @Override     public void onCreate(Bundle savedInstanceState)
{          super.onCreate(savedInstanceState);          setRetainInstance(true);      }  }

为了测试,在使用和未使用Fragment的情况下,强制旋转屏幕,你会发现从保留内存缓存加载图片时几乎没有滞后。