高效地加载图片(三) 缓存图片

若是只须要加载一张图片,那么直接加载就能够.可是,若是要在相似ListView,GridView或者ViewPager的控件中加载大量的图片时,问题就会变得复杂.在使用这类控件时,在短期内可能会显示在屏幕上的图片数量是不固定的.html

这类控件会经过子View的复用来保持较低的内存占用.而Garbage Collector也会在View被复用时释放对应的Bitmap,保证这些没用用到的Bitmap不会长期存在于内存中.可是为了保证控件的流畅滑动,在一个View再次滑动出如今屏幕上时,咱们须要避免图片的重复性加载.而此时,在内存和磁盘上开辟一块缓存空间每每可以保证图片的快速重复加载.java

使用内存缓存android

一块内存缓存在耗费必定应用内存基础上,可以让快速加载图片成为可能.而LruCache正合适用来缓存图片,对最近使用过的对象保存在LinkedHashMap中,而且将最近未使用过的对象释放.缓存

为了给LrcCache肯定一个合适的大小,有如下一些因素须要考虑:app

1.应用中其余组件占用内存的状况异步

2.有多少图片可能会显示在屏幕上?有多少图片将要显示在屏幕上?ide

3.屏幕的尺寸和屏幕密度是多少?与Nexus S这类高屏幕密度设备相比,Galaxy Nexs这类超高屏幕密度的设备,每每须要更大的缓存空间来存储相同数量的图片.函数

4.图片的尺寸以及其余的参数,还有每张图片将会占用多少内存.ui

5.图片被访问的频率有多高?是否有一些图片的访问频率会比另一些更高?若是是这样,咱们可能须要将一些图片长存于内存中,或者使用多个LrcCache来对不一样的Bitmap进行分组.this

6.咱们还须要在图片的数量和质量之间权衡.有些时候,在缓存中存放大量的缩略图,而在后台加载高清图片会明显提升效率.

对每一个应用来讲,须要指定的缓存大小是不必定的,这取决于咱们对应用的分析并得出相应的解决方案.若是缓存空间太小,可能会形成额外的开销,这对整个应用并没有补益;而缓存空间过大,则可能会形成java.lang.OutOfMemory异常,而且留给其余组件使用的内存空间也会相应减小.

如下为初始化一个存放Bitmap的LrcCache的例子:

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
	// 获取最大的可用空间,若是须要的空间超出这个大小,则会抛出OutOfMemory异常
	// LrcCache构造函数中的参数是以千字节为单位的
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

	// 此处缓存大小取可用内存的1/8
    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;
        }
    };
    ...
}

// 将Bitmap存入缓存
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
		// 当使用(getBitmapFromMemCache方法,根据传入的key获取Bitmap
		// 当获取到的Bitmap为空时,证实没有存储过该Bitmap
		// 此时将该Bitmap存储到LrcCache中
        mMemoryCache.put(key, bitmap);
    }
}

// 根据key从LrcCache中获取对应的Bitmap
public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}

注意:在这个例子中,应用内存的1/8被分配用做缓存.在一台正常/高屏幕分辨率的设备上,这个缓存的大小在4MB左右(32/8 MB).而使用800×480分辨率的图片填充一个全屏的GridView的话,大概须要1.5MB的内存空间(800*480*4 bytes),因此这个缓存可以存储至少2.5页的图片.

在加载一张图片到ImageView时,LrcCache会首先检查这张图片是否存在.若是图片存在,则图片会当即被更新到ImageView中,不然会开启一个后台线程去加载这张图片.

public void loadBitmap(int resId, ImageView imageView) {
	// 将图片的资源id转换为String型,做为key
    final String imageKey = String.valueOf(resId);

	// 根据key从LruCache中获取Bitmap
    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
		// 若是获取到的Bitmap不为空
		// 则直接将获取到的Bitmap更新到ImageView中
        mImageView.setImageBitmap(bitmap);
    } else {
		// 不然,则先在ImageView中设置一张占位图
        mImageView.setImageResource(R.drawable.image_placeholder);
		// 再开启一个新的异步任务去加载图片
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}

BitmapWorkerTask也须要更新,将Bitmap以键值对的形式存储到LrcCache中.

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // 在后台加载图片
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
		// 将Bitmap对象以键值对的形式存储到LrcCache中
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

使用磁盘缓存

内存缓存在访问最近使用过的图片方面可以极大地提升效率,可是咱们不能期望全部须要的图片都能在内存缓存中找到.向GridView这类数据源中有大量数据的控件,会轻易的就将内存缓存占用满.而咱们的应用也可能会被其余的任务打断(切换到后台),例如接听电话,而当咱们的应用被切换到后台时,它极有可能会被关闭,此时内存缓存也会被销毁.当用户返回咱们的应用时,应用又须要从新加载须要的图片.

而磁盘缓存会在内存缓存被销毁时继续加载图片,这样当内存缓存不可用可是又须要加载图片时就可以减小加载的时间.固然,从磁盘上读取图片要比从内存中读取图片慢,并且须要在后台线程中执行,由于图片的加载时间是不必定的.

注意:若是缓存图片须要常常访问,则将这些缓存图片存储到ContentProvider是一个更好的选择,例如图库应用就是这么作的.

如下示例是一个DiskLruCache的实现(Android source).这个示例是在内存缓存的基础上又增长了磁盘缓存.

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) {
    ...
	// 初始化内存缓存
    ...
    // 在后台线程初始化磁盘缓存
    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; // 标识结束初始化
            mDiskCacheLock.notifyAll(); // 唤醒等待中的线程
        }
        return null;
    }
}

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // 在后台解析图片
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final String imageKey = String.valueOf(params[0]);

        // 在后台线程中判断图片是否已经存在于磁盘缓存中
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);

        if (bitmap == null) { // 不存在于磁盘缓存中
            // 则正常加载图片
            final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100));
        }

        // 将加载出的图片添加到缓存中
        addBitmapToCache(imageKey, bitmap);

        return bitmap;
    }
    ...
}

public void addBitmapToCache(String key, Bitmap bitmap) {
	// 将图片添加到内存缓存中
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }

    // 同时将图片添加到磁盘缓存中
    synchronized (mDiskCacheLock) {
        if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
            mDiskLruCache.put(key, bitmap);
        }
    }
}

public Bitmap getBitmapFromDiskCache(String key) {
    synchronized (mDiskCacheLock) {
		// 当磁盘缓存正在初始化时,则等待
        while (mDiskCacheStarting) {
            try {
                mDiskCacheLock.wait();
            } catch (InterruptedException e) {}
        }
        if (mDiskLruCache != null) {
            return mDiskLruCache.get(key);
        }
    }
    return null;
}

// 当外部存储器可用时,则在应用指定文件夹中建立一个惟一的子文件夹做为缓存目录
// 而当外部设备不可用时,则使用内置存储器
public static File getDiskCacheDir(Context context, String uniqueName) {
	// 检查外部存储器是否可用,若是可用则使用外部存储器的缓存目录
	// 不然使用内部存储器的缓存目录
    final String cachePath =
            Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
                    !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
                            context.getCacheDir().getPath();

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

注意:既然磁盘缓存的操做涉及到磁盘操做,则此处的全部过程不能在UI线程中执行.同时,这也意味着在磁盘缓存初始化完毕之前是可以被访问的.为了解决这个问题,上述方法中添加了一个锁,这个锁保证了磁盘缓存在初始化完毕以前不会被应用读取.

尽管内存缓存的检查工做能够在UI线程中执行,磁盘缓存的检察工做则必须在后台线程中执行.设计磁盘的操做不管如何不该该在UI线程中执行.当图片加载成功,获得的图片会添加到这两个缓存中去以待使用.

处理配置的更改

当运行时,配置发生了改变,例如屏幕方向的变化.这种变化会使Android系统摧毁而且使用新的配置重建当前正在执行的Activity(有关此方面的更多介绍,请查看Handling Runtime Changes).为了使用户有一个顺畅的体验,咱们须要避免从新加载全部的图片.

幸运的时,咱们有一个不错的内存缓存,这个内存缓存能够经过调用FragmentsetRetainInstance(true)方法保存而且传递到新的Activity中.当Activity被重建后,这个Fragment能够从新依附到新的Activity上,这样咱们就可使用已经存在的内存缓存,快速获取图片并展现在ImageView中.

如下是经过Fragment实现保留LruCache的代码:

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
	// 获得一个用于保存LruCache的Fragment
    RetainFragment retainFragment =
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
	// 取出Fragment的LruCache
    mMemoryCache = retainFragment.mRetainedCache;
    if (mMemoryCache == null) {
		// 若是LruCache为空,则原先没有缓存
		// 须要新建并初始化一个LruCache
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            ... // Initialize cache here as usual
        }
		// 将新建的LruCache存放到Fragment中
        retainFragment.mRetainedCache = mMemoryCache;
    }
    ...
}

class RetainFragment extends Fragment {
    private static final String TAG = "RetainFragment";
    public LruCache<String, Bitmap> mRetainedCache;

    public RetainFragment() {}
	
	// 新建或者从FragmentManager中获得保存LruCache的Fragment
    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
		// 根据tag从FragmentManager中获取对应的Fragment
        RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
        if (fragment == null) {
			// 若是Fragment为空,则原先没有该Fragment
			// 即代表原先没有LruCache
			// 此时须要新建一个Fragment用于存放LruCache
            fragment = new RetainFragment();
			// 并将Fragment添加到FragmentManager中
            fm.beginTransaction().add(fragment, TAG).commit();
        }
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
		// 设置当Activity被重建时,Fragment从新依附到Activity上
        setRetainInstance(true);
    }
}

为了验证一下效果(是否从新将Fragment依附到Activity上),咱们能够旋转一下屏幕.你会发现当咱们经过Fragment保存了内存缓存,重建了Activity后从新取出图片几乎没有延时.在内存缓存中没有的图片极可能在磁盘缓存上会有,若是磁盘缓存中也没有,则会正常加载须要的图片.

相关文章
相关标签/搜索