Android 图片缓存机制的深入理解
Android加载一张图片到用户界面是很简单的,但是当一次加载多张图片时,情况就变得复杂起来。很多情况下(像ListView、GridView或ViewPager等组件),屏幕上已显示的图片和即将滑动到当前屏幕上的图片数量基本上是没有限制的。
这些组件通过重用已经移除屏幕的子视图来将降低内存的使用,垃圾回收器也会及时释放那些已经不再使用的已下载的图片,这些都是很好的方法,但是为了保持一个流畅的、快速加载的用户界面,就应该避免当再次回到某个页面时而重新处理图片。内存缓存和磁盘缓存可以帮我们做到这些,它们允许组件快速地重新加载已处理好的图片。
使用内存缓存
内存缓存允许快速地访问图片,但它以占用App宝贵的内存为代价。LruCache类(API Level 4的Support Library也支持)特别适合来做图片缓存,它使用一个强引用的LinkedHashMap来保存最近使用的对象,并且会在缓存数量超出预设的大小之前移除最近最少使用的对象。
说明:以前流行的内存缓存方案是使用软引用或弱引用来缓存图片,然而现在不推荐这样做了,因为从android 2.3(API Level 9)起,垃圾收集器更倾向于先回收软引用或弱引用,这样就使它们变得低效。另外在Android 3.0(API Level 11)之前,图片的像素数据是存储在本地内存(native memory)中的,它以一种不可预测的方式释放,因此可能会导致App超过内存限制甚至崩溃。
为了给LruCache设置一个合适的大小,以下是应该考虑的一些因素:
1.你的Activity或App的可用内存是多少?
2.一次展示到屏幕上的图片是多少?有多少图片需要预先准备好以便随时加载到屏幕?
3.设备的屏幕尺寸和密度是多少?像Galaxy Nexus这样的高分辨率(xhdpi)设备比Nexus S这样分辨率(hdpi)的设备在缓存相同数量的图片时需要更大的缓存空间。
4.图片的尺寸和配置是怎样的?每张图片会占用多少内存?
5.图片的访问频率如何?是否有一些图片比另一些访问更加频繁?如果这样的话,或许可以将某些图片一直保存在内存里或者针对不同的图片分组设置不同的LruCache对象。
6.你能否平衡图片质量和数量之间的关系?有时候存储更多低质量的图片更加有用,当在需要的时候,再通过后台任务下载高质量的图片。
这里没有一个具体的大小和计算公式适用于所有的App,你需要分析你的使用情况并得到一个合适的方案。当一个缓存太小时会导致无益的额外的开销,而缓存太大时也可能会引起Java.lang.OutOfMemory异常,另外缓存越大,留给App其他部分的内存相应就越小。
这里是一个为图片设置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. final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // Use 1/8th of the available memory for this memory cache. 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; } }; ... } 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作为缓存大小,在一个normal/hdpi的设备上最少也有4MB(32/8)的大小。一个800*480分辨率的屏幕上的一个填满图片的GridView大概占用1.5MB(800*480*4byte)的内存,因此该Cache至少可以缓存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); } }
上述线程中,在解码图片之后,也需要把它添加到内存缓存中:
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; } ... }
使用磁盘缓存
虽然内存缓存在快速访问最近使用的图片时是很有用的,但是你无法保证你所需要的图片就在缓存中,类似GridView这样展示大量数据的组件可以很轻易地就占满内存缓存。你的App也可能被类似电话这样的任务打断,当App被切换到后台后也可能被杀死,内存缓存也可能被销毁,一旦用户回到之前的界面,你的App依然要重新处理每个图片。
磁盘缓存可以用来辅助存储处理过的图片,当内存缓存中图片不可用时,可以从磁盘缓存中查找,从而减少加载次数。当然,从磁盘读取图片要比从内存读取慢并且读取时间是不可预期的,因此需要使用后台线程来读取。
说明:ContentProvider 可能是一个合适的存储频繁访问的图片的地方,比如在Image Gallery应用中。
这里的示例代码是从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); 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. 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); }
说明:初始化磁盘缓存需要磁盘操作因此它不应在主线程进行,然而这意味着有可能在磁盘缓存尚未初始化之前就有访问操作发生,为了解决这个问题,在上面的实现中,使用一个锁对象来确保只有在磁盘缓存初始化之后才会从磁盘缓存读取数据。
内存缓存可以直接在UI线程读取,然而磁盘缓存必须在后台线程检查,磁盘操作不应该在UI线程发生。当图片处理完毕后,务必将最终的图片添加到内存缓存和磁盘缓存以备后续使用。
以上就是对Android 图片缓存机制的详解,如有疑问请留言或者到本站社区交流讨论,感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!
本文向大家介绍Android中图片的三级缓存机制,包括了Android中图片的三级缓存机制的使用技巧和注意事项,需要的朋友参考一下 我们不能每次加载图片的时候都让用户从网络上下载,这样不仅浪费流量又会影响用户体验,所以Android中引入了图片的缓存这一操作机制。 原理: 首先根据图片的网络地址在网络上下载图片,将图片先缓存到内存缓存中,缓存到强引用中 也就是LruCache中。如果强引用中空
本文向大家介绍深入理解Ruby on Rails中的缓存机制,包括了深入理解Ruby on Rails中的缓存机制的使用技巧和注意事项,需要的朋友参考一下 几个场景 首先,让我先来带您浏览几个 ChangingThePresent.org 中的页面吧。我将显示站点中几个需要缓存的地方。然后,再指出我们为其中每个地方所做出的选择以及为实现这些页面所使用的代码或策略。尤其会重点讨论如下内容:
本文向大家介绍Android 图片的三级缓存机制实例分析,包括了Android 图片的三级缓存机制实例分析的使用技巧和注意事项,需要的朋友参考一下 Android 图片的三级缓存机制实例分析 当我们获取图片的时候,如果不加以协调好图片的缓存,就会造成大流量,费流量应用,用户体验不好,影响后期发展。为此,我特地分享Android图片的三级缓存机制之从网络中获取图片,来优化应用,具体分三步进行: (1
本文向大家介绍深入理解Mybatis二级缓存,包括了深入理解Mybatis二级缓存的使用技巧和注意事项,需要的朋友参考一下 上篇文章给大家介绍了mybatis一级缓存,有需要的朋友可以参考下。 与一级缓存相比,二级缓存范围更大了一些,可以被多个SqlSession所共用。 同样是发送同样的查询sql会先去缓存中找,找不到再去查询数据库。 每个namespace的mapper都会有自己的一个缓存的空
本文向大家介绍深入理解Mybatis一级缓存,包括了深入理解Mybatis一级缓存的使用技巧和注意事项,需要的朋友参考一下 客户端向数据库服务器发送同样的sql查询语句,如果每次都去访问数据库,会导致性能的降低。 那么怎么提高呢? mybatis为我们提供了一级缓存的策略 在一个sqlSession开启和关闭之间,sqlSession对象内部(其实是Executor)会维护一个缓存的对象,当查询数
本文向大家介绍深入理解Android之接口回调机制,包括了深入理解Android之接口回调机制的使用技巧和注意事项,需要的朋友参考一下 刚开始学对于这个机制理解不够深刻,现在重新整理下思路。开发中,接口回调是我们经常用到的。 接口回调的意思即,注册之后并不立马执行,而在某个时机触发执行。 首先解决啥是回调: 举个例子:某天,我打电话向你请教问题,当然是个难题,你一时想不出解决方法,我又不能拿着电话