《Android开发艺术探索》12章 Bitmap的加载和Cache

由于Bitmap的特殊以及Android对单个应用所施加的内存限制,比如16MB,这导致加载Bitmap的时候很容易出现内存溢出。下面这个异常信息在开发中应该时常遇到:

java.lang.OutofMemoryError:bitmap size exceeds VM budget

因此如何高效地加载Bitmap是一个很重要也很容易被开发者忽视的问题。
12.1 Bitmap的高效加载

  首先如何加载Bitmap:
        Bitmap在Android中指的是一张图片,可以是png格式也可以是jpg等其他常见的图片格式。那么如何加载一个图片呢?BitmapFactory类提供了四类方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分别用于支持从文件系统、资源、输入流以及字节数组中加载出一个Bitmap对象,其中decodeFile和decodeResource又间接调用了decodeStream方法,这四类方法最终是在Android的底层实现的,对应着BitmapFactory类的几个native方法。
  1. 如何高效地加载Bitmap呢?

其实核心思想也很简单,那就是采用BitmapFactory.Options来加载所需尺寸的图片。这里假设通过ImageView来显示图片,很多时候ImageView并没有图片的原始尺寸那么大,这个时候把整个图片加载进来后再设给ImageView,这显然是没必要的,因为ImageView并没有办法显示原始的图片。通过BitmapFactory.Options就可以按一定的采样率来加载缩小后的图片,将缩小后的图片在ImageView中显示,这样就会降低内存占用从而在一定程度上避免OOM,提高了Bitmap加载时的性能。
BitmapFactory提供的加载图片的四类方法都支持BitmapFactory.Options参数,通过它们就可以很方便地对一个图片进行采样缩放。

  • 根据BitmapFactory.Options中的inSampleSize参数的大小进行像素的缩放。缩放比例【1/(inSampleSize的2次方)】
  • 官方文档指出:inSampleSize的取值应该总是为2的指数,比如1、2、4、8、16,等等。如果外界传递给系统的inSampleSize不为2的指数,那么系统会向下取整并选择一个最接近2的指数来代替,比如3,系统会选择2来代替.

通过采样率即可有效地加载图片,那么到底如何获取采样率呢?获取采样率也很简单,遵循如下流程:

  1. 将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片。
  2. 从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数。
  3. 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize。
  4. 将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片。

    有了上面的两个方法,实际使用的时候就很简单了,比如ImageView所期望的图片大小为100*100像素,这个时候就可以通过如下方式高校地加载并显示图片:

mImageView.setImageBitmap(decodeSampleBitmapFromResource(getResources(),R.id.myimage,100,100))

除了BitmapFactory的decodeResource方法,其他三个decode系列的方法也是支持采样加载的,并且处理方式也是类似的,但是decodeStream方法稍微有点特殊。
12.2Android中的缓存策略

如何避免过多的流量消耗呢?

缓存

当程序第一次从网络加载图片后,就将其缓存到存储设备上,这样下次使用这张图片就不用再从网络上获取了,这样就为用户节省了流量。很多时候为了提高应用的用户体验,往往还会把图片在内存中再缓存一份,这样当应用打算从网络上请求一张图片时,程序会首先从内存中去获取,如果内存中没有那就从存储设备中去获取,如果存储设备中也没有,那就从网络上下载这张图片。因为从内存中加载图片比从存储设备中加载图片要快,所以这样既提高了程序的效率又为用户节约了不必要的流量开销。

  • 目前常用的一种缓存算法是LRU(Least Recently Used),LRU是近期最少使用的算法,:
  • 核心思想是当缓存满时,会有线淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LruCache和DiskLruCache,LruCache用于实现内存缓存,而DiskLruCache则充当了存储设备缓存,通过这二者的完美结合,就可以很方便地实现一个具有很高实用价值的ImageLoader.

LruCache是一个泛型类,它内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象。这里读者要明白强引用、软引用和弱引用的区别,如下所示:

  • 强引用:直接的对象引用;
  • 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收;
  • 弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收。
    另外LruCache是线程安全的,下面是LruCache的定义:
public class LruCache<K,V>{
    private final LinkedHashMap<k,v> map;
    ....
}

除了LruCache的创建以外,还有缓存的获取和添加,这也很简单,从LruCache中获取一个缓存对象,如下所示:

mMemoryCache.get(key);

向LruCache中添加一个缓存对象,如下所示:

mMemoryCache.put(key,bitmap);

12.2.2 DiskLruCache

  • DiskLruCache的创建
    DiskLruCache并不能通过构造方法来创建,它提供了open方法用于创建自身,如下所示:
public static DiskLruCache open(File directory,int appVersion,int valueCount,long maxSize)
  • DiskLruCache的缓存的创建、添加和查找
    DiskLruCache的缓存添加的操作是通过Editor完成的,Editor表示一个缓存对象的编程对象。
    12.2.3 ImageLoader的实现
    一般来说,一个优秀的ImageLoader应该具备如下功能:
  • 图片的同步加载;
  • 图片的异步加载;
  • 图片压缩;
  • 内存缓存;
  • 磁盘缓存;
  • 网络拉取;
    上面对ImageLoader的功能做了一个全面的分析,下面就可以一步步实现一个ImageLoader了,这里主要分为如下几步。

  • 图片压缩功能的实现下面的类用于完成图片的压缩功能

public class ImageResizer{
private static final String TAG="ImageResizer";
public ImageResizer(){
}
public Bitmap decodeSampleBitmapFromResource(Resources res,int resId,int reqWidth,int reqHeight){
  final BitmapFactory.Options options=new BitmapFactory.Options();
  options.inJustDecodeBounds=true;
  BitmapFactory.decodeResource(res,resId,options);
 options.inSampleSize=calculateInSampleSize(options,reqWidth,reqHeight);
  options.inJustDecodeBounds=false;
  return BitmapFactory.decodeResource(res,resId,options);
}
public Bitmap decodeSampledBitmapFromFileDescriptior(FileDescriptor fd,int reqWidth,int reqHeight){
  final BitmapFactory.Options options=new BitmapFactory.Options();
  options.inJustDecodeBounds=true;
  BitmapFactory.decodeFileDescriptor(fd,null,options);
  options.SampleSize=calculateInSampleSize(options,reqWidth,reqHeight);
  options.inJustDecodeBounds=false;
  return BitmapFactory.decodeFileDescriptor(fd,null,options);
}
public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth,int reqHeight){
  if(reqWidth==0||reqHeight==0){
    return 1;
}
final int height=options.outHeight;
finla int width=options.outWidth;
int inSampleSize=1;
if(height>reqHeight||width>reqWidth){
   final int halfHeight=height/2;
   final int halfWidth=width/2;
   while((halfHeight/inSampleSize)>=reqHeight&&(halfWidth/inSampleSize)>=reqWidth){
   inSampleSize*=2;
  }
}
return inSampleSize;
  }
}

ImageLoader的使用
12.3.2 优化列表的卡顿现象
这个问题困扰了很多开发者,其实答案很简单,不要再主线程中做太耗时的操作即可提高滑动的流畅度,可以从三个方面来说明这个问题。

 首先,不要再getView中执行耗时操作。对于上面的例子来说,如果直接在getView方法中加载图片,肯定会导致卡顿,因为加载图片是一个耗时的操作,这种操作必须通过异步的方式来处理,就像ImageLoader实现的那样。

  其次,控制异步任务的执行频率。这一点也很重要,对于列表来说,仅仅在getView中采用异步操作是不够的。考虑一种情况,以照片墙来说,在getView方法中会通过ImageLoader的bindBitmap方法来异步加载图片,但是如果用户刻意地频繁上下滑动,这一瞬间产生上百个异步任务,这些异步任务会造成线程池的拥堵并随即带来大量的UI更新操作,这是没有意义的。由于一瞬间存在大量的UI更新操作,这些UI操作是运行在主线程的,这就会造成一定程度的卡顿。

如何解决呢?

可以考虑在列表滑动的时候停止加载图片,尽管这个过程是异步的,等列表停下来以后再加载图片仍然可以获得良好的用户体验。具体实现时,可以给ListView或者GridView设置setOnScrollListener,并在OnScrollListener的onScrollStateChanged方法中判断列表是否处于滑动状态,如果是的话就停止加载图片,如下所示:

public void onScrollSateChanged(AbsListView view,int scrollState){
   if(scrollState==onScrollListener.SCROLL_STATE_IDLE){
       mIsGridViewIdle=true;
       mImageAdapter.notifyDataSetChanged();
   }else{
     mIsGridViewIdle=false;
   }
}

然后再getView方法中,仅当列表静止时才能加载图片,如下所示:

if(mIsGridViewIdle && mCanGetBitmapFromNetWork){
     imageView.setTag(uri);
     mImageLoader.bindBitmap(uri,imageView,mImageWidth,mImageWidth);
}

一般来说,经过上面两个步骤,列表都不会有卡顿现象,但是在某些情况下,列表还是会有偶尔的卡顿现象,这个时候还可以开启硬件加速。绝大多数情况下,硬件加速都可以解决莫名的卡顿问题,通过设置android:hardwareAccelerated=”true”即可为Activity开启硬件加速。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值