Android官网培训课:缓存位图

缓存位图
把单独一个位图加载到你的UI中是很简单直接的,但如果你想一次加载大量的图片就会变得很麻烦。像ListView,GridView和ViewPager这样的组件,当前屏幕显示的图片加上即将被显示到屏幕上的图片的总和实际上是无限的。在子view被移出屏幕后继续循环使用这些子view,这些子view占用的内存未被释放而是继续保存着。如果你不继续对位图保持长时间的引用,垃圾回收机制也会清理这些位图占用的内存。这其实也不错,但是为了让UI加载位图更快更流畅,你会试图避免持续的处理处理这些重新回到屏幕的图片。内存和sd卡缓存能够为你提供这样的帮助,允许组件重新加载已经处理过的图片,当然速度会更快。
本节课带你过一遍通过使用内存缓存和sd卡缓存来提高UI在加载大量图片时的响应性能和流畅程度。
  • 使用内存缓存
内存缓存能够使访问位图更快,代价是牺牲宝贵的内存。LruCache类很适合于缓存位图,这个类可以把最近用到的对象保存在一个强引用哈希表LinkedHashMap中,在缓存大小超出设定大小之前它会清除掉哈希表中最久未用的对象。

Note: 之前,比较流行的缓存实现是SoftReference位图缓存或WeakReference位图缓存,但现在不提倡使用。从Android2.3开始,垃圾回收机制更加积极的去回收soft/weak类型的引用,这使得这两种引用不怎么高效。另外,在Android3.0之前,位图数据被保存在本地内存中,其内存的释放并非是可预见的,这样就会存在潜在的可能性,即导致应用超出了内存的限制。

为了确定LruCache的合适缓存大小,有几个要素需要被考虑,例如:
1.activity和application的剩余部分对内存的需求有多密集?
2.一个屏幕面积要显示多少张图片?需要有多少张图片准备显示在屏幕上?
3.手机或平板的分辨率是多少,屏幕尺寸是多大?像三星Galaxy Nexus这种极高精度分辨率屏幕(xhdpi)设备要比三星Galaxy Nexus S(高精度分辨率,hdpi)需要更多的缓存来存放相同数目的图片。
4.位图的尺寸和配置是什么?占多少内存?
5.图片获取的频率是怎样的?是否某些图片比其他图片获取的更频繁?或许你应该将一部分图片始终置于内存中,或是按照获取频率对图片进行分组,然后为每个分组设置LruCache对象。
6.是否可在质量和数量之间做平衡?有时存放大数目的低品质位图会更有用,然后再在另一个后台任务中加载更高品质的图片.

没有哪个缓存大小能够刚好满足所有应用,所以决定权在于你,你需要分析然后得出一个合适的解决办法。缓存过小会产生一些毫无益处的额外开销,缓存过大可能直接导致跑出java.lang.OutOfMemory异常,也会导致应用的剩余内存过少。

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // 获取虚拟机最大可用内存,超过这个值会跑出OutOfMemory异常.以千字节为单位获取这个内存的int值
    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) {
            // 缓存大小单位以千字节来算.
            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);
}
在这个例子中,八分之一的内存被用来做缓存用,在普通分辨率或者高精度分辨率移动设备上,这个缓存大约是4M(32/8)。假如一个填满图片的GridView组件占满了整个屏幕,屏幕为800乘480分辨率,那么它大约占内存大小为1.5M(800*400*4字节),所以大约能够有2.5页图片能被缓存。
在向ImageView中加载一个位图时,LruCache会首先被检查一下。如果LruCache找到了,那么直接用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> {
    ...
    // 后台解码.
    @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可能是一个更好的缓存地点。
下面这段简单的代码用了到了Android源码中的DiskLruCache实现。
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); //根据指定的缓存位置和缓存大小创建diskLrucache
            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]);

        // 后台线程检查diskCache
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);

        if (bitmap == null) { // disk cache中未找到
            // 做正常处理
            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);
    }

    // 同时加到disk缓存
    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;
}

// 创建一个唯一的缓存子目录。先尝试在external disk上建,若external disk未挂载则建在internal disk上
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);
}
注解:初始化disk缓存需要访问disk操作,所以初始化操作不能够在main线程上进行。但这也就意味着缓存可能在初始化之前就被访问。为了解决这个问题,在上述代码实现中,用一个lock对象来保证只有在disk缓存被初始化之后才能被读取和写入。

内存缓存是在UI线程中被检查,而disk缓存则是在后台线程中被检查。disk操作一定不要发生在UI线程中。当图片的处理完成后,最终的位图会被加到内存缓存以及disk缓存中以备将来的使用。

配置的变化的处理

实时发生的配置改变,像屏幕方向的改变,会导致android销毁运行中的activity,然后以新的配置重新启动activity。当配置发生改变时,应该避免重新处理一遍所有的图片,使用户有一个平滑而快速的体验。

还好,对于上述情况,之前所创建的内存缓存可以被传递给新的activity,办法是通过调用setRetainInstance(true)来保存一个Fragment。新的activity被重建之后,保存的Fragment被重新连接,你可以访问到存在的缓存对象,这样图片可以很快的被获取到,然后重新加载到ImageView对象中。
下面是一个例子,用Fragment保存一个LruCache对象跨越配置的变化。
private LruCache<String, Bitmap> mMemoryCache;

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

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
}
做个测试,在有Fragment和没有Fragment的情况下分别选择移动设备。当你保留了缓存时,你几乎感觉不到任何延迟。在内存缓存中找不到的图片,也有可能在disk缓存中存在,如果disk缓存中也没有,那就按普通处理流程处理。
















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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值