前言:
这方面的文章,其实网上挺多的,但是都太乱,需要自己整理,或者不太适合,这篇文章,我主要是讲软引用,用在图片的下载,加载,本地缓存。
由于手机的内存有限,不能将所有图标都加载到内存中,而软引用的好处在于:
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体验性好。