加载一副位图到你的用户界面是很简单的,然而如果你需要马上加载一组更大的图片的话就会复杂的多.在许多情况下(例如有些组件像ListView,GridView以及ViewPager等),出现在屏幕上的图片总量,其中包括可能马上要滚动显示在屏幕上的那些图片,实际上是无限的.
那些通过回收即将移除屏幕的子视图的组件,内存使用得以保留.如果你不长期保持你对象的引用的话,垃圾收集器也会释放你所加载的位图内存.但为了保持一个流畅,快速加载,并且你想避免它们每次出现在屏幕上时重复加载处理这些图片的UI的话,这最好不过了.一个内存和磁盘的缓存通常能解决这个问题,允许组件快速地重新处理图片.
1、使用内存缓存(LruCache)
在占用宝贵的应用程序内存情况下,内存缓冲提供了可以快速访问位图.LruCache类,特别适合用于缓存位图的任务,最近被引用的对象保存在一个强引用LinkedHashMap中,以及在缓存超过了其指定的大小之前释放最近很少使用的对象的内存.
注意:在过去,一个常用的内存缓存实现是一个SoftReference或WeakReference的位图缓存,然而现在不推荐使用.从android2.3(API 级别9)开始,垃圾回收器更加注重于回收软/弱引用,这使得使用以上引用很大程度上无效.此外,之前的android3.0(API级别11),位图的备份数据存储在本地那些在一种可预测的情况下没有被释放的内存中,很有可能会导致应用程序内存溢出和崩溃.
简单看了下LruCache的源码,内部是一个强引用LinkedHashMap,LRU(Least Recently Used)在LruCache中的体现是,每次get获取访问的时候,把这个Value值移动到队首,put的时候把Value移动到队尾。有一点说明的是,LruCache这个类是线程安全的,多线程也可以同时使用,具体可以参考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); }
2、使用一个磁盘缓存(DiskLruCache)
一个内存缓冲对于加快访问最近浏览过的位图是很有用的,然而你不能局限图片在缓存中可用.像GridView这种具有更大的数据集的组件很容易地会占用所有内存缓存.你的应用程序会被别的任务像打电话等打断,并且当运行在后台时被进程杀死以及内存缓存被回收.一旦用户重新打开,你的应用程序不得不重新处理每一张图片.
在这种情况下使用磁盘缓存来持续处理位图,并且有助于在图片在内存缓存中不再可用时缩短加载时间.当然,从磁盘获取图片比从内存加载更慢并且应当在后台线程中处理,因为磁盘读取的时间是不可预知的.
在这类的情况下可以用DiskLruCache实现.DiskLruCache的源码包含在Android4.0源代码(libcore/luni/src/main/java/libcore/io/DiskLruCache.java).
注意:如果它们被更频繁地访问,那么一个ContentProvider可能是一个更合适的地方来存储缓存中的图像,例如在一个图片库应用程序里.
DiskLruCache的使用我就不多说了,csdn郭大侠已经讲得很详细了,我这里引用一下吧:
http://blog.csdn.net/guolin_blog/article/details/28863651
注意一点:我在使用的时候由于把flush方法放到了Activity的onPause方法中,有的时候虽然文件下载下来了,但是操作日志并没有保存到日志文件中,这个时候可以考虑直接放到editor的commit方法之后,看个人使用情况来定了。
简单看了下源码:DiskLruCache和LruCache挺相似的,内部都是用的LinkedHashMap来实现的,而且还用到了并发池,主要是用来写入journal文件的:
/** * This cache uses a single background thread to evict entries. */ private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
lru的体现和上面的基本一样,只不过这个是保存的是一个Entry,每次get需要new一个新对象,重新插入队列中,有一点需要说明的是journal文件最多有2000行,不过你可以自己改。
/** * We only rebuild the journal when it will halve the size of the journal * and eliminate at least 2000 ops. */ private boolean journalRebuildRequired() { final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000; return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD && redundantOpCount >= lruEntries.size(); }
3.处理并发
AsyncTask类提供了一种简单的方式来在一个后台线程中执行许多任务,并且把结果反馈给UI线程。这里不多解释AsynTask的使用,只是要说明一下在AsynTask中使用弱引用保存ImageView的方法:
class BitmapWorkerTask extends AsyncTask { private final WeakReference imageViewReference; private int data = 0; public BitmapWorkerTask(ImageView imageView) { // Use a WeakReference to ensure the ImageView can be garbage collected imageViewReference = new WeakReference(imageView); } // Decode image in background. @Override protected Bitmap doInBackground(Integer... params) { data = params[0]; return decodeSampledBitmapFromResource(getResources(), data, 100, 100)); } // Once complete, see if ImageView is still around and set bitmap. @Override protected void onPostExecute(Bitmap bitmap) { if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); if (imageView != null) { imageView.setImageBitmap(bitmap); } } } }
对于ImageView来说WeakReference确保那时AsyncTask并不会阻碍ImageView和任何它的引用被垃圾回收期回收.不能保证ImageView在任务完成后仍然存在,所以你必须在onPostExecute()方法中检查它的引用.ImageView可能不再存在,如果例如,可能是在任务完成之前用户退出了活动或者配置发生了变化.
常见的视图组件例如ListView和GridView当和AsyncTask结合使用时引出了另外一个问题.为了优化内存,当用户滚动时这些组件回收了子视图.如果每个子视图触发一个AsyncTask,当它完成时没法保证,相关的视图还没有被回收时已经用在了别的子视图当中.此外,还有异步任务开始的顺序是不能保证他们完成的顺序.
因此这里要提供一个当任务完成后ImageView将一个引用存储到后面能被检查的AsyncTask的解决方案.使用类似的方法,从上面的AsyncTask可以扩展到遵循类似的模式.
创建一个专用的Drawable的子类来存储一个引用备份到工作任务中.在这种情况下,一个BitmapDrawable被使用以便任务完成后一个占位符图像可以显示在ImageView中:
class AsyncDrawable extends BitmapDrawable { private final WeakReference bitmapWorkerTaskReference; public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { super(res, bitmap); bitmapWorkerTaskReference = new WeakReference(bitmapWorkerTask); } public BitmapWorkerTask getBitmapWorkerTask() { return bitmapWorkerTaskReference.get(); } }
这样的话,上面的BitmapWorkerTask类的onPostExecute方法也要进行一下改进:
class BitmapWorkerTask extends AsyncTask { ... @Override protected void onPostExecute(Bitmap bitmap) { if (isCancelled()) { bitmap = null; } if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (this == bitmapWorkerTask && imageView != null) { imageView.setImageBitmap(bitmap); } } } }
代码有点省略,具体的Demo我会放在下面。
4、处理运行时候配置更改:
程序运行时配置改变,例如屏幕的方向改变,导致系统销毁活动并且采用新的配置重新运行活动,你想要避免不得不再次处理所有的图片以使用户在配置发生改变时有一个平稳和快速的体验.
幸运地是,在使用一个内存缓存一节,你有一个不错的自己所构造的位图内存缓冲.缓存能通过新的活动实例来使用一个Fragment,这个Fragment是通过调用setRetainInstance(true)方法被保留的.活动被重新构造后,保留的片段重新连接,并且你获得现有的高速缓存对象的访问权限,使得图片能快速的加载并重新填充到ImageView对象中.具体在我的另外一篇博客总有介绍:Android开发实战-入门篇
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时几乎马上从内存加载,你应当注意到几乎没有滞后的感觉.任何图片都是先在内存缓存中找,没有的话再从磁盘缓存中找,如果都没有的话,就会像往常获取图片一样处理.
5、处理图片大小
现在的图像尺寸都是已知的,他们可以被用来决定是否应该加载完整的图片到内存或者是否用一个缩小的版本去代替加载。以下是一些值得考虑的因素:
估计加载完整图像所需要的内存;
你承诺加载这个图片所需空间带给你的程序的其他内存需求;
准备加载图像的目标ImageView或UI组件尺寸;
当前设备的屏幕尺寸和密度;
例如,如果1024*768像素的图像最终被缩略地显示在一个128*96像素的ImageView中,就不值得加载到内存中去。
告诉解码器去重新采样这个图像,加载一个更小的版本到内存中,在你的BitmapFactory.Option对象中设置inSampleSize为true。例如,将一个分辨率为2048*1536的图像用 inSampleSize值为4去编码将产生一个大小为大约512*384的位图。加载这个到内存中仅使用0.75MB,而不是完整的12MB大小的图像(假设使用ARGB_8888位图的配置)。这里有一个方法在目标的宽度和高度的基础上来计算一个SampleSize的值。
public static int calculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { if (width > height) { inSampleSize = Math.round((float) height / (float) reqHeight); } else { inSampleSize = Math.round((float) width / (float) reqWidth); } } return inSampleSize; }
NOTE:使用2的幂数设置inSampleSize的值可以使解码器更快,更有效。然而,如果你想在内存或硬盘中缓存一个图片调整后的版本,通常解码到合适的图像尺寸更适合来节省空间。
要使用这种方法,首先解码,将inJustDecodeBounds设置为true,将选项传递进去,然后再次解码,在使用新的inSampleSize值并将inJustDecodeBounds设置为false:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // First decode with inJustDecodeBounds=true to check dimensions final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); }