高效显示Bitmap之缓存Bitmap

加载一张图片到UI线程是很简单的,然而一次性加载大量图片时变得复杂。大多数情况下(比如用listview、gridview、viewpager时),屏幕滚动时要显示的图片本质上应该是可以无限多的。

当滑出屏幕时,通过重复利用子布局来减少内存的使用。垃圾回收机制还会释放你加载的图片,如果你没有任何长期存活的引用。这样很好,但是为了有流动和快速加载的UI,你应该避免每次都加载一遍。这里内存和磁盘存储是很有帮助的,允许组件快速重新加载这些图片。

Use a Memory Cache

内存缓存为加载图片提供最快的方式。LruCache类(API4以后版本的 Support Library可用)非常适合缓存图片,保持近期的对象在一个强引用类型的LinkedHashMap 中,在缓存大小超出时销毁很少使用的对象。

标记:在之前,流行的内存缓存实现方式是 SoftReference 或者WeakReference ,然而不被推荐。在Android2.3(API 9)开始,垃圾回收器更积极的收集软/弱引用使得其变得相当无效。另外,Andoir3.0(api 11)之前,存储在本地的位图的数据不会被预期释放,潜在地引起应用程序暂时地超出它的内存限制然后崩溃。

为了选择合适大小的 LruCache ,一系列因素应该被考虑到,如下:


  • How memory intensive is the rest of your activity and/or application?
  • 一次显示多少图片到屏幕。需要多少准备被显示到屏幕 。
  • 屏幕大小,设备密度?一个特别大的屏幕密度设备比如 Galaxy Nexus 将需要一个大的缓存才能跟 Nexus S保持一样的数量 的图片。
  • 这些图片的尺寸和配置,多少内存将被使用?
  • 使用图片的频繁度?一些比另一些更加有用?如果这样,你也许应该想要保持它们一直在内存中或者有多个 LruCache对象保存不同组的图片
  • 你能区别质量和数量吗 ?有时候存储更多低质量的图片,加载更高质量版本在另一个后台任务中是很有用的

没有适合所有应用程序的标准大小和准则,由你来决定一个合适的方式。缓存太小会引起额外开销没有任何好处,一

次太大会引起OOM同时给你的APP留下很小的内存。

这里是一个设置 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);
}
标记:这里,应用程序内存的8分之一被分配给缓存。一个普通/高设备是大约4MB(32/8)。800*480的设备下,全屏
的GridView应该使用1.5MB(800*400*4bytes),这样可能缓存至少2.5页的图片在内存中。
当加载一个位图到ImageView时,   LruCache类会被首先检测。如果实体被找到,他会被立即使用来更新ImageView,否则后台线程处

理图像:
ublic 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 {
    ...
    // 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


当内存缓存中的图片不能使用时,磁盘缓存来存留处理过的图片可以减少加载的时间。当然,从磁盘读取比内存要慢,因为读取时间无法预期,所以应该在后台进行。
标记:如果频繁使用,ContentProvider 是比较适合的地方来缓存图片。比如一个图片画廊的APP。
下面是一个DiskLruCache 的实现。然而,更多DiskLruCache 的介绍在Android 4.0源码中(libcore/luni/src/main/java/libcore/io/DiskLruCache.java)。
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线程中。当图片加载完成后,最后的位图会被加载在内存缓存和磁盘中。

Handle Configuration Changes

运行时配置发生改变时,比如屏幕方向变化,引起系统销毁和重启当前运行的Activity。你应该避免重新加载这些图片让用户有更好的体验。
幸运的是,你建立了一个非常好的内存缓存。通过调用setRetainInstance(true)保存一个Fragment对象来传递这个缓存对象到新的Activity对象中。当Acitivity重启时,保留的fragment会重新持有缓存对象,允许图片被快速的加载到ImageView中。
下面是配置改变时用Fragment来保留 LruCache对象的例子:
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实例时几乎感觉不到延迟。当内存中找不到时会自动去磁盘中找,如果都没有则跟平时一样。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值