Android有效地展示大图片(三)

图片缓存

只下载一张图片在你的UI上时非常简单的。但是如果你需要一次性下很多图片就不这么容易了。在很多情况下(比如ListView,GridView或者是ViewPager),要展示在屏幕上的图片加上即将要展示的图片,这个数量可是没有什么大小限制的。
以上提到的控件,为了减少内存消耗,他们都会循环利用已经触屏的子View,如果你对某个Bitmap不再持有引用,那么java的垃圾回收机制也会把这个bitmap收走。这样的话,每次一些图片重新上屏时我们需要重新处理他们。但是为了保证UI的流畅性,我们必须避免他们。这时内存缓存和硬盘缓存就会派上用场了,通过他们我们可以让这些空间快速的加载处理过的图片。、

Use a Memory Cache


内存缓存可以让我们快速得到bitmap,当然其代价就是占用可贵的程序内存。LruCache类非常适合存放缓存图片,该类把最近用到的对象以强引用存放到LinkedHashMap中去,并在缓存大小超过规定之前把最不常用的对象删除。
在以前,通常用SoftReference或者 WeakReference来作为缓存来存储bitmap.然而如今我们并不推荐这样做。从Android 2.3 开始,垃圾回收机制对soft/weak references 的攻击性变强,使得弱/软 引用失去了效果。另外,在3.0以上,bitmap的相关信息会被保存在当地内存中,我们也不知道他们什么时候释放。这样的潜在性通常会使得一个程序超出自己的内存限制而崩溃。

为了给LruCache一个确定的大小。我们需要考虑以下几个因素:

你的activity或者是application的剩余的内存有多密集?
屏幕一次性要显示多少图片,将要上屏的图片有几个?
屏幕大小和分辨率是多少?相比于Nexus S(hdpi),Galaxy Nexus(xhdpi)需要更大的内存来存储相同数量的图片。
这些图片的尺寸和形状是怎样的,它们每个会占有多少内存?
这些图片被取出的频繁吗?是不是一些比另外一些更频繁。如果是这样的话,那么你可能需要保存那几个图片一直在内存中。或者你可以多创建几个LruCache来分组存放图片。
均衡图片的质量和数量。有时候存储很多的质量低的图片是很有用处的。而后台在下载的时候可能会是高质量的图片。

LruCache的大小并没有一个特定的值。它的大小取决于你的分析。缓存太小只会引起不必要的开销而没有什么实际用处,太大的话就只会留给程序运行的空间较小,容易造成OOM 。

下面是一个建立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.得到虚拟机的最大的可用内存,并以KB的单位存储
    //超出此内存就会报OOM.
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // Use 1/8th of the available memory for this memory cache.
   //用可用内存的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.缓存的大小以KB为单位。
            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分给了缓存。在分辨率中高端的设备上,这最低也是4M,在800x480的设备上,一个充满图片的全屏的GridView大约会用1.5M,所以这个缓存最少存储了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);
    }
}

上述代码中的BitmapWorkerTask中也会作相应的更新并把处理好的图片放在缓存中
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;
    }
    ...
}

Use a Disk Cache

内存缓存在获取最近浏览过的图片速度的确很快,但是我们不能只依赖只在这些内存里才有的图片。像是GridView这种需要大量数据填充的控件很快就把内存缓存充满了。而且你的程序也可能会被其余的程序打断,比如一个来电,后台线程可能被销毁,内存缓存就会被破坏。一旦resume,程序还得重新处理图片。
在上述这些情况下,硬盘缓存就派上了用场。硬盘缓存会在保存被处理过得图片至于也减少了重新下载那些在内存中找不到的图片的次数。当然从硬盘获取图片的速度相比较而言比较慢,所以应该从后台线程进行。而且硬盘读取速度也是不可预知的。

注:当获取图片比较频繁时,用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) {
    ...
// 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.
//在目标app的缓存目录下创建唯一的子目录。如果可以用sdcard上的则用sdcard 的空间,如果没有则用手机本身的存储。
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);
}

注:初始化硬盘内存不可以在主线程运行。但是在硬盘缓存完成初始化之前我们也不可以访问该缓存。为了达到这个目的,上述代码中用了一个锁对象来控制,这样的话只有等硬盘初始化完成,app才有机会访问硬盘缓存。

检查内存缓存中是否有目标图是在主线程中进行的,而检查硬盘缓存则是在后台线程进行的。当图片处理完毕后,最终的bitmap将在两个缓存中都进行存储以便将来使用。

Handle Configuration Changes

运行期间会有一些配置上的变化,比如说屏幕切换方向,是的Android重启当前Activity,而为了让用户有更流畅的体验我们又不想重新处理这些图片。
幸运的是,在Use a Memory Cache  部分,你有了一个存放bitmap的缓存,这个缓存可以通过一个Fragment传递给新的Activity。这个fragment是通过调用setRetainInstance(true)而得以保留的。当Activity重新创建后,这个一直存在的Fragment便会重新attached,这样你就可以获得内存缓存了,图片自然会快速加载了。
下面的代码就是在切屏时用Fragment来保存LruCache.
private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment retainFragment =
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache = retainFragment.mRetainedCache;
    if (mMemoryCache == null) {
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            ... // Initialize cache here as usual
        }
        retainFragment.mRetainedCache = mMemoryCache;
    }
    ...
}

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

    public RetainFragment() {}

    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
        RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
        if (fragment == null) {
            fragment = new RetainFragment();
            fm.beginTransaction().add(fragment, TAG).commit();
        }
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
}
为了测试,我们可以在保留Fragment和不保留Fragment时分别旋转屏幕。你会发现,当保留了cache时,切屏时填充图片有很小或者几乎没有停滞。
在缓存中找不到的图片可能放在硬盘缓存中。如果硬盘中也没有,那么他们就会从后台处理了。
附异步加载图片源码下载地址:http://files.cnblogs.com/domyself918/DisplayingBitmaps.zip

猜你喜欢

转载自blog.csdn.net/domyself918/article/details/37656529
今日推荐