前言:
这方面的文章,其实网上挺多的,但是都太乱,需要自己整理,或者不太适合,这篇文章,我主要是讲软引用,用在图片的下载,加载,本地缓存。
由于手机的内存有限,不能将所有图标都加载到内存中,而软引用的好处在于:
1. 在内存足够时,所有的图片都在内存中;
2. 内存不足时,将释放掉当前页面不需要用到的图片内存,而只加载需要用到的,这样,就防止内存爆掉,导致手机Crash。
流程图:
实现:
AsyncImageLoader.java 实现图片的内存管理,本地缓存管理,下载管理,并通知需要显示图片的activity来显示:
private static HashMap<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>(); private static final int nThreadPoolSize = 5; private static ExecutorService mExecutorService = Executors.newFixedThreadPool(nThreadPoolSize); private OnImageLoadListener mImageLoadListener; private Handler mHandler;
先定义几个变量,imageCache即是一个HashMap的对图片的软引用, mExecutorService是一个线程池,避免过多的图片同时去下载显示,导致手机负载过重,影响性能,mImageLoadListener是一个回调函数接口,即谁注册,通知谁,谁来用。
public AsyncImageLoader(OnImageLoadListener l){ mImageLoadListener = l; mHandler = new Handler(){ @Override public void handleMessage(Message msg) { switch(msg.what){ case 0: String imageUrl = msg.getData().getString("imageUrl"); int position = msg.getData().getInt("position"); mImageLoadListener.ImageLoadFinished((Bitmap)msg.obj, imageUrl, position); break; default: break; } } }; }
初始化类,参数是回调接口,内部实现一个消息处理的handler。
public void loadBitmap(String imageUrl, final int position){ final String imgUrl = imageUrl; mExecutorService.execute(new Runnable(){ @Override public void run() { Bitmap bitmap = loadBitmapFromCache(imgUrl); if(bitmap == null){ bitmap = loadBitmapFromLocal(imgUrl); if(bitmap == null){ bitmap = loadBitmapFromUrl(imgUrl); if(bitmap == null){ imageCache.put(imgUrl, null); return; } } } Message msg = mHandler.obtainMessage(0, bitmap); Bundle bundle = new Bundle(); bundle.putString("imageUrl", imgUrl); bundle.putInt("position", position); msg.setData(bundle); mHandler.sendMessage(msg); return; } }); }
启动线程,完成:从内存、从本地缓存、从网络下载三步骤,并通知显示。
咱们来看看,这三个步骤是如何实现完成的
step1, 查找内存:
public Bitmap loadBitmapFromCache(String imageUrl){ Bitmap bm = null; if(imageCache.containsKey(imageUrl)){ SoftReference<Bitmap> softReference = imageCache.get(imageUrl); if(softReference != null){ bm = softReference.get(); if(bm == null){ imageCache.remove(imageUrl); } } } return bm; }
不在软引用,即内存中,返回NULL,如果在,看位图数据不在,去除URL,然后返回NULL;
step2, 查找本地缓存:
private Bitmap loadBitmapFromLocal(String imageUrl){ String filename = getFilenameFromUrl(imageUrl); File dir = new File(Environment.getExternalStorageDirectory(), "imifun"); if(dir.exists()){ File filepath = new File(dir, filename); Bitmap bm = BitmapFactory.decodeFile(filepath.getAbsolutePath()); if(bm != null){ if(!imageCache.containsKey(imageUrl)){ imageCache.put(imageUrl, new SoftReference<Bitmap>(bm)); } } } return loadBitmapFromCache(imageUrl); }
从SD卡指定的imifun目录下,找查以imageUrl为名字的图片,如果存在,加载至软引用,并由软引用返回通知UI,当然不存在,也交给软引用,反正是找不到的嘛;
step3, 网络下载:
private String getFilenameFromUrl(String imageUrl){ String [] strs = imageUrl.split("/"); String filename = strs[strs.length - 1]; if(filename.contains(".")){ filename = filename.substring(0, filename.lastIndexOf(".")); } return filename; }
private Bitmap loadBitmapFromUrl(String imageUrl){ URL m; InputStream i = null; try { m = new URL(imageUrl); HttpURLConnection connection = (HttpURLConnection) m.openConnection(); connection.setDoInput(true); connection.connect(); i = connection.getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] b = new byte[1024]; int len = 0; while ((len = i.read(b, 0, 1024)) != -1){ baos.write(b, 0, len); baos.flush(); } byte[] bytes = baos.toByteArray(); Bitmap bm=BitmapFactory.decodeByteArray(bytes,0,bytes.length); if(bm != null){ saveBitmap(bm, getFilenameFromUrl(imageUrl)); if(!imageCache.containsKey(imageUrl)){ imageCache.put(imageUrl, new SoftReference<Bitmap>(bm)); } } } catch (MalformedURLException e1) { e1.printStackTrace(); return null; } catch (IOException e) { e.printStackTrace(); return null; } return loadBitmapFromCache(imageUrl); }
这里采用HTTP方式下载,没什么好说的,大家都会,下载好了后,保存至本地缓存,但这里要说下,就是先将图片采用流的方式保存至数组,然后在写文件。为啥要这么麻烦?哎,直接保存,有时候保存的图片不正确,无法打开,因为文件没有EOF结束符,但是,采用流的方式,可以保证有EOF。
最后,给出保存的函数吧:
private void saveBitmap(Bitmap bitmap, String filename){ if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){ File dir = new File(Environment.getExternalStorageDirectory(), "imifun"); if(!dir.exists()){ dir.mkdirs(); } File file = new File(dir, filename); FileOutputStream fos; if(!file.exists()){ try { file.createNewFile(); fos = new FileOutputStream(file); if(bitmap.compress(Bitmap.CompressFormat.PNG, 70, fos)){ fos.flush(); } fos.close(); } catch (IOException e) { e.printStackTrace(); } } } }
保存,与读取本地缓存相反,在SD卡上,创建以imageUrl为名字的文件,将数据流写进去,即OK。
最后,给出interface定义:
public interface OnImageLoadListener{ public void ImageLoadFinished(Bitmap bitmap, String imageUrl, int position); }
在需要使用软引用的地方,实现这个接口中的方法即可。
例子:
1. 初始化异步图片加载类:
private void initAsycImageLoader(){ mImageUrls = new ArrayList<String>(); mImageLoad = new AsyncImageLoader(new OnImageLoadListener(){ @Override public void ImageLoadFinished(Bitmap bitmap, String imageUrl, int position) { if(mImageUrls.contains(imageUrl)){ ImageView iv = (ImageView) rootView.findViewWithTag(position); if(iv != null && bitmap != null && bitmap.isRecycled() == false){ iv.setImageBitmap(bitmap); iv.setScaleType(ScaleType.FIT_XY); } } } }); }
2. 调用
private void setImageViewBg(ImageView iv, String imgUrl, int position, int defaultResImg){ Bitmap bitmap = mImageLoad.loadBitmapFromCache(imgUrl); iv.setTag(position); mImageUrls.add(imgUrl); if(bitmap != null && bitmap.isRecycled() == false){ iv.setImageBitmap(bitmap); iv.setScaleType(ScaleType.FIT_XY); }else{ iv.setImageResource(defaultResImg); mImageLoad.loadBitmap(imgUrl, position); } }
position,因为是在listview中显示,所以,将posotion设置为imageview的tag,方便回调回来时,查找。
总结:
AsyncImageLoader实现就这么多,不过,这个类还可以改进,实现更高的效率:
1. 通常,我们请求服务器获取一堆数据,比如类似现在的新闻,10条json数据,那么,我们可以一次性进图片放进ArrayList中,并传给异步加载类;
2. 异步加载类,就开始一个接一个的去下载,完成后返回数据,通知在该显示的地方显示;
这里,还有个小问题,不知道大家看出来了没有?
Android在图片加载到内存,是有个限制的,一次性加载到内存的图片大小不能超过8MB,否则就会Crash。当然,我们的后台一般都不会这么做,但是,如果某个带相册图片浏览的应用,全屏浏览单张图片,而这张图片是由于后台的编辑,不小心传了张大于8MB的,呃,可想而之的后果。。
网上有解决办法,大家可以找找,我这给个提示:先读取图片的宽,高属性,判断是否超过8MB,如果超过,我们可以取这张图的缩略图,然后在显示,或者显示部分,当然啦,这得需求,或用户来决定哪种方法好,或者UI体验性好。