LrcCache和DiskLruCache相结合打造图片加载框架
1概述
这几在研究图片加载的方面的知识,在网上看了一下前辈们写的文章,受到了一些启发,于是综合多方面的知识,将这些整合起来,自己边写了一个图片加载框架。说到图片加载最容易出问题的就是OOM就是内存溢出,所以一定要限制加载图片时使用的内存,这就使用到Android提供的缓存类LruCache,关于LruCache的知识这里不再赘述,大家自行学习。但是如果图片非常的多而且频繁操作的话,加上LruCache的缓存空间有限,缓存就不得不经常更新,效果会大打折扣,于是就想到使用LruCache和DiskLruCache结合起来,做一个二级缓存。DiskLruCache使用的手机的SD卡或者手机存储作为缓存,不占用手机App运行时占用的内存。
2缓存
主要思路,得到的图片Bitmap存入LruCache中,如果LruCache如果空间不够使用会按照最近最少使用的原则去把最近最少使用的Bitmap删除。将LruCache删除的Bitmap存入DiskLruCache缓存中,实现二级缓存。
2.1 改造LruCache
由于Android提供LruCache 在删除最近最少使用的对象时是直接删除对象,但是我要的是将删除的对象返回,并存入到DiskLruCache中,所有要对LruCache进行改造。由于LruCache的缓存方法public final V put(K key, V value)如下,不能重写,所有只能重新写一个与LruCache一样的类,复制LruCache的所有方法过来,但是单独改写public final V put(K key, V value)以及相关的private void trimToSize(int maxSize)。
public final Vput(K
key, V value) {
if (key == null|| value ==
null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous !=
null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
重新建立类BitmapLruCache,重新写public finalV
put(Kkey, V
value)和private voidtrimToSize(int
maxSize)方法。
/** * 将对象加入缓存,如果缓存已经满,则删除最近最少使用的对象,并返回被删除的对象列表 * @param key * @param value * @return */ public final LinkedHashMap<K, V> put(K key, V value) { if (key == null || value == null) { throw new NullPointerException("key == null || value == null"); } V previous; synchronized (this) { putCount++; size += safeSizeOf(key, value); previous = map.put(key, value); if (previous != null) { size -= safeSizeOf(key, previous); } } if (previous != null) { entryRemoved(false, key, previous, value); } return trimToSize(maxSize); }
/** * 删除最近最少使用对象,并返回被删除的对象列表 * @param maxSize * @return */ public LinkedHashMap<K, V> trimToSize(int maxSize) { LinkedHashMap<K, V> trimMap = new LinkedHashMap<K, V>(0, 0.75f, true); //被删除的对象列表 while (true) { K key; V value; synchronized (this) { if (size < 0 || (map.isEmpty() && size != 0)) { throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!"); } if (size <= maxSize || map.isEmpty()) { break; } Map.Entry<K, V> toEvict = map.entrySet().iterator().next(); key = toEvict.getKey(); value = toEvict.getValue(); trimMap.put(key, value); //添加被删除的对象 map.remove(key); size -= safeSizeOf(key, value); evictionCount++; } entryRemoved(true, key, value, null); } return trimMap; }
2.2 DiskLruCache
对应DiskLruCache大家可以学习这篇文章http://blog.csdn.net/guolin_blog/article/details/28863651,说的非常好,我就不赘述了。
2.3 LruCache和DiskLruCache相结合
建立BitmapCacheL2类,结合LrcCache和DiskLruCache,写缓存方法。
在构造方法初始化LrcCache和DiskLruCache
public BitmapCacheL2(Context context){ mLrcCache = new BitmapLruCache<String, Bitmap>(mCacheSize){ @Override protected int sizeOf(String key, Bitmap value) { // TODO Auto-generated method stub if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { return value.getByteCount(); } else { return value.getRowBytes() * value.getHeight(); } // Pre HC-MR1 } }; //start 初始化手机SD存储缓存 File cacheDir = getDiskCacheDir(context, "thumb"); if (!cacheDir.exists()) { cacheDir.mkdirs(); } // 创建DiskLruCache实例,初始化缓存数据 try { mDiskLruCache = DiskLruCache .open(cacheDir, getAppVersion(context), 1, MAX_FILE_SIZE); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } //end 初始化手机SD存储缓存 }
BitmapCacheL2 最主要的的两个方法分别是添加缓存和取出缓存的方法如下
/** * 保存bitmap到缓存 * @param url * @param bitmap */ public void putBitmap(String url, Bitmap bitmap) { Log.i(tag, "putBitmap *** "); //start 将bitmap存入内存缓存,如果已经存满,则删除最近最少使用的bitmap,并返回被删除的bitmap对象列表 LinkedHashMap<String, Bitmap> trimMap; trimMap = mLrcCache.put(url, bitmap); putBitmap2DiskLruCache(url,bitmap); //end 将bitmap存入内存缓存,如果已经存满,则删除最近最少使用的bitmap,并返回被删除的bitmap对象列表 //start 将被LrucCache删除的bitmap存入DiskLruCache if(null!=trimMap && !trimMap.isEmpty()){ Log.i(tag, " LruCache--->DiskCache"); Iterator<?> it = trimMap.entrySet().iterator(); while(it.hasNext()){ Entry<String, Bitmap> entry = (Entry<String, Bitmap>) it.next(); putBitmap2DiskLruCache(entry.getKey(), entry.getValue());//向DiskLruCache添加缓存
} } ///end 将被LrucCache删除的bitmap存入DiskLruCache }
/** * 从缓存获取bitmap */ public Bitmap getBitmap(String url) { // TODO Auto-generated method stub Log.i(tag, "getBitmap ***"); //首先从手机内存缓存中获取 Bitmap map = mLrcCache.get(url); //手机内存缓存没有,再从手机SD存储缓存中获取 if(null==map){ map = getBitmapFromDiskLruCache(url); } return map; }
3图片加载类SDImageLoader
图片加载类的主要工作过程:首先查找缓存,找到相应Bitmap则返回Bitmap,如果没有则根据路径从本地加载或者从网络下载;将从本地加载或者从网络下载Bitmap加入到缓存中,并刷新UI。当然加载图片Bitmap的操作都是在线程中进行的,为了管理这些线程,我建立了线程池,和用于管理线程池的线程,和线程池执行的任务队列,线程的调度方式有两种FIFO或者LIFO。
3.1图片加载类初始化
图片加载类使用单例模式:
/** * 获取实例对象 * @param context * @param mThreadCount 并行线程数量 * @param type 任务执行顺序 * @return */ public static SDImageLoader getInstance (Context context,int mThreadCount,Type type) { if(null==mInstace){ synchronized (ImageLoader.class) { if(null == mInstace) { mInstace = new SDImageLoader(mThreadCount,type,context); } } } return mInstace; }
初始化图片加载类:
/** * 初始化 * @param mThreadCount 线程数量 * @param type 调度类型 * @param context */ private void init(int mThreadCount,Type type,Context context) { mSemaphonreThreadPool = new Semaphore(mThreadCount); //任务执行信号量
//start 初始化控制线程池任务执行的线程 mPoolThread = new Thread() { @Override public void run() { super.run(); Looper.prepare(); mPoolThreadHandler = new Handler () { @Override public void handleMessage(Message msg) { super.handleMessage(msg); try { mSemaphonreThreadPool.acquire(); //线程池去取出一个任务执行 mThreadPool.execute(getTask()); } catch (InterruptedException e) { e.printStackTrace(); } } }; mSemaphorePoolThreadHandler.release(); Looper.loop(); } }; mPoolThread.start(); //start 初始化控制线程池任务执行的线程 /* int maxMemory = (int)Runtime.getRuntime().maxMemory(); int cacheMemory = maxMemory/8;*/ mLruCache = new BitmapCacheL2(context); //缓存 mTaskQueue = new LinkedList<Runnable>(); //任务队列 mThreadPool = Executors.newFixedThreadPool(mThreadCount); //线程池
}
3.2图片加载类的主要方法
图片加载类的主要方法:public void loadImage(final String path, final ImageView imageView,boolean isFromNetwork),首先通过isFromNetwork判断是加载本地图片或者网络图片,然后选择一种加载图片Runnable任务,将任务加入到线程池任务队列中,并用Handler发送消息,通知控制控制线程,控制线程按照Type的调度类型取出任务提交给线程池执行。Runnable任务得到Bitmap之后会根据ImageView的高宽和Bitmap的高宽做图片的采样压缩,节省内存;接着使用Handler发送消息更新UI。
public void loadImage(final String path, final ImageView imageView,boolean isFromNetwork){ if(null == imageView) { return; } imageView.setTag(path); //start 初始化更新UI方法 if(null == mUIHandler){ mUIHandler = new Handler(){ @Override public void handleMessage(Message msg) { ImageViewBeanHolder holder = (ImageViewBeanHolder) msg.obj; ImageView img = holder.imageView; String tagPath = img.getTag().toString(); if(tagPath.equals(holder.path)){ //判断是否对应的路径 Log.i(TAG, " path = " + holder.path + ""); img.setImageBitmap(holder.bitmap); } } }; } //end 初始化更新UI方法 Bitmap bitmap = getBitmapFromLruCache(path); //从缓存中获取Bitmap if(null != bitmap ){ refreshImageView(bitmap, imageView, path); //刷新UI }else { if(isFromNetwork == true) { //从网络加载图片 // addTask(new BitmapFromNetworkRunnable(path,imageView)); addTask(new Runnable() { @Override public void run() { Bitmap bm = null; if(Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO){ //Android 2.2 以前版本使用此方法 downloadImgByUrlHttpClient(path,imageView); }else { bm = downloadImgByUrl(path,imageView); } // bm = downloadImgByUrlHttpClient(path,imageView); if(null == bm) { mSemaphonreThreadPool.release(); return; } mSemaphonreThreadPool.release(); addBitmap2LruCache(path, bm); refreshImageView(bm, imageView, path); } }); } else { //加载手机本地的图片 addTask(new Runnable() { @Override public void run() { // 加载图片 // 图片压缩 // 1获取图片显示的大小 ImageSize imageSize = getImageSize(imageView); // 2图片压缩 Bitmap bm = decodeSampleBitmap(path, imageSize.width, imageSize.height); addBitmap2LruCache(path, bm); mSemaphonreThreadPool.release(); refreshImageView(bm, imageView, path); } }); } } }
3.2.1加载手机本地图片
addTask(new Runnable() { @Override public void run() { // 加载图片 // 图片压缩 // 1获取图片显示的大小 ImageSize imageSize = getImageSize(imageView); // 2图片压缩 Bitmap bm = decodeSampleBitmap(path, imageSize.width, imageSize.height); addBitmap2LruCache(path, bm); mSemaphonreThreadPool.release(); refreshImageView(bm, imageView, path); } });
3.2.2 加载网络图片
加载网络图片,考虑到Android版本问题,Android 2.2 以前使用HttpClient,Android 2.2以后使用HttpURLConnection。
HttpClient 的下载图片方法:
/** * 从网络下载图片 * @param urlStr * @param imageview * @return */ @SuppressWarnings("deprecation") public Bitmap downloadImgByUrlHttpClient(String urlStr, ImageView imageview) { Log.i(TAG,"downloadImgByUrlHttpClient *** "); HttpClient httpclient = new DefaultHttpClient(); HttpGet httpget = new HttpGet(urlStr); try { HttpResponse response = httpclient.execute(httpget); if(HttpStatus.SC_OK == response.getStatusLine().getStatusCode()) { HttpEntity entity = response.getEntity(); InputStream is = null; is = new BufferedInputStream(entity.getContent()); /* is.mark(is.available()); Options opts = new Options(); opts.inJustDecodeBounds = true; bitmap = BitmapFactory.decodeStream(is, null, opts); //获取imageview想要显示的宽和高 ImageSize imageViewSize = getImageSize(imageview); opts.inSampleSize = getBitmapSampleSize(opts, imageViewSize.width, imageViewSize.height); opts.inJustDecodeBounds = false; is.reset(); bitmap = BitmapFactory.decodeStream(is, null, opts);*/ // is = new BufferedInputStream(conn.getInputStream()); Log.i(TAG, " befor available() = " + is.available()); Bitmap bitmap = BitmapFactory.decodeStream(is); Log.i(TAG, "after available() = " + is.available()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); // start 按照图片格式将bitmap转为输出流 if (urlStr.endsWith("png")) { bitmap.compress(CompressFormat.PNG, 50, baos); } else if (urlStr.endsWith("webp")) { bitmap.compress(CompressFormat.WEBP, 50, baos); } else { bitmap.compress(CompressFormat.JPEG, 50, baos); } // end 按照图片格式将bitmap转为输出流 InputStream isBm = new ByteArrayInputStream(baos.toByteArray()); Log.i(TAG, " befor available() isBm = " + isBm.available()); isBm.mark(isBm.available()); // start 采样压缩图片 Options opts = new Options(); opts.inJustDecodeBounds = true; bitmap = BitmapFactory.decodeStream(isBm, null, opts); Log.i(TAG, "after available() isBm = " + isBm.available()); // 获取imageview想要显示的宽和高 ImageSize imageViewSize = getImageSize(imageview); opts.inSampleSize = getBitmapSampleSize(opts, imageViewSize.width, imageViewSize.height); // 采样 opts.inJustDecodeBounds = false; if (isBm.markSupported()) { isBm.reset(); } bitmap = BitmapFactory.decodeStream(isBm, null, opts); // end 采样压缩图片 isBm.close(); baos.close(); // bitmap = BitmapFactory.decodeStream(is); is.close(); return bitmap; } } catch (ClientProtocolException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }finally { httpclient.getConnectionManager().shutdown(); } return null; }
HttpURLConnection 下载图片的方法:
/** * 从网络下载图片 * @param urlStr 图片地址 * @param imageview * @return */ public Bitmap downloadImgByUrl(String urlStr, ImageView imageview) { Log.i(TAG, "downloadImgByUrl *** "); FileOutputStream fos = null; BufferedInputStream is = null; try { URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.connect(); Log.i(TAG, " ResponseCode = " + conn.getResponseCode()); is = new BufferedInputStream(conn.getInputStream()); Log.i(TAG, " befor available() = " + is.available()); Bitmap bitmap = BitmapFactory.decodeStream(is); Log.i(TAG, "after available() = " + is.available()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); // start 按照图片格式将bitmap转为输出流 if (urlStr.endsWith("png")) { bitmap.compress(CompressFormat.PNG, 50, baos); } else if (urlStr.endsWith("webp")) { bitmap.compress(CompressFormat.WEBP, 50, baos); } else { bitmap.compress(CompressFormat.JPEG, 50, baos); } // end 按照图片格式将bitmap转为输出流 InputStream isBm = new ByteArrayInputStream(baos.toByteArray()); Log.i(TAG, " befor available() isBm = " + isBm.available()); isBm.mark(isBm.available()); // start 采样压缩图片 Options opts = new Options(); opts.inJustDecodeBounds = true; bitmap = BitmapFactory.decodeStream(isBm, null, opts); Log.i(TAG, "after available() isBm = " + isBm.available()); // 获取imageview想要显示的宽和高 ImageSize imageViewSize = getImageSize(imageview); opts.inSampleSize = getBitmapSampleSize(opts, imageViewSize.width, imageViewSize.height); // 采样 opts.inJustDecodeBounds = false; if (isBm.markSupported()) { isBm.reset(); } bitmap = BitmapFactory.decodeStream(isBm, null, opts); // end 采样压缩图片 isBm.close(); baos.close(); is.close(); conn.disconnect(); return bitmap; } catch (Exception e) { e.printStackTrace(); } finally { try { if (is != null) is.close(); } catch (IOException e) { } try { if (fos != null) fos.close(); } catch (IOException e) { } } return null; }
4仿微信图片选择MainActivity1
主要过程:使用ContentResolver搜索手机内所有的图片,得到所有包含图片的文件夹;在搜索的同时得到文件夹的第一张图片的路径,和包含图片最多的文件夹;Handler发送消息更新UI显示包含图片最多的文件夹内的所有图片;增加点击事件可以选择其他的文件夹。
4.1 搜索手机内的图片
/** * 利用contentPrivider扫描手机中的图片 */ private void initDatas(){ if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { Toast.makeText(this,"没有存储卡",Toast.LENGTH_LONG).show(); return; } mProgressDialog = ProgressDialog.show(this,null,"正在查找.."); new Thread(){ @Override public void run() { super.run(); Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; ContentResolver cr = MainActivity1.this.getContentResolver(); Cursor cursor = cr.query(mImageUri, null, MediaStore.Images.Media.MIME_TYPE + " = ? or " + MediaStore.Images.Media.MIME_TYPE + " = ?", new String[]{"image/jpeg", "image/png"}, MediaStore.Images.Media.DATE_MODIFIED); Set<String> mDirPath = new HashSet<String>(); //已经扫描过的包含图片文件的文件夹路径 String firstImage = null; while (cursor.moveToNext()) { String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)); //第一张图片的路径 if(null == firstImage){ firstImage = path; } //start 获取该图片的父路径 File parentFile = new File(path).getParentFile(); if(parentFile==null){ continue; } //end 获取该图片的父路径 String dirPath = parentFile.getAbsolutePath(); FolderBean folderBean = null; if(mDirPath.contains(dirPath)){ continue; }else { mDirPath.add(dirPath); folderBean = new FolderBean(); folderBean.setDir(dirPath); folderBean.setFirstImgPath(path); } if(parentFile.list() == null) { continue; } //start 获取该文件夹下的图片文件数量 int picsSize = parentFile.list(new FilenameFilter() { @Override public boolean accept(File dir, String filename) { if(filename.endsWith(".jpg") ||filename.endsWith(".jpeg") ||filename.endsWith(".png")) { return true; } return false; } }).length; //end 获取该文件夹下的图片文件数量 folderBean.setCount(picsSize); mFolderBeans.add(folderBean); //start 设置图片文件最多的文件夹为当前文件夹 if(picsSize > mMaxCount){ mMaxCount = picsSize; mCurrentDir = parentFile; } } //end 设置图片文件最多的文件夹为当前文件夹 cursor.close(); mDirPath = null; mHandler.sendEmptyMessage(0x110); } }.start(); }
4.2 更新UI
private void data2View() { if(mCurrentDir == null){ Toast.makeText(this,"没有扫描到图片",Toast.LENGTH_LONG).show(); return; } //start 当前文件夹的图片文件 mImgs = Arrays.asList(mCurrentDir.list(new FilenameFilter() { @Override public boolean accept(File dir, String filename) { if(filename.endsWith(".jpg") ||filename.endsWith(".jpeg") ||filename.endsWith(".png")) { return true; } return false; } })); //end 当前文件夹的图片文件 if(null==mAdapter){ mAdapter = new ImageAdapterAdapter(this,mImgs,mCurrentDir.getAbsolutePath()); } mGridView.setAdapter(mAdapter); mDirCount.setText(mMaxCount + ""); mDirName.setText(mCurrentDir.getName()); }
4.3 弹出选择文件夹的窗口的初始化
/** * 初始化popupwindow */ private void initPop() { Log.i(TAG,"mFolderBeans = "+mFolderBeans); mPop = new ListImageDirPopupWindow(this,mFolderBeans); mPop.setAnimationStyle(R.style.dir_popupwindow_anim); mPop.setOnDismissListener(new PopupWindow.OnDismissListener() { @Override public void onDismiss() { lightOn(); } }); //start设置弹出窗口图片路径选择回调监听 mPop.setOnDirSelectedListener(new ListImageDirPopupWindow.OnDirSelectedListener() { @Override public void onDirSelected(FolderBean folderBean) { if(null != folderBean) { mCurrentDir = new File(folderBean.getDir()); //选中文件路径 mImgs = Arrays.asList(mCurrentDir.list(new FilenameFilter() { //文件路径中图片路径 @Override public boolean accept(File dir, String filename) { if(filename.endsWith(".jpg") ||filename.endsWith(".jpeg") ||filename.endsWith(".png")) { return true; } return false; } })); //start 刷新图片gridView if(null==mAdapter){ mAdapter = new ImageAdapterAdapter(MainActivity1.this,mImgs,mCurrentDir.getAbsolutePath()); mGridView.setAdapter(mAdapter); }else { mAdapter.setDirPath(mCurrentDir.getAbsolutePath()); mAdapter.setSourceData(mImgs); mAdapter.notifyDataSetChanged(); } //end 刷新图片gridView mDirCount.setText(mImgs.size() + ""); //文件中图片的数量 mDirName.setText(mCurrentDir.getName()); //文件名 } mPop.dismiss(); } }); //end 设置弹出窗口图片路径选择回调监听 }
5加载网络图片MainActivity2
首先活动网络图片的链接再更新UI显示图片
5.1 获取网络加载的Url
所有的网络图片的Url都在Images类中,所有的链接约有3000多张,有些链接可能已经失效,当然你也可以自己抓取百度的图片里面的图片,我是用Chrome浏览器的一个插件“小乐图客”来抓取的。
/** * 获取图片链接 * @param num * @return */ private List<String> getUrlList(int num) { List<String> urlList = null; if(num < 0) { //所有链接 urlList = Arrays.asList(imageThumbUrls); } if(num == 0) { //无链接 urlList = new LinkedList<String>(); } if(num >0) { //根据数量 if(num < imageThumbUrls.length) { urlList = new LinkedList<String>(); for(int i=0 ;i<num; i++) { urlList.add(imageThumbUrls[i]); } } else { urlList = Arrays.asList(imageThumbUrls); } } return urlList; }
5.2 更新UI
private void bindGvData2(){ mAdapter = new ImageAdapter2(this, mGv,getUrlList(-1)); mGv.setAdapter(mAdapter); }
本文结束,谢谢各位阅读,如有错误请指出,谢谢!
参考资料:
http://blog.csdn.net/lmj623565791/article/details/41874561
http://blog.csdn.net/guolin_blog/article/details/28863651
http://blog.csdn.net/xiaanming/article/details/9825113
http://my.oschina.net/jeffzhao/blog/80900
源码下载链接:
http://download.csdn.net/detail/luoshishou/9508282