作为一名Android开发人员,相信大家对图片OOM的问题已经耳熟能详了,关于图片缓存和解决OOM的开源项目也是相当的多,被大家熟知的就是Universal_image_loader和Volley了,Volley在前面的文章中已经有介绍。Universal_image_loader在图片缓存功能方面应该算功能最强的,但是感觉很多功能用不上,所以在项目中我一般不太喜欢使用Universal_image_loader(因为本身自己的App源码非常多,加入这些开源库就就更大了,容易出现无法编译的问题,因为Android貌似对一个应用中的方法个数好像有限制,貌似是655**个吧,具体多少我也记不清)。
关于处理图片缓存上,我接触的两个播放器项目中,使用的都是BitmapFun,BitmapFun 是Google为Android开发提供了一个教程,既然是Google提供的,那么我觉得作为一名合格的Android开发人员很有必要学习学习,而且BitmapFun非常简单,基本可以满足我们项目中对于图片缓存处理需求了。
对于开源项目的学习,我通常很少在应用层面来学习的,因为如何使用一个开源项目的相关博客已经相当多了,而且写得都非常详细,对于大多数开源项目它都是自带sample的,所以如果想学习如何使用某个开源项目,好好研究sample就行了,但是我始终认为,熟悉经典开源项目源码才是王道。好了废话不多说,我们开始学习BitmapFun源码吧。
1、BitmapFun结构
BitmapFun和其他开源库的结构稍有不同,因为它仅仅是Google的培训教程,所以BitmapFun和它的sample放在了一个工程里面,结构图如下:上面部分是BitmapFun的应用,下面部分是BitmapFun的源码。
2、相关类介绍
在BitmapFun中最重要的一个类就是ImageFetcher,请求图片主要就是调用loadImage方法,但是这个类是继承ImageResizer,而ImageResizser是继承ImageWorker,所以我们就从ImageWorker开始学习吧
ImageWorker.java/** 这个类用来封装一次图片的加载过程,包括使用从缓存中加载 */public abstract class ImageWorker { private static final String TAG = ImageWorker; //这个变量用于动画效果,没有实际意义 private static final int FADE_IN_TIME = 200; //缓存,包括磁盘缓存和内存缓存 private ImageCache mImageCache; //创建缓存需要的参数 private ImageCache.ImageCacheParams mImageCacheParams; //加载过程中,ImageView显示的图片 private Bitmap mLoadingBitmap; //是否使用渐变效果 private boolean mFadeInBitmap = true; //是否提前退出任务,如果true,那么图片请求回来后是不会显示出来的 private boolean mExitTasksEarly = false; //是否暂停任务 protected boolean mPauseWork = false; private final Object mPauseWorkLock = new Object(); protected Resources mResources; private static final int MESSAGE_CLEAR = 0; private static final int MESSAGE_INIT_DISK_CACHE = 1; private static final int MESSAGE_FLUSH = 2; private static final int MESSAGE_CLOSE = 3; protected ImageWorker(Context context) { mResources = context.getResources(); } /** * 请求一张图片的接口 * @param 图片url * @param 要显示这种图片的ImageView */ public void loadImage(Object data, ImageView imageView) { if (data == null) { return; } BitmapDrawable value = null; //如果缓存对象不为空,那么从内存缓存中读取对象 if (mImageCache != null) { value = mImageCache.getBitmapFromMemCache(String.valueOf(data)); } if (value != null) { // 内存缓存命中,那么直接显示 imageView.setImageDrawable(value); } else if (cancelPotentialWork(data, imageView)) { //内存缓存没有命中,那么创建一个图片请求Task,将imageView作为参数 final BitmapWorkerTask task = new BitmapWorkerTask(imageView); //AsyncDrawable 是BitmapDrawable子类,主要用来存放当前任务的弱应用 final AsyncDrawable asyncDrawable = new AsyncDrawable(mResources, mLoadingBitmap, task); //将asyncDrawable设置到imageView中,这样imageView和当前任务就一一对应了 imageView.setImageDrawable(asyncDrawable); //调用AsyncTask的executeOnExecutor方法,这个AsyncTask和Android系统中的AsyncTask有些区别,但是使用上一样的 task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR, data); } } /** * 设置加载过程中的默认图片 * * @param bitmap */ public void setLoadingImage(Bitmap bitmap) { mLoadingBitmap = bitmap; } /** * 将本地图片设置为默认图片 * * @param resId */ public void setLoadingImage(int resId) { mLoadingBitmap = BitmapFactory.decodeResource(mResources, resId); } /** * 添加一个缓冲对象,创建磁盘缓存时需要子线程中完成 * @param fragmentManager * @param cacheParams The cache parameters to use for the image cache. */ public void addImageCache(FragmentManager fragmentManager, ImageCache.ImageCacheParams cacheParams) { mImageCacheParams = cacheParams; mImageCache = ImageCache.getInstance(fragmentManager, mImageCacheParams); //完成磁盘缓存初始化 new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE); } /** * Adds an {@link ImageCache} to this {@link ImageWorker} to handle disk and memory bitmap * caching. * @param activity * @param diskCacheDirectoryName See * {@link ImageCache.ImageCacheParams#ImageCacheParams(Context, String)}. */ public void addImageCache(FragmentActivity activity, String diskCacheDirectoryName) { mImageCacheParams = new ImageCache.ImageCacheParams(activity, diskCacheDirectoryName); mImageCache = ImageCache.getInstance(activity.getSupportFragmentManager(), mImageCacheParams); new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE); } /** * 设置是否使用渐变效果 */ public void setImageFadeIn(boolean fadeIn) { mFadeInBitmap = fadeIn; } //是否提前退出任务 public void setExitTasksEarly(boolean exitTasksEarly) { mExitTasksEarly = exitTasksEarly; setPauseWork(false); } /** * Subclasses should override this to define any processing or work that must happen to produce * the final bitmap. This will be executed in a background thread and be long running. For * example, you could resize a large bitmap here, or pull down an image from the network. * * @param data The data to identify which image to process, as provided by * {@link ImageWorker#loadImage(Object, ImageView)} * @return The processed bitmap */ protected abstract Bitmap processBitmap(Object data); /** * @return The {@link ImageCache} object currently being used by this ImageWorker. */ protected ImageCache getImageCache() { return mImageCache; } /** * Cancels any pending work attached to the provided ImageView. * @param imageView */ public static void cancelWork(ImageView imageView) { //通过ImageView找到task,为什么可以找到?因为imageView和task一一对应 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); //如果task不为空,那么取消 if (bitmapWorkerTask != null) { bitmapWorkerTask.cancel(true); if (BuildConfig.DEBUG) { final Object bitmapData = bitmapWorkerTask.data; Log.d(TAG, cancelWork - cancelled work for + bitmapData); } } } /** * Returns true if the current work has been canceled or if there was no work in * progress on this image view. * Returns false if the work in progress deals with the same data. The work is not * stopped in that case. */ public static boolean cancelPotentialWork(Object data, ImageView imageView) { //通过imageView找到task final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (bitmapWorkerTask != null) { //如果找到的task不为null,并且task的url和给定的url相同,那么取消任务 final Object bitmapData = bitmapWorkerTask.data; if (bitmapData == null || !bitmapData.equals(data)) { bitmapWorkerTask.cancel(true); if (BuildConfig.DEBUG) { Log.d(TAG, cancelPotentialWork - cancelled work for + data); } } else { // The same work is already in progress. return false; } } return true; } /** * 通过iamgeView找到对应的Task */ 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; } /** * 一个请求图片的异步任务, */ private class BitmapWorkerTask extends AsyncTask分析完ImageWorker之后,我们发现在ImageWorker中已经提供了获取网络图片的方法loadImage,当我调用了此方法后,首先会试图从内存缓存获取图片,如果获取成功,直接返回,如果没有获取成功,则启动一个BitmapWorkerTask,使用异步线程获取图片,在异步线程中,首先到磁盘中获取,如果磁盘没有获取,最后才从网络获取,我们发现在BitmapWorkerTask中是通过调用processBitmap方法完成图片获取的,但是这个方法是一个抽象方法,需要子类去实现,那我们到它的子类ImageResizer中
@Override protected Bitmap processBitmap(Object data) { return processBitmap(Integer.parseInt(String.valueOf(data))); }它调用的是另外一个重载的processBitmap方法,我们看看另外一个方法吧
private Bitmap processBitmap(int resId) { if (BuildConfig.DEBUG) { Log.d(TAG, processBitmap - + resId); } return decodeSampledBitmapFromResource(mResources, resId, mImageWidth, mImageHeight, getImageCache()); }我们发现这个方法仅仅是用来加载本地图片的,那它是如何实现网络图片的加载的呢,如果你把ImageResizer源码通读一边,你会发现ImageResizer这个类的主要功能如下:1、设置显示图片的sizse2、从磁盘缓存中加载图片所以从网络加载图片根本不是这个类的功能,聪明的同学马上就应该想到了ImageFetcher这个类,对!,我们就直接看看ImageFetcher这个类吧
private Bitmap processBitmap(String data) { final String key = ImageCache.hashKeyForDisk(data); FileDescriptor fileDescriptor = null; FileInputStream fileInputStream = null; DiskLruCache.Snapshot snapshot; //检查mHttpDiskCache是否已经初始化,这里一定要注意,mHttpDiskCache这个磁盘缓存是在ImageFetcher调用addImageCache时初始化的,如果你没有调用addImageCache //那么这里就会阻塞,从而无法获取图片,具体情况还请大家自己分析代码吧 synchronized (mHttpDiskCacheLock) { // Wait for disk cache to initialize while (mHttpDiskCacheStarting) { try { mHttpDiskCacheLock.wait(); } catch (InterruptedException e) {} } //下面这段代码就是从mHttpDiskCache里面写入图片 if (mHttpDiskCache != null) { try { snapshot = mHttpDiskCache.get(key); if (snapshot == null) { if (BuildConfig.DEBUG) { Log.d(TAG, processBitmap, not found in http cache, downloading...); } DiskLruCache.Editor editor = mHttpDiskCache.edit(key); if (editor != null) { //图片逻辑在这里 if (downloadUrlToStream(data, editor.newOutputStream(DISK_CACHE_INDEX))) { editor.commit(); } else { editor.abort(); } } snapshot = mHttpDiskCache.get(key); } if (snapshot != null) { fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX); fileDescriptor = fileInputStream.getFD(); } } catch (IOException e) { Log.e(TAG, processBitmap - + e); } catch (IllegalStateException e) { Log.e(TAG, processBitmap - + e); } finally { if (fileDescriptor == null && fileInputStream != null) { try { fileInputStream.close(); } catch (IOException e) {} } } } } Bitmap bitmap = null; if (fileDescriptor != null) { //调用ImageResizer中的方法来将mHttpDiskCache中的缓存生成指定大小的图片 bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth, mImageHeight, getImageCache()); } if (fileInputStream != null) { try { fileInputStream.close(); } catch (IOException e) {} } return bitmap; } /** * 从网络通过HttpURLConnection下载图片,并写入到磁盘缓存 * * @param urlString The URL to fetch * @return true if successful, false otherwise */ 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; } catch (final IOException e) { Log.e(TAG, Error in downloadBitmap - + e); } finally { if (urlConnection != null) { urlConnection.disconnect(); } try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (final IOException e) {} } return false; }好了,对于Bitmapfun的整个代码逻辑我就简单的分析到这里吧,其实了解了Bitmapfun的代码逻辑后,我们完全可以对其进行优化,我在这里仅仅提出一点可以优化的地方,优化的方法就交给大家完成吧比如BitmapWorkerTask在获取图片的时候先是读取磁盘缓存,然后从网络获取,也就是说如果读取本地和读取网络图片时在同一条线程中完成的,这个时候就有可能出现一个问题,本地图片存在却无法加载出来:例如:在网络条件不好的情况下,前面的五个图片请求刚好用完了所有的线程,由于网络条件不好,一直没有返回,而第六个图片刚好有缓存,那么它是无法加载出来的,因为没有线程了,所以解决方案就是学习Volley(我前面的文章对于Volley已经介绍了)中的解决方案,让一条线程专门处理本地图片,其他线程用于处理网络图片。就写到这里吧,如果大家有什么没看明白或者我写错了的,欢迎留言.....