热门标签 | HotTags
当前位置:  开发笔记 > Android > 正文

AndroidApp中实现相册瀑布流展示的实例分享

这篇文章主要介绍了AndroidApp中实现相册瀑布流展示的实例分享,例子中利用到了缓存LruCache类的相关算法来解决大量加载问题,需要的朋友可以参考下

传统界面的布局方式总是行列分明、坐落有序的,这种布局已是司空见惯,在不知不觉中大家都已经对它产生了审美疲劳。这个时候瀑布流布局的出现,就给人带来了耳目一新的感觉,这种布局虽然看上去貌似毫无规律,但是却有一种说不上来的美感,以至于涌现出了大批的网站和应用纷纷使用这种新颖的布局来设计界面。
记得我在之前已经写过一篇关于如何在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 mMemoryCache; 
 
 /** 
  * ImageLoader的实例。 
  */ 
 private static ImageLoader mImageLoader; 
 
 private ImageLoader() { 
  // 获取应用程序最大可用内存 
  int maxMemory = (int) Runtime.getRuntime().maxMemory(); 
  int cacheSize = maxMemory / 8; 
  // 设置图片缓存大小为程序最大可用内存的1/8 
  mMemoryCache = new LruCache(cacheSize) { 
   @Override 
   protected 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 taskCollection; 
 
 /** 
  * MyScrollView下的直接子布局。 
  */ 
 private static View scrollLayout; 
 
 /** 
  * MyScrollView布局的高度。 
  */ 
 private static int scrollViewHeight; 
 
 /** 
  * 记录上垂直方向的滚动距离。 
  */ 
 private static int lastScrollY = -1; 
 
 /** 
  * 记录所有界面上的图片,用以可以随时控制对图片的释放。 
  */ 
 private List imageViewList = new ArrayList(); 
 
 /** 
  * 在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(); 
  setOnTouchListener(this); 
 } 
 
 /** 
  * 进行一些关键性的初始化操作,获取MyScrollView的高度,以及得到第一列的宽度值。并在这里开始加载第一页的图片。 
  */ 
 @Override 
 protected 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(); 
  } 
 } 
 
 /** 
  * 监听用户的触屏事件,如果用户手指离开屏幕则开始进行滚动检测。 
  */ 
 @Override 
 public 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) { 
     endIndex = Images.imageUrls.length; 
    } 
    for (int i = startIndex; i  getScrollY() 
     && borderTop  { 
 
  /** 
   * 图片的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,在里面设置好瀑布流的布局方式,如下所示:

 
 
  
 
   
   
 
   
   
 
   
   
  
 

 
可以看到,这里我们使用了刚才编写好的MyScrollView作为根布局,然后在里面放入了一个直接子布局LinearLayout用于统计当前滑动布局的高度,然后在这个布局下又添加了三个等宽的LinearLayout分别作为第一列、第二列和第三列的布局,这样在MyScrollView中就可以动态地向这三个LinearLayout里添加图片了。
最后,由于我们使用到了网络和SD卡存储的功能,因此还需要在AndroidManifest.xml中添加以下权限:

 
 

这样我们所有的编码工作就已经完成了,现在可以尝试运行一下,效果如下图所示:

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 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(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 { 
  // 在后台加载图片。 
  @Override 
  protected Bitmap doInBackground(Integer... params) { 
    final Bitmap bitmap = decodeSampledBitmapFromResource( 
        getResources(), params[0], 100, 100); 
    addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); 
    return bitmap; 
  } 
} 

掌握了以上两种方法,不管是要在程序中加载超大图片,还是要加载大量图片,都不用担心OOM的问题了!


推荐阅读
  • Android系统移植与调试之如何修改Android设备状态条上音量加减键在横竖屏切换的时候的显示于隐藏
    本文介绍了如何修改Android设备状态条上音量加减键在横竖屏切换时的显示与隐藏。通过修改系统文件system_bar.xml实现了该功能,并分享了解决思路和经验。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • “你永远都不知道明天和‘公司的意外’哪个先来。”疫情期间,这是我们最战战兢兢的心情。但是显然,有些人体会不了。这份行业数据,让笔者“柠檬” ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • Java验证码——kaptcha的使用配置及样式
    本文介绍了如何使用kaptcha库来实现Java验证码的配置和样式设置,包括pom.xml的依赖配置和web.xml中servlet的配置。 ... [详细]
  • 本文介绍了Android 7的学习笔记总结,包括最新的移动架构视频、大厂安卓面试真题和项目实战源码讲义。同时还分享了开源的完整内容,并提醒读者在使用FileProvider适配时要注意不同模块的AndroidManfiest.xml中配置的xml文件名必须不同,否则会出现问题。 ... [详细]
  • Android开发实现的计时器功能示例
    本文分享了Android开发实现的计时器功能示例,包括效果图、布局和按钮的使用。通过使用Chronometer控件,可以实现计时器功能。该示例适用于Android平台,供开发者参考。 ... [详细]
  • 本文是关于自学Android的笔记,包括查看类的源码的方法,活动注册的必要性以及布局练习的重要性。通过学习本文,读者可以了解到在自学Android过程中的一些关键点和注意事项。 ... [详细]
  • SLAM中相机运动估计的基本问题及解决方案
    本文讨论了SLAM中相机运动估计的基本问题,指出了解决方案的存在。作者认为阅读相关SLAM书籍是掌握基础原理的有效途径,而不是仅仅依赖现成的解决方案。同时,作者也提到了激光雷达和特征点匹配等技术在SLAM中的应用,并建议读者深入理解相关原理,而不是盲目追求现成的代码。 ... [详细]
  • 本文介绍了在Mac上安装Xamarin并使用Windows上的VS开发iOS app的方法,包括所需的安装环境和软件,以及使用Xamarin.iOS进行开发的步骤。通过这种方法,即使没有Mac或者安装苹果系统,程序员们也能轻松开发iOS app。 ... [详细]
  • 本文由编程笔记#小编整理,主要介绍了关于数论相关的知识,包括数论的算法和百度百科的链接。文章还介绍了欧几里得算法、辗转相除法、gcd、lcm和扩展欧几里得算法的使用方法。此外,文章还提到了数论在求解不定方程、模线性方程和乘法逆元方面的应用。摘要长度:184字。 ... [详细]
  • 本文介绍了如何使用JSONObiect和Gson相关方法实现json数据与kotlin对象的相互转换。首先解释了JSON的概念和数据格式,然后详细介绍了相关API,包括JSONObject和Gson的使用方法。接着讲解了如何将json格式的字符串转换为kotlin对象或List,以及如何将kotlin对象转换为json字符串。最后提到了使用Map封装json对象的特殊情况。文章还对JSON和XML进行了比较,指出了JSON的优势和缺点。 ... [详细]
author-avatar
mobiledu2502871243
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有