Android教程进阶:缓存位图

时间:2013-03-19 09:30来源:eoeAndroid 作者:adminjet 点击:

加载一副位图到你的用户界面是很简单的,然而如果你需要马上加载一组更大的图片的话就会复杂的多.在许多情况下(例如有些组件像ListView,GridView以及ViewPager等),出现在屏幕上的图片总量,其中包括可能马上要滚动显示在屏幕上的那些图片,实际上是无限的.

那些通过回收即将移除屏幕的子视图的组件,内存使用得以保留.如果你不长期保持你对象的引用的话,垃圾收集器也会释放你所加载的位图内存.但为了保持一个流畅,快速加载,并且你想避免它们每次出现在屏幕上时重复加载处理这些图片的UI的话,这最好不过了.一个内存和磁盘的缓存通常能解决这个问题,允许组件快速地重新处理图片.

这个要点教你当加载多个位图时使用一个内存和磁盘的位图缓存来提高响应速度以及提升整个UI界面的流畅性.

使用一个内存缓存


在占用宝贵的应用程序内存情况下,内存缓冲提供了可以快速访问位图.LruCache类(也可以使用API级别4的Support Library)特别适合用于缓存位图的任务,最近被引用的对象保存在一个强引用LinkedHashMap中,以及在缓存超过了其指定的大小之前释放最近很少使用的对象的内存.

注意:在过去,一个常用的内存缓存实现是一个SoftReferenceWeakReference的位图缓存,然而现在不推荐使用.从android2.3(API 级别9)开始,垃圾回收器更加注重于回收软/弱引用,这使得使用以上引用很大程度上无效.此外,之前的android3.0(API级别11),位图的备份数据存储在本地那些在一种可预测的情况下没有被释放的内存中,很有可能会导致应用程序内存溢出和崩溃.

为了选择一个合适的的LruCache大小,许多因素应当被予以考虑,例如:

    • 其余的活动或者应用程序都是很耗内存的嘛?
    • 大量的图片是如何立刻出现在屏幕上的?需要多少即将在屏幕上显示的?
    • 设备的屏幕的大小和分辨率是多少?一个额外的高密度屏(xhdpi)设备,例如Galaxy Nexus 将需要一个更大的缓存来维持内存中相同数量的图片,与像Nexus S (hdpi)设备比较起来.
    • 位图是什么尺寸和配置以及每张图片要占用多少内存?
    • 图片访问频繁嘛?比起别的将会被频繁地访问吗?也许你可能总是想要在内存中保存一定项,甚至对于不同的位图组来说有多个LRUCache对象.
    • 你能在数量和质量之间取得平衡嘛?有时对于存储更多的低质量的位图是更有用的,潜在地在另外的后台任务中加载一个更高质量版本.

没有具体的大小以及适合所有应用程序的公式,它是由你来分析您的使用情况,并拿出一个合适的解决方案.缓存太小会导致额外的没有益处的开销,缓存过大会再次导致java.lang.OutOfMemory异常,并且只保留下你的应用程序其余相当少的内存来运行你的应用程序. 这个例子是针对位图来设置一个LruCache:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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.在一个800* 480分辨率的设备上全屏显示一个充满图片的GridView控件视图将使用1.5MB左右的缓存,所以这将在内存中缓冲至少四分之一的图片.

当将一张位图加载进一个ImageView中,LruCache首先被检查.如果有输入,缓存立刻被使用来更新这个ImageView视图,另外一个后台线程随之诞生来处理这张图片:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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也需要被更新添加到内存缓冲项中:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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这种具有更大的数据集的组件很容易地会占用所有内存缓存.你的应用程序会被别的任务像打电话等打断,并且当运行在后台时被进程杀死以及内存缓存被回收.一旦用户重新打开,你的应用程序不得不重新处理每一张图片.

在这种情况下使用磁盘缓存来持续处理位图,并且有助于在图片在内存缓存中不再可用时缩短加载时间.当然,从磁盘获取图片比从内存加载更慢并且应当在后台线程中处理,因为磁盘读取的时间是不可预知的. 注意:如果它们被更频繁地访问,那么一个ContentProvider可能是一个更合适的地方来存储缓存中的图像,例如在一个图片库应用程序里.

包括在这一类的示例代码是一个基本DiskLruCache实现.然而,一个更强大和推荐的DiskLruCache解决方案包括在Android4.0源代码(libcore/luni/src/main/java/libcore/io/DiskLruCache.java). 返回移植之前的Android版本上使用这个类的,应该是相当简单(快速搜索已经实现这个解决方案). 这里是个更新的示例代码,包括在这一类示例应用程序中使用简单的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.MEDIA_MOUNTED

  || Environment.isExternalStorageRemovable() ?

  context.getExternalCacheDir()。getPath() : context.getCacheDir()。getPath();

  return new File(cachePath + File.separator + uniqueName);

  }
当内存缓存在UI线程中被检查时,磁盘缓存在后台线程中被检查.磁盘操作应当永远不会发生在UI线程中.当图片处理完成时,最终的位图都将被添加到内存和磁盘缓存中已被将来时候.

处理配置更改


程序运行时配置改变,例如屏幕的方向改变,导致系统销毁活动并且采用新的配置重新运行活动(有关此问题的更多信息,请参见Handling Runtime Changes).你想要避免不得不再次处理所有的图片以使用户在配置发生改变时有一个平稳和快速的体验.

幸运地是,在使用一个内存缓存一节,你有一个不错的自己所构造的位图内存缓冲.缓存能通过新的活动实例来使用一个Fragment,这个Fragment是通过调用setRetainInstance(true)方法被保留的.活动被重新构造后,保留的片段重新连接,并且你获得现有的高速缓存对象的访问权限,使得图片能快速的加载并重新填充到ImageView对象中.

接下来是一个使用Fragment将一个LruCache对象保留在配置更改中的范例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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的情况下旋转设备和不旋转设备.在你保留缓存的情况下,由于图片填充到activity时几乎马上从内存加载,你应当注意到几乎没有滞后的感觉.任何图片都是先在内存缓存中找,没有的话再从磁盘缓存中找,如果都没有的话,就会像往常获取图片一样处理.
分享到:

凌阳教育培训【凌阳科技旗下教育品牌】——专业的培训机构,全国唯一“按班公布学员就业去向”的诚信机构

关注我们:

全国免费咨询电话:156-0117-5697或010-62981113(转2824) 服务监督电话:010-62981113-2800

京ICP备09010168号  京公网安备11010802010586号

北京安卓培训中心:海淀区上地信息产业基地中黎科技园1号楼3层A段

Copyright © 2017 SunplusEdu Inc. All Rights Reserved