Android Bitmap的加载优化与Cache相关介绍
一.高效加载Bitmap
BitMapFactory提供了四类方法:decodeFile,decodeResource,decodeStream和decodeByteArray分别用于从文件系统,资源,输入流以及字节数组中加载出一个Bitmap对象。
高效加载Bitmap很简单,即采用BitMapFactory.options来加载所需要尺寸图片。BitMapFactory.options就可以按照一定的采样率来加载缩小后的图片,将缩小后的图片置于ImageView中显示。
通过采样率即可高效的加载图片,遵循如下方式获取采样率:
- 将BitmapFactory.Options的inJustDecodeBounds参数设置为true并加载图片
- 从BitmapFactory.Options中取出图片的原始宽高信息,即对应于outWidth和outHeight参数
- 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize
- 将BitmapFactory.Options的injustDecodeBounds参数设置为false,然后重新加载图片
过上述四个步骤,加载出的图片就是最终缩放后的图片,当然也有可能没有缩放。
代码实现如下:
publicBitmapdecodeSampledBitmapFromResource(Resourcesres,intresId,intreqWidth,intreqHeight){
//FirstdecodewithinJustDecodeBounds=truetocheckdimensions
finalBitmapFactory.Optionsoptions=newBitmapFactory.Options();
options.inJustDecodeBounds=true;
BitmapFactory.decodeResource(res,resId,options);
//CalculateinSampleSize
options.inSampleSize=calculateInSampleSize(options,reqWidth,reqHeight);
//DecodebitmapwithinSampleSizeset
options.inJustDecodeBounds=false;
returnBitmapFactory.decodeResource(res,resId,options);
}
publicintcalculateInSampleSize(BitmapFactory.Optionsoptions,intreqWidth,intreqHeight){
if(reqWidth==0||reqHeight==0){
return1;
}
//Rawheightandwidthofimage
finalintheight=options.outHeight;
finalintwidth=options.outWidth;
Log.d(TAG,"origin,w="+width+"h="+height);
intinSampleSize=1;
if(height>reqHeight||width>reqWidth){
finalinthalfHeight=height/2;
finalinthalfWidth=width/2;
//CalculatethelargestinSampleSizevaluethatisapowerof2and
//keepsbothheightandwidthlargerthantherequestedheightandwidth.
while((halfHeight/inSampleSize)>=reqHeight&&(halfWidth/inSampleSize)>=reqWidth){
inSampleSize*=2;
}
}
Log.d(TAG,"sampleSize:"+inSampleSize);
returninSampleSize;
}
实际使用就可以像下面这样了,如加载100*100的图片大小,就可以像下面这样高效的加载图片了:
mImageView.setImageBitmap( decodeSampledBitmapFromResource(getResource(),R.id.myimage,100,100));
二.Android中的缓存策略
目前常用的算法是LRU,即近期最少使用算法,当缓存存满时,会优先淘汰近期最少使用的缓存对象
2.1LruCache
LruCache是一个泛型类,其内部实现机制是LinkedHashMap以强引用的方式存储外部的缓存对象,提供了get()和put()来完成缓存对象的存取。当缓存满了,移除较早的缓存对象,再添加新的。LruCache是线程安全的。
- 强引用:直接的对象引用
- 软引用:当一个对象只有软引用时,系统内存不足时,会被gc回收
- 弱引用:当一个对象只有弱引用时,随时会被回收
2.2DiskLriCache
DiskLruCache用于实现存储设备缓存,即磁盘缓存。
2.2.1DiskLruCache的创建
由于它不属于AndroidSDK的一部分,所以不能通过构造方法来创建,提供了open()方法用于自身的创建
publicstaticDiskLruCacheopen(Filedirectory,intappversion,intvalueCount,longmaxSize);
典型的DiskLruCache的创建过程
privatestaticfinalDisk_CACHE_SIZE=1024*1024*50;//50M
FilediskCaCheDir=getDiskCacheDir(mContext,"bitmap");
if(!diskCacheDir.exists()){
diskCacheDir.mkdirs();
}
mDiskLruCache=DiskLruCache.open(diskCaCheDir,1,1,Disk_CACHE_SIZE);
第三个参数表示单个节点所对应的数据,一般设置为1即可。
2.2.2DiskLruCache的缓存添加缓存的添加操作是通过Editor完成的,Editor表示一个缓存对象的编辑对象。DiskLruCache不允许同时编辑一个缓存对象。
2.2.3DiskLruCache的缓存查找
缓存查找过程也需要将url转换为key,通过DiskLruCache的get()得到一个Snapshot对象,然后通过该对象即可得到缓存的文件输入流,得到文件输入流即可得到Bitmap对象了。为了避免加载过程中OOM,一般不会直接加载原始图片。在前面介绍通过BitmapFactory.Options来加载一张缩放后的图片,但是那种方法对FileInputStream的缩放存在问题,原因是FileInputStream是一种有序的文件流,而两次decodeStream调用影响了文件流的位置属性,导致了第二次decodeStream时得到的是null。为了解决这个问题,可以通过文件流得到其对应的文件描述符,然后通过BitmapFactory.decodeFileDescriptor方法来加载一张缩放过后的图片。
Bitmapbitmap=null;
Stringkey=hashKeyFormUrl(url);
DiskLruCache.SnapshotsnapShot=mDiskLruCache.get(key);
if(snapShot!=null){
FileInputStreamfileInputStream=(FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
//获取文件描述符
FileDescriptorfileDescriptor=fileInputStream.getFD();
//通过BitmapFactory.decodeFileDescriptor来加载一张缩放后的图片
bitmap=mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,
reqWidth,reqHeight);
if(bitmap!=null){
addBitmapToMemoryCache(key,bitmap);
}
}
returnbitmap;
}
三.ImageLoader的实现
具备的功能,即图片的同步加载,异步加载,图片的压缩,内存缓存,磁盘缓存以及网络拉取。
3.1图片压缩功能
如前面所述。
3.2内存缓存和磁盘缓存的实现
选择LruCache和DiskLruCache来分别完成内存缓存和磁盘缓存的工作
3.3同步加载和异步加载的接口设计
关于同步加载:从loadBitmap的实现可以看出,其工作过程遵循如下几个步骤:先试着从内存缓存中读取图片,接着从磁盘缓存中读取图片,最后试着从网络拉取图片。另外该方法不能在主线程中调用,否则就会抛出异常。因为加载图片是一个耗时的操作。
关于异步加载:从bindBitmap中可以看出,binfBitmap会先试着从内存缓存中读取结果,如果成功就直接返回,否则会从线程池中去调用loadBitmap(),当加载成功后,再讲图片,图片地址以及需要绑定的ImageView封装成一个loaderResult对象,通过mMainHandler向主线程发送一个消息,这样就可以在主线程中给ImageView设置图片了。图片的异步加载是一个很有用的功能,很多时候调用者不想在单独的线程中以同步的方式来加载图片,并将图片设置给需要的ImageVIew,从而ImageLoader内部需要自己需要在内部线程中加载图片,并且将图片设置给所需要的ImageView。
ImageLoader源码可以点击这里:下载 查看ImageLoader的实现
四.ImageLoader的使用
核心是ImageAdapter,其中的getView()的核心方法如下:
@Override
publicViewgetView(intposition,ViewconvertView,ViewGroupparent){
ViewHolderholder=null;
if(convertView==null){
convertView=mInflater.inflate(R.layout.image_list_item,parent,false);
holder=newViewHolder();
holder.imageView=(ImageView)convertView.findViewById(R.id.image);
convertView.setTag(holder);
}else{
holder=(ViewHolder)convertView.getTag();
}
ImageViewimageView=holder.imageView;
finalStringtag=(String)imageView.getTag();
finalStringuri=getItem(position);
if(!uri.equals(tag)){
imageView.setImageDrawable(mDefaultBitmapDrawable);
}
if(mIsGridViewIdle&&mCanGetBitmapFromNetWork){
imageView.setTag(uri);
//这句话将图片的复杂加载过程交给ImageLoader了
mImageLoader.bindBitmap(uri,imageView,mImageWidth,mImageWidth);
}
returnconvertView;
}
对于上述代码ImageAdapter来说,ImageLoader的加载图片的复杂过程,更不需要知道。
优化列表卡顿现象:
- 不要在getView()中做加载图片的操作,那样肯定会耗时,像这个例子中一样,交给ImageLoaer来实现。
- 控制异步加载频率,如果用户刻意的频繁的上下滑动,可能在一瞬间加载几百个异步任务,这样会给线程池造成拥堵。解决的办法是考虑在用户滑动列表时,停止加载图片。等到列表停下来时,在进行异步加载任务。
- 开启硬件加速:给Activity添加配置android:hardwareAccelerated=”true”
总结
以上就是这篇文章的全部内容了,希望本文的内容对给我Android开发者们能带来一定的帮助,如果有疑问大家可以留言交流。