BitmapFun使用与深入学习
2016-05-08 20:16
567 查看
转载请注明出处:/article/7733233.html
根据Android开发文档的说明:每个应用程序会获得至少16MB的独立内存空间;因此要确保自己的图片加载程序在16MB的情况下依然能运转起来。
在ListView、Gridview、RecyclerView中图片加载存在的问题有
图片显示错位
无法确保所有的异步任务能够按顺序执行
当用户快速滑动时,ImageView已经被回收,而绑定的线程还在运行,浪费CPU,浪费内存。
解决方案(思路)
ImageView和Task无法对应时则取消任务
ImageView和Task绑定准确的加载对应图片
ImageView和Task相互持有对方的引用
获取当前应用要求的图片显示的高度和宽度
全屏模式下显示的最大宽度、长度(如点击看大图的场景):
final DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
final int height = displayMetrics.heightPixels; //当前手机屏幕高度
final int width = displayMetrics.widthPixels; //当前手机屏幕宽度
final int longest = (height > width ? height : width) / 2; //在此我们限定图片最长宽度的二分之一,这里会降低图片分辨率,但是大大节省了加载内存
listView中显示的最大宽度、长度(查看缩略图场景):
下面的代码是在GridView场景下的代码
mImageThumbSize = 100dp;
mImageThumbSpacing = 1dp;
final int numColumns = (int) Math.floor( mGridView.getWidth() / (mImageThumbSize + mImageThumbSpacing));
final int columnWidth = (mGridView.getWidth() / numColumns) - mImageThumbSpacing;
其实我们可以直接设置缩略图为mImageThumbSize、但是为了更精确所以求取columnWidth作为预期值
获取待加载图片的维度和类型信息
BitmapFactory有decodeByteArray(), decodeFile(), decodeResource()三种方式将图片资源加载成Bitmap类型数据。默认情况下这些方法都会尝试为构造得到的Bitmap分配内存,因此容易造成OOM。三种方式都有一个额外的签名——BitmapFactory.Options,通过它可以设置解码的一些选项。设置inJustDecodeBounds属性为真,三个方法将不会分配内存,但是会设置对应BitmapFactory.Options的outWidth,
outHeight 和outMimeType属性,通过这样的方法获取即将加载的图片的大小和类型。
检测待加载图片的维度和类型信息的代码:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
载入低维度图片进入内存
BitmapFactory.Options的inSampleSize属性,它的值用于产生一个比当前图片更小的图。inSampleSize等于4,则通过BitmapFactory获得的Bitmap为原始图片的四分之一宽度高度,inSampleSize小于1,等价于inSampleSize等于1.
下面是相关代码:
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeResource(res, resId, options); //获得预期的缩小版Bitmap
//下面的方法第二个是预期宽度,第三个是预期高度
public static int calculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final 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;
}
设置内存缓存大小——根据当前系统资源
针对不同Android版本的优化
2.3.3(level 10)以及更低版本Bitmap的原生像素是直接存储在原生内存中,而Bitmap是存在Dalvik堆中的。原生内存的垃圾回收不是高效的,容易造成OOM。因此一旦确定一个bitmap不会再被使用就调用Bitmap的recycle方法。
3.0(level 11)以及更高版本Bitmap的原生像素和关联Bitmap是存在Dalvik堆中的。BitmapFactory.Options.inBitmap属性标志构造bitmap的时候尝试重复使用已经加载好的bitmap内容。
一、创建一个ImageFetcher对象
ImageCache.ImageCacheParams cacheParams = new ImageCache.ImageCacheParams(getActivity(), IMAGE_CACHE_DIR);
cacheParams.setMemCacheSizePercent(0.25f); // Set memory cache to 25% of app memory
二、使用ImageFetcher对象加载图片
mImageFetcher.loadImage(mImageUrl, mImageView);
第一个参数是网络地址(String),第二个参数是网络加载图片要显示的ImageView控件
三、ImageFetcher一些高级使用
分析目的:
创建ImageFetcher对象
ImageFetcher.loadImage方法内部原理
了解ImageFetcher的缓存、网络访问、破解图片显示混乱、取消无效任务的实现
其它高级功能的底层实现(需要注意的地方)
调用mImageFetcher.flushCache()、mImageFetcher.closeCache() 、mImageFetcher.clearCache()其效果依次是调用ImageWorker的mImageCache.flush()、{mImageCache.close(); mImageCache = null;}、mImageCache.clearCache()和ImageFetcher的flushCacheInternal()、closeCacheInternal()、clearCacheInternal()方法;
调用mImageFetcher.setImageSize(height)其效果是调用ImageResizer的同名方法
调用mImageFetcher.setPauseWork(true)其效果是调用ImageWork的同名方法
调用mImageFetcher.setExitTasksEarly(false)其效果是调用ImageWork的同名方法
ImageFetcher主要任务是完成从网络上下载图片资源。
ImageFetcher().ImageFetcher.class
都是调用父类的同名构造器进行,同时执行init方法。
init()@ImageFetcher.class
init方法很简单就是首先检查当前网络是否可能,不可用弹出一条toast;
给ImageFetcher对象的mHttpCacheDir域进行初始化,即创建一个用于缓存网络图片的文件夹;
setLoadingImage()@ImageFetcher.class
ImageFetcher和其父类ImageSizer都没有重写该方法,该方法的定义只会出现在ImageWorker类中。
addImageCache()@ImageFetcher.class
ImageFetcher和其父类ImageSizer都没有重写该方法,该方法的定义只会出现在ImageWorker类中。虽然ImageFetcher中没有重写addImageCache方法,但是该方法最终会调用initDiskLruCache方法,而ImageFetcher重写了该方法!下面我们看看该方法完成了哪些工作
initDiskCacheInternal()@ImageFetcher.class
1、没有目录则创建目录
2、如果当前目录下之前有缓存文件,则利用日志文件恢复缓存信息;如果没有缓存文件则创建日志文件
3、唤醒正在等待的线程
上面的代码都是介绍创建ImageFetcher对象所涉及的源代码,接下来我们介绍下如何利用ImageFetcher获取指定url的图片资源。
loadImage()@ImageFetcher.class
ImageFetcher和其父类ImageSizer都没有重写该方法,该方法的定义只会出现在ImageWorker类中。该方法会先从ImageWork的内存缓存中尝试获取数据,如果获取不到则最终会调用下面的processBitmap方法,从磁盘中或者网络中获取数据。
processBitmap@mageResizer.class
1、能进入到这里,表明缓存区还没初始化结束,进入休眠状态;当缓存区初始化结束会收到一个唤醒信号。
2、利用key从缓存区中获取到对应的DiskLruCache.Snapshot对象,该对象会有一个对缓存文件的InputStream引用,可以利用它读取文件数据
3、如果获取不到snapshot则证明当前磁盘中没有对应的缓存,因此需要从网上下载;先获取到一个DiskLruCache.Editor对象(会先创建一个相应文件),该对象是一个向key对应的文件进行写入数据的工具。
4、从Snapshot中获取到对应InputStream,并利用InputStream创建一个FileDescriptor对象
5、利用ImageResizer中定义的ecodeSampledBitmapFromDescriptor(fileDescrip方法获取一个Bitmap对象;这里的 mImageWidth, mImageHeight来自于ImageResizer类
注意:这里我们对于DiskLruCache的介绍并不详细,只是稍微过了一遍,更加深入的分析可以参考另一篇博客《OkHttp深入学习(三)——Cache》里面的DiskLruCache跟这里的DiskLruCache如出一辙,甚至怀疑它们有相互抄袭的可能,有兴趣的读者可以跳过去看看。
downloadUrlToStream()@mageResizer.class
利用url创建一个HttpUrlConnection连接,然后获取输入输出流,逻辑很简单这里不细讲了。
ImageResizer类的主要作用就是对图像的处理,如在限制大小的情况下利用给定的文件描述符得到一个Bitmap对象。
public class ImageFetcher extends ImageResizer
ImageResizer()@ImagerResizer.class
该构造器首先调用父类的同名构造方法,随后调用setImageSize方法
SetImageSize()@mageResizer.class
该方法只是对ImagerFetcher的protected int mImageWidth; protected int mImageHeight;两个域进行初始化
decodeSampledBitmapFromDescriptor()@mageResizer.class
1、使用inJustDecodeBounds=true检查维度
2、计算缩小的倍数,2的幂
3、Android 3.0(level-11)及以上版本则会执行这行代码;一种针对3.0更高版本的优化
4、返回Bitmap对象
ImageWork是整个BitmapFun最核心的一个内,前面的ImageFetcher和ImageResizer都是继承自该类。图片加载的除了网络部分和产生Bitmap对象由子类实现,其它内容都是由ImageWork实现。
public class ImageResizer extends ImageWorker
ImageWorker()@ImageWorker.class
从context中获取到对应的getResources对象;
setLoadingImage()@ImageWorker.class
只是简单的对ImageWorker的mLoadingBitmap域进设置;该域用于指定在异步加载时ImageView显示的图片。
addImageCache()@ImageWorker.class
1、在非UI线程中调用initDiskCacheInternal()方法
2、这里使用Fragment的好处在于,当手机屏幕旋转的时候Fragment可以重复被使用,很多资源不需要重新加载
initDiskCacheInternal()@ImageWorker.class
该方法会被子类ImageFetcher重写,但是最后也还是会调用到这里。
loadImage()@ImageWorker.class
1、从缓存中(内存)尝试获取文件,获取到则直接将结果显示出来
2、调用cancelPotentialWork方法取消与当前ImageView绑定的异步任务
3、利用网址(String)和ImageView控件创建一个BitmapWorkTask对象,随后又利用该对象创建一个AsynDrawable对象
4、将该对象设置给imageView空间进行显示
5、开启BitmapWorkTask对象的任务;等价于将task的DoInBackground方法交给一个 Executors.newFixedThreadPool(2, sThreadFactory);线程池去执行。而这个线程池最多两个线程同时工作,而且更要命的是磁盘缓存文件和网络请求文件两个功能都是在这个线程池中工作,因此一旦两条线程在访问网络的过程中阻塞,那么我们也无法获取到本地的缓存文件。所以这部分如果要优化可以将缓存部分单独通过一条线程去访问。Volley也就是这么干的!
cancelPotentialWork()@ImageWorker.class
1、获取到与当前imageView绑定的异步任务
2、如果获取到的异步任务对应的url地址跟当前url地址不同则取消之前的异步任务,否则返回false;
3、该方法返回true表明针对当前的imageView需要创建一个新的BitmapWorkTask对象。
getBitmapWorkerTask()@ImageWorker.class
看到这里突然想到了”ImageView和Task相互持有对方的引用“这句话。通过ImageView的setImageDrawable方法向其传递一个自定义的Drawable对象——AsyncDrawable,而AsyncDrawable中有一个异步任务——BitmapWorkerTask;BitmapWorkerTask的构建又是由url和ImageView所创建的;这样ImageView和BitmapWorkerTask之间实现了相互引用。
setExitTasksEarly()@ImageWorker.class
设置mExitTasksEarly提前退出任务标志位和mPauseWork暂停工作标志位,这些标志位会在BitmapWorkTask的DoInBackground方法中被不断检测
setPauseWork()@ImageWorker.class
设置mPauseWork暂停工作标志位、该标志位会在BitmapWorkTask的DoInBackground方法中被不断检测
BitmapWorkerTask实现了AsynTask方法,用于异步网上加载图片。
private class BitmapWorkerTask extends AsyncTask<Void, Void, BitmapDrawable>
BitmapWorkerTask()@BitmapWorkerTask.class
doInBackground()@BitmapWorkerTask.class
1、对定义在ImageWorker的Object mPauseWorkLock = new Object()对象、定义在ImageWorker的boolean mPauseWork = false对象进行判断。如果当前的BitmapWorkTask任务被暂停同时没有被取消则等待被唤醒。
2、从缓存中(磁盘中)获取目标图片
3、缓存中没有获取到数据,则通过processBitmap方法从网络上获取数据,ImageWorker的porcessBitmap方法如下:protected abstract
Bitmap processBitmap(Object data);即该方法是一个抽象方法。它的实现在其子类中。
4、根据当前系统版本的不同创建对应的Drawable类型对象,
5、将网上获取到的Bitmap存入缓存中
onPostExecute()@BitmapWorkerTask.class
1、设置一个真正的Drawable对象给Imageview,之前通过etImageDrawable方法传入的AsyncDrawable并不能显示什么数据,不过可以显示一个正在加载的图片。setImageDrawable(imageView, value)可以近似看成imageView.setImageDrawable(value);
onCancelled()@BitmapWorkTask.class
1、唤醒正在等待mPauseWorkLock锁的方法,但是这个时候调用isCancelled方法会返回true!
private DiskLruCache mDiskLruCache; //磁盘缓存图片,这里的缓存是保证之前在视图中显示过;而ImageFetcher的DiskLruCache中文缓存可能还没有被显示过。
getDiskCacheDir()@getDiskCacheDir.class
BitmapFun到此就介绍完毕了,内容主要涉及到ImageWork、ImageResizer、ImageFetcher、ImageCache、ImageWorkTask这几个类。ImageWork是核心,其loadImage方法是加载图片的核心,该方法接收一个String
url网路地址和一个ImageView控件。该方法会首先从ImageCache的LruCache<String, BitmapDrawable>中获取一个BitmapDrawable对象,如果不行则将url和ImageView包装成一个BitmapWorkTask类进行异步网络访问,该类的DoInBackground方法内部又会从磁盘中即ImageCache的DiskLruCache中获取Bitmap对象,如果获取不到则通过ImageFetcher的processBitmap方法获取,该方法内部又会从ImageFetcher的DiskLruCache中获取一个文件读入流,如果没有这样一个对应的文件读入流则通过ImageFetcher的 downloadUrlToStream方法从网上获取图片,最后调用ImageResizer的decodeSampledBitmapFromDescriptor()方法根据之前设置的图片宽度和高度大小将得到的二进制文件转换成一个合适的BitmapDrawable对象,最后imageWork会将结果显示到ImageView中。
BitmapFun几个难点就是ImageView和ImageWorkTask相互引用,避免图片显示混乱的情况。BitmapWorkTask的doInBackgroud方法中会一直检测mPauseWork和mExitTasksEarly
两个标志位,使得我们可以暂停或者取消当前正在执行的异步的网络请求任务。Android加载Bitmap资源避免OOM异常。 最后对于缓存内容本节并没有深入的介绍,DiskLruCache可以参考博客《OkHttp深入学习(三)——Cache》。文中源码下载地址
reference:https://developer.android.com/intl/zh-cn/training/displaying-bitmaps/index.html
背景介绍
根据Android开发文档的说明:每个应用程序会获得至少16MB的独立内存空间;因此要确保自己的图片加载程序在16MB的情况下依然能运转起来。在ListView、Gridview、RecyclerView中图片加载存在的问题有
图片显示错位
无法确保所有的异步任务能够按顺序执行
当用户快速滑动时,ImageView已经被回收,而绑定的线程还在运行,浪费CPU,浪费内存。
解决方案(思路)
ImageView和Task无法对应时则取消任务
ImageView和Task绑定准确的加载对应图片
ImageView和Task相互持有对方的引用
Android加载图片常用优化手段(该部分主要体现在ImageResizer部分)
获取当前应用要求的图片显示的高度和宽度全屏模式下显示的最大宽度、长度(如点击看大图的场景):
final DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
final int height = displayMetrics.heightPixels; //当前手机屏幕高度
final int width = displayMetrics.widthPixels; //当前手机屏幕宽度
final int longest = (height > width ? height : width) / 2; //在此我们限定图片最长宽度的二分之一,这里会降低图片分辨率,但是大大节省了加载内存
listView中显示的最大宽度、长度(查看缩略图场景):
下面的代码是在GridView场景下的代码
mImageThumbSize = 100dp;
mImageThumbSpacing = 1dp;
final int numColumns = (int) Math.floor( mGridView.getWidth() / (mImageThumbSize + mImageThumbSpacing));
final int columnWidth = (mGridView.getWidth() / numColumns) - mImageThumbSpacing;
其实我们可以直接设置缩略图为mImageThumbSize、但是为了更精确所以求取columnWidth作为预期值
获取待加载图片的维度和类型信息
BitmapFactory有decodeByteArray(), decodeFile(), decodeResource()三种方式将图片资源加载成Bitmap类型数据。默认情况下这些方法都会尝试为构造得到的Bitmap分配内存,因此容易造成OOM。三种方式都有一个额外的签名——BitmapFactory.Options,通过它可以设置解码的一些选项。设置inJustDecodeBounds属性为真,三个方法将不会分配内存,但是会设置对应BitmapFactory.Options的outWidth,
outHeight 和outMimeType属性,通过这样的方法获取即将加载的图片的大小和类型。
检测待加载图片的维度和类型信息的代码:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
载入低维度图片进入内存
BitmapFactory.Options的inSampleSize属性,它的值用于产生一个比当前图片更小的图。inSampleSize等于4,则通过BitmapFactory获得的Bitmap为原始图片的四分之一宽度高度,inSampleSize小于1,等价于inSampleSize等于1.
下面是相关代码:
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeResource(res, resId, options); //获得预期的缩小版Bitmap
//下面的方法第二个是预期宽度,第三个是预期高度
public static int calculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final 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;
}
设置内存缓存大小——根据当前系统资源
LruCache<String, Bitmap> mMemoryCache; final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); //这是VM所能提供的最大内存使用数量,超过这个值将抛出OOM异常 final int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getByteCount() / 1024; //根据记载图片实际大小计算size而不是根据图片数量 } };
针对不同Android版本的优化
2.3.3(level 10)以及更低版本Bitmap的原生像素是直接存储在原生内存中,而Bitmap是存在Dalvik堆中的。原生内存的垃圾回收不是高效的,容易造成OOM。因此一旦确定一个bitmap不会再被使用就调用Bitmap的recycle方法。
3.0(level 11)以及更高版本Bitmap的原生像素和关联Bitmap是存在Dalvik堆中的。BitmapFactory.Options.inBitmap属性标志构造bitmap的时候尝试重复使用已经加载好的bitmap内容。
简单使用:
一、创建一个ImageFetcher对象private ImageFetcher mImageFetcher; mImageFetcher = new ImageFetcher(getActivity(), mImageThumbSize); mImageFetcher.setLoadingImage(R.drawable.empty_photo); mImageFetcher.addImageCache(getActivity().getSupportFragmentManager(), cacheParams);
ImageCache.ImageCacheParams cacheParams = new ImageCache.ImageCacheParams(getActivity(), IMAGE_CACHE_DIR);
cacheParams.setMemCacheSizePercent(0.25f); // Set memory cache to 25% of app memory
二、使用ImageFetcher对象加载图片
mImageFetcher.loadImage(mImageUrl, mImageView);
第一个参数是网络地址(String),第二个参数是网络加载图片要显示的ImageView控件
三、ImageFetcher一些高级使用
mImageFetcher.flushCache(); mImageFetcher.closeCache(); mImageFetcher.clearCache(); mImageFetcher.setImageSize(height); mImageFetcher.setPauseWork(true); mImageFetcher.setExitTasksEarly(false);
源码分析
分析目的:创建ImageFetcher对象
ImageFetcher.loadImage方法内部原理
了解ImageFetcher的缓存、网络访问、破解图片显示混乱、取消无效任务的实现
其它高级功能的底层实现(需要注意的地方)
调用mImageFetcher.flushCache()、mImageFetcher.closeCache() 、mImageFetcher.clearCache()其效果依次是调用ImageWorker的mImageCache.flush()、{mImageCache.close(); mImageCache = null;}、mImageCache.clearCache()和ImageFetcher的flushCacheInternal()、closeCacheInternal()、clearCacheInternal()方法;
调用mImageFetcher.setImageSize(height)其效果是调用ImageResizer的同名方法
调用mImageFetcher.setPauseWork(true)其效果是调用ImageWork的同名方法
调用mImageFetcher.setExitTasksEarly(false)其效果是调用ImageWork的同名方法
ImageFetcher.class
ImageFetcher主要任务是完成从网络上下载图片资源。private File mHttpCacheDir; //网络文件的缓存目录 private final Object mHttpDiskCacheLock = new Object(); //对象锁 private boolean mHttpDiskCacheStarting = true; //缓存初始化标志 private DiskLruCache mHttpDiskCache; //磁盘缓存对象 private static final int HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10MB //缓存文件最大容量
ImageFetcher().ImageFetcher.class
public ImageFetcher(Context context, int imageWidth, int imageHeight) { super(context, imageWidth, imageHeight); init(context); } public ImageFetcher(Context context, int imageSize) { super(context, imageSize); init(context); }
都是调用父类的同名构造器进行,同时执行init方法。
init()@ImageFetcher.class
private void init(Context context) { checkConnection(context); mHttpCacheDir = ImageCache.getDiskCacheDir(context, HTTP_CACHE_DIR); } private void checkConnection(Context context) { final ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); final NetworkInfo networkInfo = cm.getActiveNetworkInfo(); if (networkInfo == null || !networkInfo.isConnectedOrConnecting()) { Toast.makeText(context, R.string.no_network_connection_toast, Toast.LENGTH_LONG).show(); } }
init方法很简单就是首先检查当前网络是否可能,不可用弹出一条toast;
给ImageFetcher对象的mHttpCacheDir域进行初始化,即创建一个用于缓存网络图片的文件夹;
setLoadingImage()@ImageFetcher.class
ImageFetcher和其父类ImageSizer都没有重写该方法,该方法的定义只会出现在ImageWorker类中。
addImageCache()@ImageFetcher.class
ImageFetcher和其父类ImageSizer都没有重写该方法,该方法的定义只会出现在ImageWorker类中。虽然ImageFetcher中没有重写addImageCache方法,但是该方法最终会调用initDiskLruCache方法,而ImageFetcher重写了该方法!下面我们看看该方法完成了哪些工作
initDiskCacheInternal()@ImageFetcher.class
@Override protected void initDiskCacheInternal() { super.initDiskCacheInternal(); initHttpDiskCache(); } private void initHttpDiskCache() { if (!mHttpCacheDir.exists()) { mHttpCacheDir.mkdirs(); } //note1 synchronized (mHttpDiskCacheLock) { if (ImageCache.getUsableSpace(mHttpCacheDir) > HTTP_CACHE_SIZE) { //HTTP_CACHE_SIZE 等于 10MB mHttpDiskCache = DiskLruCache.open(mHttpCacheDir, 1, 1, HTTP_CACHE_SIZE); //note2 } mHttpDiskCacheStarting = false; mHttpDiskCacheLock.notifyAll(); //note3 } }
1、没有目录则创建目录
2、如果当前目录下之前有缓存文件,则利用日志文件恢复缓存信息;如果没有缓存文件则创建日志文件
3、唤醒正在等待的线程
上面的代码都是介绍创建ImageFetcher对象所涉及的源代码,接下来我们介绍下如何利用ImageFetcher获取指定url的图片资源。
loadImage()@ImageFetcher.class
ImageFetcher和其父类ImageSizer都没有重写该方法,该方法的定义只会出现在ImageWorker类中。该方法会先从ImageWork的内存缓存中尝试获取数据,如果获取不到则最终会调用下面的processBitmap方法,从磁盘中或者网络中获取数据。
processBitmap@mageResizer.class
@Override protected Bitmap processBitmap(Object data) { return processBitmap(String.valueOf(data)); } private Bitmap processBitmap(String data) { final String key = ImageCache.hashKeyForDisk(data); FileDescriptor fileDescriptor = null; FileInputStream fileInputStream = null; DiskLruCache.Snapshot snapshot; synchronized (mHttpDiskCacheLock) { while (mHttpDiskCacheStarting) { try { mHttpDiskCacheLock.wait();} catch (InterruptedException e) {} //note1 } if (mHttpDiskCache != null) { try { snapshot = mHttpDiskCache.get(key); if (snapshot == null) {//note2 DiskLruCache.Editor editor = mHttpDiskCache.edit(key); if (editor != null) {//note3 if (downloadUrlToStream(data,editor.newOutputStream(DISK_CACHE_INDEX))) { editor.commit(); } else {editor.abort();} } snapshot = mHttpDiskCache.get(key); } if (snapshot != null) { //note4 fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX); fileDescriptor = fileInputStream.getFD(); } } catch (IOException e) {.... } finally { if (fileDescriptor == null && fileInputStream != null) { try { fileInputStream.close();} catch (IOException e) {} } }//end of finally }//end of if }//end of synchronized Bitmap bitmap = null; if (fileDescriptor != null) { bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth, mImageHeight, getImageCache());//note5 } if (fileInputStream != null) { try { fileInputStream.close(); } catch (IOException e) {} } return bitmap; }
1、能进入到这里,表明缓存区还没初始化结束,进入休眠状态;当缓存区初始化结束会收到一个唤醒信号。
2、利用key从缓存区中获取到对应的DiskLruCache.Snapshot对象,该对象会有一个对缓存文件的InputStream引用,可以利用它读取文件数据
3、如果获取不到snapshot则证明当前磁盘中没有对应的缓存,因此需要从网上下载;先获取到一个DiskLruCache.Editor对象(会先创建一个相应文件),该对象是一个向key对应的文件进行写入数据的工具。
4、从Snapshot中获取到对应InputStream,并利用InputStream创建一个FileDescriptor对象
5、利用ImageResizer中定义的ecodeSampledBitmapFromDescriptor(fileDescrip方法获取一个Bitmap对象;这里的 mImageWidth, mImageHeight来自于ImageResizer类
注意:这里我们对于DiskLruCache的介绍并不详细,只是稍微过了一遍,更加深入的分析可以参考另一篇博客《OkHttp深入学习(三)——Cache》里面的DiskLruCache跟这里的DiskLruCache如出一辙,甚至怀疑它们有相互抄袭的可能,有兴趣的读者可以跳过去看看。
downloadUrlToStream()@mageResizer.class
public boolean downloadUrlToStream(String urlString, OutputStream outputStream) { disableConnectionReuseIfNecessary(); HttpURLConnection urlConnection = null; BufferedOutputStream out = null; BufferedInputStream in = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection) url.openConnection(); in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE); out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE); int b; while ((b = in.read()) != -1) { out.write(b); } return true; } .... return false; }
利用url创建一个HttpUrlConnection连接,然后获取输入输出流,逻辑很简单这里不细讲了。
ImageResizer.class
ImageResizer类的主要作用就是对图像的处理,如在限制大小的情况下利用给定的文件描述符得到一个Bitmap对象。public class ImageFetcher extends ImageResizer
protected int mImageWidth; protected int mImageHeight;
ImageResizer()@ImagerResizer.class
public ImageResizer(Context context, int imageWidth, int imageHeight) { super(context); setImageSize(imageWidth, imageHeight); } public ImageResizer(Context context, int imageSize) { super(context); setImageSize(imageSize); }
该构造器首先调用父类的同名构造方法,随后调用setImageSize方法
SetImageSize()@mageResizer.class
public void setImageSize(int width, int height) { mImageWidth = width; mImageHeight = height; } public void setImageSize(int size) { setImageSize(size, size); }
该方法只是对ImagerFetcher的protected int mImageWidth; protected int mImageHeight;两个域进行初始化
decodeSampledBitmapFromDescriptor()@mageResizer.class
public static Bitmap decodeSampledBitmapFromDescriptor( FileDescriptor fileDescriptor, int reqWidth, int reqHeight, ImageCache cache) { final BitmapFactory.Options options = new BitmapFactory.Options(); // note1 options.inJustDecodeBounds = true; BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); //note2 options.inJustDecodeBounds = false; if (Utils.hasHoneycomb()) {addInBitmapOptions(options, cache);} //note3 return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); }
1、使用inJustDecodeBounds=true检查维度
2、计算缩小的倍数,2的幂
3、Android 3.0(level-11)及以上版本则会执行这行代码;一种针对3.0更高版本的优化
4、返回Bitmap对象
ImageWorker.class
ImageWork是整个BitmapFun最核心的一个内,前面的ImageFetcher和ImageResizer都是继承自该类。图片加载的除了网络部分和产生Bitmap对象由子类实现,其它内容都是由ImageWork实现。public class ImageResizer extends ImageWorker
protected Resources mResources; private ImageCache mImageCache; private Bitmap mLoadingBitmap; private ImageCache.ImageCacheParams mImageCacheParams; private final Object mPauseWorkLock = new Object(); protected boolean mPauseWork = false; //暂停标志位 private boolean mExitTasksEarly = false; //提前退出标志位
ImageWorker()@ImageWorker.class
protected ImageWorker(Context context) { mResources = context.getResources(); }
从context中获取到对应的getResources对象;
setLoadingImage()@ImageWorker.class
public void setLoadingImage(Bitmap bitmap) { mLoadingBitmap = bitmap; }
只是简单的对ImageWorker的mLoadingBitmap域进设置;该域用于指定在异步加载时ImageView显示的图片。
addImageCache()@ImageWorker.class
public void addImageCache(FragmentManager fragmentManager, ImageCache.ImageCacheParams cacheParams) { mImageCacheParams = cacheParams; mImageCache = ImageCache.getInstance(fragmentManager, mImageCacheParams); new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE); //note1 }
1、在非UI线程中调用initDiskCacheInternal()方法
2、这里使用Fragment的好处在于,当手机屏幕旋转的时候Fragment可以重复被使用,很多资源不需要重新加载
initDiskCacheInternal()@ImageWorker.class
protected void initDiskCacheInternal() { if (mImageCache != null) { mImageCache.initDiskCache(); } }
该方法会被子类ImageFetcher重写,但是最后也还是会调用到这里。
loadImage()@ImageWorker.class
public void loadImage(Object data, ImageView imageView) { if (data == null) { return; } BitmapDrawable value = null; if (mImageCache != null) { value = mImageCache.getBitmapFromMemCache(String.valueOf(data)); //note1 } if (value != null) { imageView.setImageDrawable(value); } else if (cancelPotentialWork(data, imageView)) { //note2 final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView); final AsyncDrawable asyncDrawable = new AsyncDrawable(mResources, mLoadingBitmap, task); //note3 imageView.setImageDrawable(asyncDrawable); //note4 task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR); //note5 } }
1、从缓存中(内存)尝试获取文件,获取到则直接将结果显示出来
2、调用cancelPotentialWork方法取消与当前ImageView绑定的异步任务
3、利用网址(String)和ImageView控件创建一个BitmapWorkTask对象,随后又利用该对象创建一个AsynDrawable对象
4、将该对象设置给imageView空间进行显示
5、开启BitmapWorkTask对象的任务;等价于将task的DoInBackground方法交给一个 Executors.newFixedThreadPool(2, sThreadFactory);线程池去执行。而这个线程池最多两个线程同时工作,而且更要命的是磁盘缓存文件和网络请求文件两个功能都是在这个线程池中工作,因此一旦两条线程在访问网络的过程中阻塞,那么我们也无法获取到本地的缓存文件。所以这部分如果要优化可以将缓存部分单独通过一条线程去访问。Volley也就是这么干的!
cancelPotentialWork()@ImageWorker.class
public static boolean cancelPotentialWork(Object data, ImageView imageView) { final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); //note1 if (bitmapWorkerTask != null) { final Object bitmapData = bitmapWorkerTask.mData; //note2 if (bitmapData == null || !bitmapData.equals(data)) { bitmapWorkerTask.cancel(true); } else { return false; } } return true; //note3 }
1、获取到与当前imageView绑定的异步任务
2、如果获取到的异步任务对应的url地址跟当前url地址不同则取消之前的异步任务,否则返回false;
3、该方法返回true表明针对当前的imageView需要创建一个新的BitmapWorkTask对象。
getBitmapWorkerTask()@ImageWorker.class
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { if (imageView != null) { final Drawable drawable = imageView.getDrawable(); if (drawable instanceof AsyncDrawable) { final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; return asyncDrawable.getBitmapWorkerTask(); } } return null; }
看到这里突然想到了”ImageView和Task相互持有对方的引用“这句话。通过ImageView的setImageDrawable方法向其传递一个自定义的Drawable对象——AsyncDrawable,而AsyncDrawable中有一个异步任务——BitmapWorkerTask;BitmapWorkerTask的构建又是由url和ImageView所创建的;这样ImageView和BitmapWorkerTask之间实现了相互引用。
setExitTasksEarly()@ImageWorker.class
public void setExitTasksEarly(boolean exitTasksEarly) { mExitTasksEarly = exitTasksEarly; setPauseWork(false); }
设置mExitTasksEarly提前退出任务标志位和mPauseWork暂停工作标志位,这些标志位会在BitmapWorkTask的DoInBackground方法中被不断检测
setPauseWork()@ImageWorker.class
public void setPauseWork(boolean pauseWork) { synchronized (mPauseWorkLock) { mPauseWork = pauseWork; if (!mPauseWork) { mPauseWorkLock.notifyAll(); } } }
设置mPauseWork暂停工作标志位、该标志位会在BitmapWorkTask的DoInBackground方法中被不断检测
BitmapWorkerTask.class@ImageWorker.class
BitmapWorkerTask实现了AsynTask方法,用于异步网上加载图片。private class BitmapWorkerTask extends AsyncTask<Void, Void, BitmapDrawable>
private Object mData; private final WeakReference<ImageView> imageViewReference; //所引用,不影响垃圾回收
BitmapWorkerTask()@BitmapWorkerTask.class
public BitmapWorkerTask(Object data, ImageView imageView) { mData = data; imageViewReference = new WeakReference<ImageView>(imageView); }
doInBackground()@BitmapWorkerTask.class
@Override protected BitmapDrawable doInBackground(Void... params) { final String dataString = String.valueOf(mData); Bitmap bitmap = null; BitmapDrawable drawable = null; synchronized (mPauseWorkLock) { while (mPauseWork && !isCancelled()) { //note1 try { mPauseWorkLock.wait(); } catch (InterruptedException e) {} } } if (mImageCache != null && !isCancelled() && getAttachedImageView() != null && !mExitTasksEarly) { bitmap = mImageCache.getBitmapFromDiskCache(dataString); //note2 } if (bitmap == null && !isCancelled() && getAttachedImageView() != null && !mExitTasksEarly) { bitmap = processBitmap(mData); //note3 } if (bitmap != null) { if (Utils.hasHoneycomb()) { drawable = new BitmapDrawable(mResources, bitmap); }//note4 else { drawable = new RecyclingBitmapDrawable(mResources, bitmap); } if (mImageCache != null) { mImageCache.addBitmapToCache(dataString, drawable); } //note5 } return drawable; }
1、对定义在ImageWorker的Object mPauseWorkLock = new Object()对象、定义在ImageWorker的boolean mPauseWork = false对象进行判断。如果当前的BitmapWorkTask任务被暂停同时没有被取消则等待被唤醒。
2、从缓存中(磁盘中)获取目标图片
3、缓存中没有获取到数据,则通过processBitmap方法从网络上获取数据,ImageWorker的porcessBitmap方法如下:protected abstract
Bitmap processBitmap(Object data);即该方法是一个抽象方法。它的实现在其子类中。
4、根据当前系统版本的不同创建对应的Drawable类型对象,
5、将网上获取到的Bitmap存入缓存中
onPostExecute()@BitmapWorkerTask.class
@Override protected void onPostExecute(BitmapDrawable value) { if (isCancelled() || mExitTasksEarly) { value = null; } final ImageView imageView = getAttachedImageView(); if (value != null && imageView != null) { setImageDrawable(imageView, value); //note1 } }
1、设置一个真正的Drawable对象给Imageview,之前通过etImageDrawable方法传入的AsyncDrawable并不能显示什么数据,不过可以显示一个正在加载的图片。setImageDrawable(imageView, value)可以近似看成imageView.setImageDrawable(value);
onCancelled()@BitmapWorkTask.class
@Override protected void onCancelled(BitmapDrawable value) { super.onCancelled(value); synchronized (mPauseWorkLock) { mPauseWorkLock.notifyAll(); //note1 } }
1、唤醒正在等待mPauseWorkLock锁的方法,但是这个时候调用isCancelled方法会返回true!
ImageCache.class
private DiskLruCache mDiskLruCache; //磁盘缓存图片,这里的缓存是保证之前在视图中显示过;而ImageFetcher的DiskLruCache中文缓存可能还没有被显示过。private LruCache<String, BitmapDrawable> mMemoryCache;//内存缓存图片
getDiskCacheDir()@getDiskCacheDir.class
public static File getDiskCacheDir(Context context, String uniqueName) { // Check if media is mounted or storage is built-in, if so, try and use external cache dir // otherwise use internal cache dir final String cachePath = ...获取一个绝对路径 return new File(cachePath + File.separator + uniqueName); }
BitmapFun到此就介绍完毕了,内容主要涉及到ImageWork、ImageResizer、ImageFetcher、ImageCache、ImageWorkTask这几个类。ImageWork是核心,其loadImage方法是加载图片的核心,该方法接收一个String
url网路地址和一个ImageView控件。该方法会首先从ImageCache的LruCache<String, BitmapDrawable>中获取一个BitmapDrawable对象,如果不行则将url和ImageView包装成一个BitmapWorkTask类进行异步网络访问,该类的DoInBackground方法内部又会从磁盘中即ImageCache的DiskLruCache中获取Bitmap对象,如果获取不到则通过ImageFetcher的processBitmap方法获取,该方法内部又会从ImageFetcher的DiskLruCache中获取一个文件读入流,如果没有这样一个对应的文件读入流则通过ImageFetcher的 downloadUrlToStream方法从网上获取图片,最后调用ImageResizer的decodeSampledBitmapFromDescriptor()方法根据之前设置的图片宽度和高度大小将得到的二进制文件转换成一个合适的BitmapDrawable对象,最后imageWork会将结果显示到ImageView中。
BitmapFun几个难点就是ImageView和ImageWorkTask相互引用,避免图片显示混乱的情况。BitmapWorkTask的doInBackgroud方法中会一直检测mPauseWork和mExitTasksEarly
两个标志位,使得我们可以暂停或者取消当前正在执行的异步的网络请求任务。Android加载Bitmap资源避免OOM异常。 最后对于缓存内容本节并没有深入的介绍,DiskLruCache可以参考博客《OkHttp深入学习(三)——Cache》。文中源码下载地址
reference:https://developer.android.com/intl/zh-cn/training/displaying-bitmaps/index.html
相关文章推荐
- 中英文切换导航制作
- 地铁译:Spark for python developers --- 搭建Spark虚拟环境1
- [Modern OpenGL系列(三)]用OpenGL绘制一个三角形
- 如何下载北大图书馆多媒体资源服务平台讲座
- java提示框
- thinkphp 配置多数据库
- C/S和B/S两种架构区别
- kruskal算法+并查集 输出每一条路径
- Error: Could not find or load main class org.apache.hadoop.hdfs.server.namenode.NameNode
- 20145315 《Java程序设计》第十周学习总结
- 集合框架--keySet
- poj 3278 bfs
- visual studio编译器用cmd编译运行.c文件
- 安全模式下的加解密
- 20145315 《Java程序设计》实验五实验报告
- 第十周学习进度条
- Android应用开发SharedPreferences存储数据的使用方法
- 后缀数组---Milk Patterns
- 数据库
- jsp笔记