Android图片缓存显示框架实现

Android开发中有时候需要我们自己实现一个图片显示的功能,要求能够根据图片URL来从网络上获取图片,如果该图片

已经再内存中则直接显示,否则在SD卡中查找,如果SD卡中没有最后尝试从网络中下载,如果下载成功后就保存到sd卡中,

下次就不需要再次下载了。这里实现了一个图片加载实现类,项目中需要可以直接使用方便我们的日后开发。

异步加载框架如下:

/**
 * 一个拥有图片缓存能力的图片读取实用类。
 * 

* 读取图片时,如果指定图片已存在于内存中则直接返回,否则尝试从sd卡中 * 查找,如果sd卡中仍不存在则尝试从网络下载之(下载成功后会自动保存到 * sd卡,下次就不需要再次下载了,从而节省流量)。 * * @author AndyYuan * @see #loadBitmap(ImageView, String, String, ImageCallBack, Observer, int, int, boolean) **/ public class AsyncBitmapLoader { private final static String TAG = AsyncBitmapLoader.class.getSimpleName(); /** * 使用在线列表的目的在于防止正在在载中的商品图片被重复下载(同时开启多个线程下载同一图片), * 否则很容易就内在溢出了! */ private ArrayList mDownloadingGoodPics = new ArrayList(); /** * 内存图片软引用缓冲 */ private LruCache> mImageCache = null; /** * 本地缓存目录 */ private String mLocalCacheDir = null; /** * 构造方法。 * * @param DEFAULT_CACHED_SDCARD_PATH 此路径目录地址末尾是需要"/"的哦! */ public AsyncBitmapLoader(String DEFAULT_CACHED_SDCARD_PATH) { this(DEFAULT_CACHED_SDCARD_PATH // 默认以系统允许的APP的1/8可用内存作为最大图片缓存内存 , ((int) (Runtime.getRuntime().maxMemory() / 1024)) / 8); } /** * 构造方法。 * * @param DEFAULT_CACHED_SDCARD_PATH 此路径目录地址末尾是需要"/"的哦! * @param maxMemory 用于图片缓存的最大内存(单位是KB) * @see LruCache */ public AsyncBitmapLoader(String DEFAULT_CACHED_SDCARD_PATH, int maxMemory) { this.mLocalCacheDir = DEFAULT_CACHED_SDCARD_PATH; int appMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); Log.d(TAG, "【ABL】程序当前可运行的最大内存是:" + appMemory + "KB, 用于图片缓存的内存空间:" + maxMemory + "KB."); // LruCache通过构造函数传入缓存值,以KB为单位。 mImageCache = new LruCache>(maxMemory); } /** * 清空指定key的缓存(注意:本方法当前只实现了清除内存缓存,暂未实现同时清除sd卡中的缓存文件哦)。 * * @param key * @retrun true表示清除成功,否则不成功(可能是此key的缓存并不存) */ public boolean clearCache(String key) { if (key != null && mImageCache != null) { return this.mImageCache.remove(key) != null; } return false; } /** * 尝试异步加载图片. *

* 逻辑可能是优先从内存中读取、内存没找到的情况下再尝试从本地读取,最后才尝试从网络读取. *

* 在imageURL == null且imageFileName != null的情况下,本类相当于本地文件缓存实现类。 * * @param imageView 图片成功获取后要显示的目标UI组件 * @param imageURL 在没有缓存的情况下尝试从网络加载的地址:本参数可为null,但与参数imageFileName不可同时为 * null.本参数为null时即意味着只尝试从本地SD卡中读取图片数据到缓存中(而不从网络读取了) * @param imageCallBack 图片读取完成(成功或失败)后的回调,调用者的图片的显示处理逻辑可在此实现 * @return 成功读取Bitmap对象后即返回之,否则返回null */ public Bitmap loadBitmap(final ImageView imageView, final String imageURL , final ImageCallBack imageCallBack) { return loadBitmap(imageView, imageURL, null, imageCallBack , null, -1, -1, false); } /** * 尝试异步加载图片. *

* 逻辑可能是优先从内存中读取、内存没找到的情况下再尝试从本地读取,最后才尝试从网络读取. *

* 在imageURL == null且imageFileName != null的情况下,本类相当于本地文件缓存实现类。 * * @param imageView 图片成功获取后要显示的目标UI组件 * @param imageURL 在没有缓存的情况下尝试从网络加载的地址:本参数可为null,但与参数imageFileName不可同时为 * null.本参数为null时即意味着只尝试从本地SD卡中读取图片数据到缓存中(而不从网络读取了) * @param imageCallBack 图片读取完成(成功或失败)后的回调,调用者的图片的显示处理逻辑可在此实现 * @param reqWidth 生成的Bitmap对象时的真正图片宽(此值用于减小最终生成的Bitmap内存时有用),<=0表示不启用 * @param reqHeight 生成的Bitmap对象时的真正图片高(此值用于减小最终生成的Bitmap内存时有用),<=0表示不启用 * @return 成功读取Bitmap对象后即返回之,否则返回null */ public Bitmap loadBitmap(final ImageView imageView, final String imageURL , final ImageCallBack imageCallBack /** * 要转换成的像素数,比如:原图是640*640的大图,但用到的地方只需要200*200的图, * 那么此值设为200*200为佳,因这将使得返回的Bitmap对象占用的内存为200*200而非640*640 */ , final int reqWidth, final int reqHeight) { return loadBitmap(imageView, imageURL, null, imageCallBack , null, reqWidth, reqHeight, false); } /** * 尝试异步加载图片. *

* 逻辑可能是优先从内存中读取、内存没找到的情况下再尝试从本地读取,最后才尝试从网络读取. *

* 在imageURL == null且imageFileName != null的情况下,本类相当于本地文件缓存实现类。 * * @param imageView 图片成功获取后要显示的目标UI组件 * @param imageURL 在没有缓存的情况下尝试从网络加载的地址:本参数可为null,但与参数imageFileName不可同时为 * null.本参数为null时即意味着只尝试从本地SD卡中读取图片数据到缓存中(而不从网络读取了) * @param imageFileName 要缓存的文件名(本方法中使用的优先级1),通常情况下就是最终缓存到SD卡中的文件名。本 * 参数可为null,为null时本方法将尝试智能从参数 imageURL中取出来本方法中使用的优先级2),但并不绝对,因为url * 中很可能没有存放真正的文件名!本参数可为null,但与参数imageURL不可同时为null. * @param imageCallBack 图片读取完成(成功或失败)后的回调,调用者的图片的显示处理逻辑可在此实现 * @return 成功读取Bitmap对象后即返回之,否则返回null */ public Bitmap loadBitmap(final ImageView imageView, final String imageURL , final String imageFileName, final ImageCallBack imageCallBack) { return loadBitmap(imageView, imageURL, imageFileName, imageCallBack , null, -1, -1, false); } /** * 尝试异步加载图片. *

* 逻辑可能是优先从内存中读取、内存没找到的情况下再尝试从本地读取,最后才尝试从网络读取. *

* 在imageURL == null且imageFileName != null的情况下,本类相当于本地文件缓存实现类。 * * @param imageView 图片成功获取后要显示的目标UI组件 * @param imageURL 在没有缓存的情况下尝试从网络加载的地址:本参数可为null,但与参数imageFileName不可同时为 * null.本参数为null时即意味着只尝试从本地SD卡中读取图片数据到缓存中(而不从网络读取了) * @param imageFileName 要缓存的文件名(本方法中使用的优先级1),通常情况下就是最终缓存到SD卡中的文件名。本 * 参数可为null,为null时本方法将尝试智能从参数 imageURL中取出来本方法中使用的优先级2),但并不绝对,因为url * 中很可能没有存放真正的文件名!本参数可为null,但与参数imageURL不可同时为null. * @param imageCallBack 图片读取完成(成功或失败)后的回调,调用者的图片的显示处理逻辑可在此实现 * @param reqWidth 生成的Bitmap对象时的真正图片宽(此值用于减小最终生成的Bitmap内存时有用),<=0表示不启用 * @param reqHeight 生成的Bitmap对象时的真正图片高(此值用于减小最终生成的Bitmap内存时有用),<=0表示不启用 * @return 成功读取Bitmap对象后即返回之,否则返回null */ public Bitmap loadBitmap(final ImageView imageView, final String imageURL , final String imageFileName, final ImageCallBack imageCallBack /** * 要转换成的像素数,比如:原图是640*640的大图,但用到的地方只需要200*200的图, * 那么此值设为200*200为佳,因这将使得返回的Bitmap对象占用的内存为200*200而非640*640 */ , final int reqWidth, final int reqHeight ) { return loadBitmap(imageView, imageURL, imageFileName , imageCallBack, null, reqWidth, reqHeight, false); } /** * 尝试异步加载图片. *

* 逻辑可能是优先从内存中读取、内存没找到的情况下再尝试从本地读取,最后才尝试从网络读取. *

* 在imageURL == null且imageFileName != null的情况下,本类相当于本地文件缓存实现类。 * * @param imageView 图片成功获取后要显示的目标UI组件 * @param imageURL 在没有缓存的情况下尝试从网络加载的地址:本参数可为null,但与参数imageFileName不可同时为 * null.本参数为null时即意味着只尝试从本地SD卡中读取图片数据到缓存中(而不从网络读取了) * @param imageFileName 要缓存的文件名(本方法中使用的优先级1),通常情况下就是最终缓存到SD卡中的文件名。本 * 参数可为null,为null时本方法将尝试智能从参数 imageURL中取出来本方法中使用的优先级2),但并不绝对,因为url * 中很可能没有存放真正的文件名!本参数可为null,但与参数imageURL不可同时为null. * @param imageCallBack 图片读取完成(成功或失败)后的回调,调用者的图片的显示处理逻辑可在此实现 * @param imageFileNameCallBack 服务端可能返回的文件名(有些情况下可能不会返回文件名),本回调用于获取服务端 * 可能返回来的文件名。本回调的作用在于本例用本方法前完全没办法拿到文件名的情况下,尝试下载服务端文件数据的同时 * 也把文件名取下来供调用本方法者存起来后绪使用哦.本回调被调用时会将服务端传过来的文件名作为Observer类的update * 方法的参数data传过来。本参数为null表示不开启回调功能.
为何要使用本参数而不干脆让本方法返回一个包含了文件 * 的数据组呢(简单又好理解),目的是不想加重对让原来的方法使用的难度,反正需要文件名的地方用本回调就行了,这样 * 更合理一些 * @param reqWidth 生成的Bitmap对象时的真正图片宽(此值用于减小最终生成的Bitmap内存时有用),<=0表示不启用 * @param reqHeight 生成的Bitmap对象时的真正图片高(此值用于减小最终生成的Bitmap内存时有用),<=0表示不启用 * @param donotLoadFromDisk true表示当内存中不存在缓存时不尝试从SD卡读取直接从网络加载,否则将按正常逻辑尝 * 试从sd卡加载,一般情况下请用false * @return 成功读取Bitmap对象后即返回之,否则返回null */ public Bitmap loadBitmap(final ImageView imageView , final String imageURL , final String imageFileName , final ImageCallBack imageCallBack , final Observer imageFileNameCallBack /** * 要转换成的像素数,比如:原图是640*640的大图,但用到的地方只需要200*200的图, * 那么此值设为200*200为佳,因这将使得返回的Bitmap对象占用的内存为200*200而非640*640 */ , final int reqWidth, final int reqHeight , final boolean donotLoadFromDisk ) { // 此2参数不可同时为null! if (imageURL == null && imageFileName == null) { Log.e(TAG, "【ABL】imageURL和imageFileName不可同时为null!"); return null; } // 是否要调整图片的BitmapFactory.Options.inSampleSize值(从而节省内存) final boolean needSetInSampleSize = (reqWidth > 0 && reqHeight > 0); Log.d(TAG, "【ABL】======================图片" + imageURL + "(" + imageFileName + ")载入中======================"); final String cacheKey = (imageURL != null ? imageURL : imageFileName); //在内存缓存中,则返回Bitmap对象 if (mImageCache.get(cacheKey) != null) { // Log.d(TAG, "【MALL】imageCache.get("+imageURL+") != null!"); SoftReference reference = mImageCache.get(cacheKey); Bitmap bitmap = (Bitmap) reference.get(); if (bitmap != null) { Log.d(TAG, "【ABL】缓存中找到了图片:" + imageURL + "直接返回。。。。"); return bitmap; } } Log.d(TAG, "【ABL】缓存中没有找到图片(或者已找到但SoftReference中实例已被回收并置为null):" + imageURL + "..."); /** * 加上一个对本地SD卡缓存的查找 */ final String bitmapFileName = (imageFileName == null ? imageURL.substring(imageURL.lastIndexOf("/") + 1) : imageFileName); Log.d(TAG, "【ABL】imageFileName=" + imageFileName + ", imageURL=" + imageURL + ", bitmapFileName=" + bitmapFileName); if (donotLoadFromDisk) { Log.i(TAG, "【ABL】donotLoadFromDisk=true,不需要尝试从磁盘加载,将尝试从网络加载!"); } else { File localCachedFile = new File(mLocalCacheDir + bitmapFileName); Log.d(TAG, "【ABL】它存在于本地SD卡" + localCachedFile.getAbsolutePath() + "中吗?" + localCachedFile.exists()); if (localCachedFile.exists()) { Bitmap bb = null; // 为了代码的健壮性,在涉及Bitmap操作的地方应习惯性地进行OOM显示捕获,防止在极烂的手机上OOM try { bb = BitmapFactory.decodeFile(localCachedFile.getAbsolutePath() , needSetInSampleSize ? BitmapHelper.computeSampleSize2(localCachedFile.getAbsolutePath(), reqWidth, reqHeight) : null); } catch (OutOfMemoryError oom) { Log.w(TAG, "A在执行BitmapFactory.decodeFile(..)时OOM了,本次操作没有继续哦。", oom); } if (bb != null) { // 2019-09-28 add by AndyYuan mImageCache.put(cacheKey, new SoftReference(bb)); } Log.d(TAG, "【ABL】它存在于本地SD卡中,直接返回了。。。"); return bb; } } Log.d(TAG, "【ABL】》》》》》》》它也不存在于本地SD卡中,从网络异步加载马上开始..."); // 商品图片已经在”下载中“(用于防止重复下载) if (mDownloadingGoodPics.contains(cacheKey)) { Log.d(TAG, "【ABL】" + imageURL + "正在下载中,本次下载请求将被忽略,无需重复下载,否则将OOM!"); } else { Log.d(TAG, "【ABL】" + imageURL + "还未下载,马上开始异步下载线程..."); final Handler handler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == 0) { Log.d(TAG, "【ABL】》》》》》》》图片" + imageURL + "从网络加载完成,即将显示到UI上."); imageCallBack.imageLoad(imageView, (Bitmap) msg.obj); } else if (msg.what == -1) { Log.d(TAG, "【ABL】》》》》》》》图片" + imageURL + "从网络加载失败了!"); imageCallBack.imageLoadFaild(imageView); } } }; //如果不在内存缓存中,也不在本地(被jvm回收掉),则开启线程下载图片 new Thread() { @Override public void run() { // 将正在下载中的商品图片地址加入到”下载中“列表(用于防止重复下载) mDownloadingGoodPics.add(cacheKey); boolean downSucess = false; //** 【说明:】为何使用HTTP先把文件下载下来后再decode成bitmap而不是先用bitmap.decodeStream后再将其保存成本地文件呢? //** 原因在于后者保存的文件要远大于实际存放于服务器端的文件大小(因Bitmap再转文件时有很多位图元数据等,而且也不能压缩 //** ,因服力端本身已压缩过,再压缩就质量越来越差了),而前者下下来后还是跟服务端一样的大小,再decode成bitmap一样正常使用 Bitmap bitmap = null; try { // InputStream bitmapIs = ToolKits.getStreamFromURL(imageURL); // 目录不存在则建立之 File dir = new File(mLocalCacheDir); Log.e("HistoryAdapter", "创建下载目录:" + mLocalCacheDir); String savedFilePath = null; // String savedFilePath = HttpFileDownloadHelper.downloadFile(imageURL, dir.getAbsolutePath(), 0, null); //// bitmap = BitmapFactory.decodeStream(bitmapIs); Object[] downloadRet = HttpFileDownloadHelper.downloadFileEx(imageURL , dir.getAbsolutePath(), 0, null, false); if (downloadRet != null && downloadRet.length >= 3) { // 0单元存放的是文件最终下载到的绝对路径地址 savedFilePath = (String) downloadRet[0]; // 服务端返回的文件名(有时候本AsyncBitmapLoader无法指明要缓存的文件名,而服务端可以返回文件名,那么 // 本方法中就不需要明确传进来了,节省调用AsyncBitmapLoader类的容易程度) String downloadFileName = (String) downloadRet[2]; if (imageFileNameCallBack != null) imageFileNameCallBack.update(null, downloadFileName); // if (savedFilePath != null) { // 为了代码的健壮性,在涉及Bitmap操作的地方应习惯性地进行OOM显示捕获,防止在极烂的手机上OOM try { bitmap = BitmapFactory.decodeFile(savedFilePath , needSetInSampleSize ? BitmapHelper.computeSampleSize2(savedFilePath, reqWidth, reqHeight) : null); Log.e("HistoryAdapter", "下载的图片:" + bitmap); } catch (OutOfMemoryError oom) { Log.w(TAG, "B在执行BitmapFactory.decodeFile(..)时OOM了,本次操作没有继续哦。", oom); } if (bitmap != null) { // mImageCache.put(cacheKey, new SoftReference(bitmap)); Message msg = handler.obtainMessage(0, bitmap); handler.sendMessage(msg); downSucess = true; } } } } catch (Exception e) { Log.d(TAG, e.getMessage(), e); } finally { // 下载或其它处理导致从网络读取没有成功的情况下 if (!downSucess) { Message msg = handler.obtainMessage(-1, null); handler.sendMessage(msg); } } // 将正在下载中的商品图片地址“下载中”列表移除(已经下载完了或者下载出错) mDownloadingGoodPics.remove(cacheKey); } }.start(); } return null; } /** * 回调接口. */ public static abstract class ImageCallBack { public abstract void imageLoad(ImageView imageView, Bitmap bitmap); public void imageLoadFaild(ImageView imageView) { // default do nothing } } public static abstract class HistoryCallBack { public abstract void imageLoad(ImageView imageView, Bitmap bitmap); public void imageLoadFaild(ImageView imageView) { // default do nothing } } }

该类的使用非常简单:

1:先实定义该对象:

private AsyncBitmapLoader asyncLoader = null;

2:在构造函数中实例化该对象:

this.asyncLoader = new AsyncBitmapLoader(AvatarHelper.getUserAvatarSavedDirHasSlash(context));

其中AvatarHelper.getUserAvatarSavedDirHasSlash(context))为我们需要获取的图片保存目录。

3:使用该图片加载框架:

// 尝试为群组加载群头像
Bitmap bitmap = asyncLoader.loadBitmap(ImageView(目标加载)
        , ImageURL
        , null
        , new AsyncBitmapLoader.ImageCallBack() {
            @Override
            public void imageLoad(ImageView imageView, Bitmap bitmap) {
                imageView.setImageBitmap(bitmap);

                // 刷新列表的UI显示(以便及时显示最新的头像)
                Adapter.this.notifyDataSetChanged();
            }
        }
        , null
        // 指定生成的Bitmap对象所描述的图片的大小(更小的尺寸将成倍地减小内存消耗)
        , 200, 200 // 此头像用于界面中的用户头像,大小参考:main_alarms_list_item.xml中的@+id/main_alarms_list_item_iconView
        // 加载缓存时只尝试从内存加载(不尝试从磁盘),目的是希望在下次启动app时能及时刷新缓
        // 存(但又不是频繁从网络加载,确保只在每次启动app时加载一次),以便保持群头像的最新
        , true
);

if (bitmap != null) {
   ImageView.setImageBitmap(bitmap);
}

你可能感兴趣的:(Bitmap问题,基础知识)