Welcome 微信登录

首页 / 移动开发 / Android / Android App中实现相册瀑布流展示的实例分享

传统界面的布局方式总是行列分明、坐落有序的,这种布局已是司空见惯,在不知不觉中大家都已经对它产生了审美疲劳。这个时候瀑布流布局的出现,就给人带来了耳目一新的感觉,这种布局虽然看上去貌似毫无规律,但是却有一种说不上来的美感,以至于涌现出了大批的网站和应用纷纷使用这种新颖的布局来设计界面。
记得我在之前已经写过一篇关于如何在Android上实现照片墙功能的文章了,但那个时候是使用的GridView来进行布局的,这种布局方式只适用于“墙”上的每张图片大小都相同的情况,如果图片的大小参差不齐,在GridView中显示就会非常的难看。而使用瀑布流的布局方式就可以很好地解决这个问题,因此今天我们也来赶一下潮流,看看如何在Android上实现瀑布流照片墙的功能。
首先还是讲一下实现原理,瀑布流的布局方式虽然看起来好像排列的很随意,其实它是有很科学的排列规则的。整个界面会根据屏幕的宽度划分成等宽的若干列,由于手机的屏幕不是很大,这里我们就分成三列。每当需要添加一张图片时,会将这张图片的宽度压缩成和列一样宽,再按照同样的压缩比例对图片的高度进行压缩,然后在这三列中找出当前高度最小的一列,将图片添加到这一列中。之后每当需要添加一张新图片时,都去重复上面的操作,就会形成瀑布流格局的照片墙,示意图如下所示。

201648235457901.png (347×459)
听我这么说完后,你可能会觉得瀑布流的布局非常简单嘛,只需要使用三个LinearLayout平分整个屏幕宽度,然后动态地addView()进去就好了。确实如此,如果只是为了实现功能的话,就是这么简单。可是别忘了,我们是在手机上进行开发,如果不停地往LinearLayout里添加图片,程序很快就会OOM。因此我们还需要一个合理的方案来对图片资源进行释放,这里仍然是准备使用LruCache算法,这个具体的在文后会专门讲,先知道是用这么回事~
下面我们就来开始实现吧,新建一个Android项目,起名叫PhotoWallFallsDemo,并选择4.0的API。
第一个要考虑的问题是,我们到哪儿去收集这些大小参差不齐的图片呢?这里我事先在百度上搜索了很多张风景图片,并且为了保证它们访问的稳定性,我将这些图片都上传到了我的CSDN相册里,因此只要从这里下载图片就可以了。新建一个Images类,将所有相册中图片的网址都配置进去,代码如下所示:
public class Images { public final static String[] imageUrls = new String[] {"http://img.my.csdn.net/uploads/201309/01/1378037235_3453.jpg","http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg","http://img.my.csdn.net/uploads/201309/01/1378037235_9280.jpg","http://img.my.csdn.net/uploads/201309/01/1378037234_3539.jpg","http://img.my.csdn.net/uploads/201309/01/1378037234_6318.jpg","http://img.my.csdn.net/uploads/201309/01/1378037194_2965.jpg","http://img.my.csdn.net/uploads/201309/01/1378037193_1687.jpg","http://img.my.csdn.net/uploads/201309/01/1378037193_1286.jpg","http://img.my.csdn.net/uploads/201309/01/1378037192_8379.jpg","http://img.my.csdn.net/uploads/201309/01/1378037178_9374.jpg","http://img.my.csdn.net/uploads/201309/01/1378037177_1254.jpg","http://img.my.csdn.net/uploads/201309/01/1378037177_6203.jpg","http://img.my.csdn.net/uploads/201309/01/1378037152_6352.jpg","http://img.my.csdn.net/uploads/201309/01/1378037151_9565.jpg","http://img.my.csdn.net/uploads/201309/01/1378037151_7904.jpg","http://img.my.csdn.net/uploads/201309/01/1378037148_7104.jpg","http://img.my.csdn.net/uploads/201309/01/1378037129_8825.jpg","http://img.my.csdn.net/uploads/201309/01/1378037128_5291.jpg","http://img.my.csdn.net/uploads/201309/01/1378037128_3531.jpg","http://img.my.csdn.net/uploads/201309/01/1378037127_1085.jpg","http://img.my.csdn.net/uploads/201309/01/1378037095_7515.jpg","http://img.my.csdn.net/uploads/201309/01/1378037094_8001.jpg","http://img.my.csdn.net/uploads/201309/01/1378037093_7168.jpg","http://img.my.csdn.net/uploads/201309/01/1378037091_4950.jpg","http://img.my.csdn.net/uploads/201308/31/1377949643_6410.jpg","http://img.my.csdn.net/uploads/201308/31/1377949642_6939.jpg","http://img.my.csdn.net/uploads/201308/31/1377949630_4505.jpg","http://img.my.csdn.net/uploads/201308/31/1377949630_4593.jpg","http://img.my.csdn.net/uploads/201308/31/1377949629_7309.jpg","http://img.my.csdn.net/uploads/201308/31/1377949629_8247.jpg","http://img.my.csdn.net/uploads/201308/31/1377949615_1986.jpg","http://img.my.csdn.net/uploads/201308/31/1377949614_8482.jpg","http://img.my.csdn.net/uploads/201308/31/1377949614_3743.jpg","http://img.my.csdn.net/uploads/201308/31/1377949614_4199.jpg","http://img.my.csdn.net/uploads/201308/31/1377949599_3416.jpg","http://img.my.csdn.net/uploads/201308/31/1377949599_5269.jpg","http://img.my.csdn.net/uploads/201308/31/1377949598_7858.jpg","http://img.my.csdn.net/uploads/201308/31/1377949598_9982.jpg","http://img.my.csdn.net/uploads/201308/31/1377949578_2770.jpg","http://img.my.csdn.net/uploads/201308/31/1377949578_8744.jpg","http://img.my.csdn.net/uploads/201308/31/1377949577_5210.jpg","http://img.my.csdn.net/uploads/201308/31/1377949577_1998.jpg","http://img.my.csdn.net/uploads/201308/31/1377949482_8813.jpg","http://img.my.csdn.net/uploads/201308/31/1377949481_6577.jpg","http://img.my.csdn.net/uploads/201308/31/1377949480_4490.jpg","http://img.my.csdn.net/uploads/201308/31/1377949455_6792.jpg","http://img.my.csdn.net/uploads/201308/31/1377949455_6345.jpg","http://img.my.csdn.net/uploads/201308/31/1377949442_4553.jpg","http://img.my.csdn.net/uploads/201308/31/1377949441_8987.jpg","http://img.my.csdn.net/uploads/201308/31/1377949441_5454.jpg","http://img.my.csdn.net/uploads/201308/31/1377949454_6367.jpg","http://img.my.csdn.net/uploads/201308/31/1377949442_4562.jpg" }; } 
然后新建一个ImageLoader类,用于方便对图片进行管理,代码如下所示:
public class ImageLoader { /** * 图片缓存技术的核心类,用于缓存所有下载好的图片,在程序内存达到设定值时会将最少最近使用的图片移除掉。 */private static LruCache<String, Bitmap> mMemoryCache; /** * ImageLoader的实例。 */private static ImageLoader mImageLoader; private ImageLoader() { // 获取应用程序最大可用内存 int maxMemory = (int) Runtime.getRuntime().maxMemory(); int cacheSize = maxMemory / 8; // 设置图片缓存大小为程序最大可用内存的1/8 mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {@Overrideprotected int sizeOf(String key, Bitmap bitmap) { return bitmap.getByteCount();} };} /** * 获取ImageLoader的实例。 * * @return ImageLoader的实例。 */public static ImageLoader getInstance() { if (mImageLoader == null) {mImageLoader = new ImageLoader(); } return mImageLoader;} /** * 将一张图片存储到LruCache中。 * * @param key * LruCache的键,这里传入图片的URL地址。 * @param bitmap * LruCache的键,这里传入从网络上下载的Bitmap对象。 */public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemoryCache(key) == null) {mMemoryCache.put(key, bitmap); }} /** * 从LruCache中获取一张图片,如果不存在就返回null。 * * @param key * LruCache的键,这里传入图片的URL地址。 * @return 对应传入键的Bitmap对象,或者null。 */public Bitmap getBitmapFromMemoryCache(String key) { return mMemoryCache.get(key);} public static int calculateInSampleSize(BitmapFactory.Options options,int reqWidth) { // 源图片的宽度 final int width = options.outWidth; int inSampleSize = 1; if (width > reqWidth) {// 计算出实际宽度和目标宽度的比率final int widthRatio = Math.round((float) width / (float) reqWidth);inSampleSize = widthRatio; } return inSampleSize;} public static Bitmap decodeSampledBitmapFromResource(String pathName,int reqWidth) { // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小 final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(pathName, options); // 调用上面定义的方法计算inSampleSize值 options.inSampleSize = calculateInSampleSize(options, reqWidth); // 使用获取到的inSampleSize值再次解析图片 options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(pathName, options);}} 
这里我们将ImageLoader类设成单例,并在构造函数中初始化了LruCache类,把它的最大缓存容量设为最大可用内存的1/8。然后又提供了其它几个方法可以操作LruCache,以及对图片进行压缩和读取。
接下来新建MyScrollView继承自ScrollView,代码如下所示:
public class MyScrollView extends ScrollView implements OnTouchListener { /** * 每页要加载的图片数量 */public static final int PAGE_SIZE = 15; /** * 记录当前已加载到第几页 */private int page; /** * 每一列的宽度 */private int columnWidth; /** * 当前第一列的高度 */private int firstColumnHeight; /** * 当前第二列的高度 */private int secondColumnHeight; /** * 当前第三列的高度 */private int thirdColumnHeight; /** * 是否已加载过一次layout,这里onLayout中的初始化只需加载一次 */private boolean loadOnce; /** * 对图片进行管理的工具类 */private ImageLoader imageLoader; /** * 第一列的布局 */private LinearLayout firstColumn; /** * 第二列的布局 */private LinearLayout secondColumn; /** * 第三列的布局 */private LinearLayout thirdColumn; /** * 记录所有正在下载或等待下载的任务。 */private static Set<LoadImageTask> taskCollection; /** * MyScrollView下的直接子布局。 */private static View scrollLayout; /** * MyScrollView布局的高度。 */private static int scrollViewHeight; /** * 记录上垂直方向的滚动距离。 */private static int lastScrollY = -1; /** * 记录所有界面上的图片,用以可以随时控制对图片的释放。 */private List<ImageView> imageViewList = new ArrayList<ImageView>(); /** * 在Handler中进行图片可见性检查的判断,以及加载更多图片的操作。 */private static Handler handler = new Handler() {public void handleMessage(android.os.Message msg) {MyScrollView myScrollView = (MyScrollView) msg.obj;int scrollY = myScrollView.getScrollY();// 如果当前的滚动位置和上次相同,表示已停止滚动if (scrollY == lastScrollY) { // 当滚动的最底部,并且当前没有正在下载的任务时,开始加载下一页的图片 if (scrollViewHeight + scrollY >= scrollLayout.getHeight() && taskCollection.isEmpty()) {myScrollView.loadMoreImages(); } myScrollView.checkVisibility();} else { lastScrollY = scrollY; Message message = new Message(); message.obj = myScrollView; // 5毫秒后再次对滚动位置进行判断 handler.sendMessageDelayed(message, 5);} }; }; /** * MyScrollView的构造函数。 * * @param context * @param attrs */public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); imageLoader = ImageLoader.getInstance(); taskCollection = new HashSet<LoadImageTask>(); setOnTouchListener(this);} /** * 进行一些关键性的初始化操作,获取MyScrollView的高度,以及得到第一列的宽度值。并在这里开始加载第一页的图片。 */@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (changed && !loadOnce) {scrollViewHeight = getHeight();scrollLayout = getChildAt(0);firstColumn = (LinearLayout) findViewById(R.id.first_column);secondColumn = (LinearLayout) findViewById(R.id.second_column);thirdColumn = (LinearLayout) findViewById(R.id.third_column);columnWidth = firstColumn.getWidth();loadOnce = true;loadMoreImages(); }} /** * 监听用户的触屏事件,如果用户手指离开屏幕则开始进行滚动检测。 */@Overridepublic boolean onTouch(View v, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP) {Message message = new Message();message.obj = this;handler.sendMessageDelayed(message, 5); } return false;} /** * 开始加载下一页的图片,每张图片都会开启一个异步线程去下载。 */public void loadMoreImages() { if (hasSDCard()) {int startIndex = page * PAGE_SIZE;int endIndex = page * PAGE_SIZE + PAGE_SIZE;if (startIndex < Images.imageUrls.length) { Toast.makeText(getContext(), "正在加载...", Toast.LENGTH_SHORT) .show(); if (endIndex > Images.imageUrls.length) {endIndex = Images.imageUrls.length; } for (int i = startIndex; i < endIndex; i++) {LoadImageTask task = new LoadImageTask();taskCollection.add(task);task.execute(Images.imageUrls[i]); } page++;} else { Toast.makeText(getContext(), "已没有更多图片", Toast.LENGTH_SHORT) .show();} } else {Toast.makeText(getContext(), "未发现SD卡", Toast.LENGTH_SHORT).show(); }} /** * 遍历imageViewList中的每张图片,对图片的可见性进行检查,如果图片已经离开屏幕可见范围,则将图片替换成一张空图。 */public void checkVisibility() { for (int i = 0; i < imageViewList.size(); i++) {ImageView imageView = imageViewList.get(i);int borderTop = (Integer) imageView.getTag(R.string.border_top);int borderBottom = (Integer) imageView.getTag(R.string.border_bottom);if (borderBottom > getScrollY()&& borderTop < getScrollY() + scrollViewHeight) { String imageUrl = (String) imageView.getTag(R.string.image_url); Bitmap bitmap = imageLoader.getBitmapFromMemoryCache(imageUrl); if (bitmap != null) {imageView.setImageBitmap(bitmap); } else {LoadImageTask task = new LoadImageTask(imageView);task.execute(imageUrl); }} else { imageView.setImageResource(R.drawable.empty_photo);} }} /** * 判断手机是否有SD卡。 * * @return 有SD卡返回true,没有返回false。 */private boolean hasSDCard() { return Environment.MEDIA_MOUNTED.equals(Environment .getExternalStorageState());} /** * 异步下载图片的任务。 * * @author guolin */class LoadImageTask extends AsyncTask<String, Void, Bitmap> {/*** 图片的URL地址*/ private String mImageUrl;/*** 可重复使用的ImageView*/ private ImageView mImageView;public LoadImageTask() { }/*** 将可重复使用的ImageView传入** @param imageView*/ public LoadImageTask(ImageView imageView) {mImageView = imageView; }@Override protected Bitmap doInBackground(String... params) {mImageUrl = params[0];Bitmap imageBitmap = imageLoader.getBitmapFromMemoryCache(mImageUrl);if (imageBitmap == null) { imageBitmap = loadImage(mImageUrl);}return imageBitmap; }@Override protected void onPostExecute(Bitmap bitmap) {if (bitmap != null) { double ratio = bitmap.getWidth() / (columnWidth * 1.0); int scaledHeight = (int) (bitmap.getHeight() / ratio); addImage(bitmap, columnWidth, scaledHeight);}taskCollection.remove(this); }/*** 根据传入的URL,对图片进行加载。如果这张图片已经存在于SD卡中,则直接从SD卡里读取,否则就从网络上下载。** @param imageUrl* 图片的URL地址* @return 加载到内存的图片。*/ private Bitmap loadImage(String imageUrl) {File imageFile = new File(getImagePath(imageUrl));if (!imageFile.exists()) { downloadImage(imageUrl);}if (imageUrl != null) { Bitmap bitmap = ImageLoader.decodeSampledBitmapFromResource( imageFile.getPath(), columnWidth); if (bitmap != null) {imageLoader.addBitmapToMemoryCache(imageUrl, bitmap);return bitmap; }}return null; }/*** 向ImageView中添加一张图片** @param bitmap* 待添加的图片* @param imageWidth* 图片的宽度* @param imageHeight* 图片的高度*/ private void addImage(Bitmap bitmap, int imageWidth, int imageHeight) {LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(imageWidth, imageHeight);if (mImageView != null) { mImageView.setImageBitmap(bitmap);} else { ImageView imageView = new ImageView(getContext()); imageView.setLayoutParams(params); imageView.setImageBitmap(bitmap); imageView.setScaleType(ScaleType.FIT_XY); imageView.setPadding(5, 5, 5, 5); imageView.setTag(R.string.image_url, mImageUrl); findColumnToAdd(imageView, imageHeight).addView(imageView); imageViewList.add(imageView);} }/*** 找到此时应该添加图片的一列。原则就是对三列的高度进行判断,当前高度最小的一列就是应该添加的一列。** @param imageView* @param imageHeight* @return 应该添加图片的一列*/ private LinearLayout findColumnToAdd(ImageView imageView, int imageHeight) {if (firstColumnHeight <= secondColumnHeight) { if (firstColumnHeight <= thirdColumnHeight) {imageView.setTag(R.string.border_top, firstColumnHeight);firstColumnHeight += imageHeight;imageView.setTag(R.string.border_bottom, firstColumnHeight);return firstColumn; } imageView.setTag(R.string.border_top, thirdColumnHeight); thirdColumnHeight += imageHeight; imageView.setTag(R.string.border_bottom, thirdColumnHeight); return thirdColumn;} else { if (secondColumnHeight <= thirdColumnHeight) {imageView.setTag(R.string.border_top, secondColumnHeight);secondColumnHeight += imageHeight;imageView.setTag(R.string.border_bottom, secondColumnHeight);return secondColumn; } imageView.setTag(R.string.border_top, thirdColumnHeight); thirdColumnHeight += imageHeight; imageView.setTag(R.string.border_bottom, thirdColumnHeight); return thirdColumn;} }/*** 将图片下载到SD卡缓存起来。** @param imageUrl* 图片的URL地址。*/ private void downloadImage(String imageUrl) {HttpURLConnection con = null;FileOutputStream fos = null;BufferedOutputStream bos = null;BufferedInputStream bis = null;File imageFile = null;try { URL url = new URL(imageUrl); con = (HttpURLConnection) url.openConnection(); con.setConnectTimeout(5 * 1000); con.setReadTimeout(15 * 1000); con.setDoInput(true); con.setDoOutput(true); bis = new BufferedInputStream(con.getInputStream()); imageFile = new File(getImagePath(imageUrl)); fos = new FileOutputStream(imageFile); bos = new BufferedOutputStream(fos); byte[] b = new byte[1024]; int length; while ((length = bis.read(b)) != -1) {bos.write(b, 0, length);bos.flush(); }} catch (Exception e) { e.printStackTrace();} finally { try {if (bis != null) { bis.close();}if (bos != null) { bos.close();}if (con != null) { con.disconnect();} } catch (IOException e) {e.printStackTrace(); }}if (imageFile != null) { Bitmap bitmap = ImageLoader.decodeSampledBitmapFromResource( imageFile.getPath(), columnWidth); if (bitmap != null) {imageLoader.addBitmapToMemoryCache(imageUrl, bitmap); }} }/*** 获取图片的本地存储路径。** @param imageUrl* 图片的URL地址。* @return 图片的本地存储路径。*/ private String getImagePath(String imageUrl) {int lastSlashIndex = imageUrl.lastIndexOf("/");String imageName = imageUrl.substring(lastSlashIndex + 1);String imageDir = Environment.getExternalStorageDirectory().getPath() + "/PhotoWallFalls/";File file = new File(imageDir);if (!file.exists()) { file.mkdirs();}String imagePath = imageDir + imageName;return imagePath; }}} 
MyScrollView是实现瀑布流照片墙的核心类,这里我来重点给大家介绍一下。首先它是继承自ScrollView的,这样就允许用户可以通过滚动的方式来浏览更多的图片。这里提供了一个loadMoreImages()方法,是专门用于加载下一页的图片的,因此在onLayout()方法中我们要先调用一次这个方法,以初始化第一页的图片。然后在onTouch方法中每当监听到手指离开屏幕的事件,就会通过一个handler来对当前ScrollView的滚动状态进行判断,如果发现已经滚动到了最底部,就会再次调用loadMoreImages()方法去加载下一页的图片。
那我们就要来看一看loadMoreImages()方法的内部细节了。在这个方法中,使用了一个循环来加载这一页中的每一张图片,每次都会开启一个LoadImageTask,用于对图片进行异步加载。然后在LoadImageTask中,首先会先检查一下这张图片是不是已经存在于SD卡中了,如果还没存在,就从网络上下载,然后把这张图片存放在LruCache中。接着将这张图按照一定的比例进行压缩,并找出当前高度最小的一列,把压缩后的图片添加进去就可以了。
另外,为了保证照片墙上的图片都能够合适地被回收,这里还加入了一个可见性检查的方法,即checkVisibility()方法。这个方法的核心思想就是检查目前照片墙上的所有图片,判断出哪些是可见的,哪些是不可见。然后将那些不可见的图片都替换成一张空图,这样就可以保证程序始终不会占用过高的内存。当这些图片又重新变为可见的时候,只需要再从LruCache中将这些图片重新取出即可。如果某张图片已经从LruCache中被移除了,就会开启一个LoadImageTask,将这张图片重新加载到内存中。
然后打开或新建activity_main.xml,在里面设置好瀑布流的布局方式,如下所示:
<com.example.photowallfallsdemo.MyScrollView xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/my_scroll_view"android:layout_width="match_parent"android:layout_height="match_parent" > <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" ><LinearLayoutandroid:id="@+id/first_column"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:orientation="vertical" > </LinearLayout><LinearLayoutandroid:id="@+id/second_column"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:orientation="vertical" > </LinearLayout><LinearLayoutandroid:id="@+id/third_column"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:orientation="vertical" > </LinearLayout></LinearLayout></com.example.photowallfallsdemo.MyScrollView>
 
可以看到,这里我们使用了刚才编写好的MyScrollView作为根布局,然后在里面放入了一个直接子布局LinearLayout用于统计当前滑动布局的高度,然后在这个布局下又添加了三个等宽的LinearLayout分别作为第一列、第二列和第三列的布局,这样在MyScrollView中就可以动态地向这三个LinearLayout里添加图片了。
最后,由于我们使用到了网络和SD卡存储的功能,因此还需要在AndroidManifest.xml中添加以下权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.INTERNET" /> 
这样我们所有的编码工作就已经完成了,现在可以尝试运行一下,效果如下图所示:

201648235542999.gif (235×418)
LruCache图片缓存技术
在你应用程序的UI界面加载一张图片是一件很简单的事情,但是当你需要在界面上加载一大堆图片的时候,情况就变得复杂起来。在很多情况下,(比如使用ListView, GridView 或者 ViewPager 这样的组件),屏幕上显示的图片可以通过滑动屏幕等事件不断地增加,最终导致OOM。
为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC操作。用这种思路来解决问题是非常好的,可是为了能让程序快速运行,在界面上迅速地加载图片,你又必须要考虑到某些图片被回收之后,用户又将它重新滑入屏幕这种情况。这时重新去加载一遍刚刚加载过的图片无疑是性能的瓶颈,你需要想办法去避免这个情况的发生。
这个时候,使用内存缓存技术可以很好的解决这个问题,它可以让组件快速地重新加载和处理图片。下面我们就来看一看如何使用内存缓存技术来对图片进行缓存,从而让你的应用程序在加载很多图片的时候可以提高响应速度和流畅性。
内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。其中最核心的类是LruCache (此类在android-support-v4的包中提供) 。这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。
为了能够选择一个合适的缓存大小给LruCache, 有以下多个因素应该放入考虑范围内,例如:
你的设备可以为每个应用程序分配多大的内存?
设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载,因为有可能很快也会显示在屏幕上?
你的设备的屏幕大小和分辨率分别是多少?一个超高分辨率的设备(例如 Galaxy Nexus) 比起一个较低分辨率的设备(例如 Nexus S),在持有相同数量图片的时候,需要更大的缓存空间。
图片的尺寸和大小,还有每张图片会占据多少内存空间。
图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片要高?如果有的话,你也许应该让一些图片常驻在内存当中,或者使用多个LruCache 对象来区分不同组的图片。
你能维持好数量和质量之间的平衡吗?有些时候,存储多个低像素的图片,而在后台去开线程加载高像素的图片会更加的有效。
并没有一个指定的缓存大小可以满足所有的应用程序,这是由你决定的。你应该去分析程序内存的使用情况,然后制定出一个合适的解决方案。一个太小的缓存空间,有可能造成图片频繁地被释放和重新加载,这并没有好处。而一个太大的缓存空间,则有可能还是会引起 java.lang.OutOfMemory 的异常。
下面是一个使用 LruCache 来缓存图片的例子:

private LruCache<String, Bitmap> mMemoryCache;@Override protected void onCreate(Bundle savedInstanceState) { // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。 // LruCache通过构造函数传入缓存值,以KB为单位。 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // 使用最大可用内存值的1/8作为缓存的大小。 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); } 
在这个例子当中,使用了系统分配给应用程序的八分之一内存来作为缓存大小。在中高配置的手机当中,这大概会有4兆(32/8)的缓存空间。一个全屏幕的 GridView 使用4张 800x480分辨率的图片来填充,则大概会占用1.5兆的空间(800*480*4)。因此,这个缓存大小可以存储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) { imageView.setImageBitmap(bitmap); } else { imageView.setImageResource(R.drawable.image_placeholder); BitmapWorkerTask task = new BitmapWorkerTask(imageView); 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; } } 
掌握了以上两种方法,不管是要在程序中加载超大图片,还是要加载大量图片,都不用担心OOM的问题了!