上一篇我们了解了android中几种解决OOM的方法,下面总结下:
1.使用Bitmap.Options对图片进行适度的缩放
2.图片使用完后,记得将图片置为null,并recycle
3.如果图片是显示到listView或GridView等控件上,应该使用ViewHolder+ConvertView的方式重用View对象
4.使用LruCache对图片进行内存缓存
5.使用文件缓存
----------------------------------------------
本篇将综合使用上述方式来实现一个小案例,异步加载大量图片。
-----------------------------------------------
1.首先建立一个数据源,存放图片的网址,这里采用tomcat作为服务器:
package cn.edu.chd.datasource;
/**
* @author Rowand jj
*提供图片资源路径的类
*/
public class Images
{
public static final String[] imageThumbUrls = {
"http://1.83.188.190:8080/1.jpg",
... ...
"http://1.83.188.190:8080/28.jpg",
"http://1.83.188.190:8080/29.bmp",
};
}
2.
对Bitmap进行缩放的工具类:
package cn.edu.chd.utils;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
/**
* @author Rowand jj
* 压缩图片
*/
public class BitmapUtils
{
/**
* 根据资源id获取到图片,并进行压缩
* @param res
* @param resId
* @param reqWidth
* @param reqHeight
* @return
*/
public static Bitmap decodeSampledBitmapFromResource(Resources res,
int resId, int reqWidth, int reqHeight)
{
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, opts);
int inSampleSize = cacluateInSampleSize(opts, reqWidth, reqHeight);
opts.inSampleSize = inSampleSize;
opts.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeResource(res, resId, opts);
return bitmap;
}
/**
* 从byte数组中获取图片并压缩
* @param data
* @param reqWidth
* @param reqHeight
* @return
*/
public static Bitmap decodeSampledBitmapFromByteArray(byte[] data,
int reqWidth, int reqHeight)
{
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(data, 0, data.length, opts);
int inSampleSize = cacluateInSampleSize(opts, reqWidth, reqHeight);
opts.inJustDecodeBounds = false;
opts.inSampleSize = inSampleSize;
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length,
opts);
return bitmap;
}
private static int cacluateInSampleSize(BitmapFactory.Options opts,
int reqWidth, int reqHeight)
{
if (opts == null)
return 1;
int inSampleSize = 1;
int realWidth = opts.outWidth;
int realHeight = opts.outHeight;
if (realHeight > reqHeight || realWidth > reqWidth)
{
int heightRatio = realHeight / reqHeight;
int widthRatio = realWidth / reqWidth;
inSampleSize = (heightRatio > widthRatio) ? widthRatio
: heightRatio;
}
return inSampleSize;
}
}
3.使用lrucache对bitmap进行内存缓存的类
package cn.edu.chd.utils;
import android.graphics.Bitmap;
import android.support.v4.util.LruCache;
import android.util.Log;
/**
* @author Rowand jj
*
*使用lrucache缓存图片到内存,做成了单例模式
*/
public class BitmapLruCacheHelper
{
private static final String TAG = null;
private static BitmapLruCacheHelper instance = new BitmapLruCacheHelper();
private LruCache<String,Bitmap> cache = null;
private BitmapLruCacheHelper()
{
int maxSize = (int) (Runtime.getRuntime().maxMemory()/8);
cache = new LruCache<String, Bitmap>(maxSize)
{
@Override
protected int sizeOf(String key, Bitmap value)
{
return value.getRowBytes()*value.getHeight();
}
};
}
/**
*加入缓存
* @param key
* @param value
*/
public void addBitmapToMemCache(String key,Bitmap value)
{
if(key == null || value == null)
{
return;
}
if(cache!=null && getBitmapFromMemCache(key)==null)
{
cache.put(key, value);
Log.i(TAG,"put to lrucache success");
}
}
/**
* 从缓存中获取图片
* @param key
* @return
*/
public Bitmap getBitmapFromMemCache(String key)
{
if(key == null)
{
return null;
}
Bitmap bitmap = cache.get(key);
Log.i(TAG,"from lrucache,bitmap="+bitmap);
return bitmap;
}
/**
* 获取实例
* @return
*/
public static BitmapLruCacheHelper getInstance()
{
return instance;
}
}
4.文件缓存的类
package cn.edu.chd.utils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Comparator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.os.StatFs;
import android.util.Log;
/**
* @author Rowand jj
*
*文件缓存
*/
public class FileCacheUtils
{
/**
*图片缓存的相对路径
*/
private static final String IMG_CACH_DIR = "/imgCache";
/**
* 手机缓存目录
*/
private static String DATA_ROOT_PATH = null;
/**
* sd卡根目录
*/
private static String SD_ROOT_PATH = Environment.getExternalStorageDirectory().getAbsolutePath();
/**
*缓存的扩展名
*/
private static final String CACHE_TAIL = ".cach";
/**
* 最大缓存空间,单位是mb
*/
private static final int CACHE_SIZE = 4;
/**
* sd卡内存低于此值时将会清理缓存,单位是mb
*/
private static final int NEED_TO_CLEAN = 10;
/**
* 上下文
*/
private Context context;
private static final String TAG = "BitmapFileCacheUtils";
public FileCacheUtils(Context context)
{
this.context = context;
DATA_ROOT_PATH = context.getCacheDir().getAbsolutePath();
}
/**
* 从缓存中获取一张图片
*/
public Bitmap getBitmapFromFile(String key)
{
if(key==null)
{
return null;
}
String filename = getCacheDirectory()+File.separator+convertKeyToFilename(key);
File file = new File(filename);
if(file.exists())
{
Bitmap bitmap = BitmapFactory.decodeFile(filename);
if(bitmap == null)
{
file.delete();
}
else
{
updateFileModifiedTime(filename);
Log.i(TAG,"get file from sdcard cache success...");
return bitmap;
}
}
return null;
}
/**
* 将图片存入文件缓存
*/
public void addBitmapToFile(String key,Bitmap bm)
{
if(bm == null || key == null)
{
return;
}
//视情况清除部分缓存
removeCache(getCacheDirectory());
String filename = convertKeyToFilename(key);
File dir = new File(getCacheDirectory());
if(!dir.exists())
{
dir.mkdirs();
}
File file = new File(dir, filename);
try
{
OutputStream out = new FileOutputStream(file);//这里需要注意,如果指定目录不存在,应该先调用mkdirs生成目录,否则可能创建文件失败
bm.compress(CompressFormat.JPEG,100, out);
out.close();
Log.i(TAG,"add file to sdcard cache success...");
} catch (Exception e)
{
e.printStackTrace();
}
}
/**
* 获取文件缓存路径
* @return
*/
private String getCacheDirectory()
{
String cachePath = null;
if(isSdcardAvailable())
{
cachePath = SD_ROOT_PATH+IMG_CACH_DIR;
}else
{
cachePath = DATA_ROOT_PATH+IMG_CACH_DIR;
}
return cachePath;
}
/**
*
* 清除40%的缓存,这些缓存被删除的优先级根据近期使用时间排列,越久没被使用,越容易被删除
*/
private void removeCache(String dirPath)
{
File dir = new File(dirPath);
File[] files = dir.listFiles();
if(files == null)
{
return;
}
double total_size = 0;
for(File file : files)
{
total_size+=file.length();
}
total_size = total_size/1024/1024;
if(total_size > CACHE_SIZE || getSdCardFreeSpace() <= NEED_TO_CLEAN)
{
Log.i(TAG,"remove cache from sdcard cache...");
int removeFactor = (int) (files.length*0.4);
Arrays.sort(files, new FileLastModifiedComparator());
for(int i = 0; i < removeFactor; i++)
{
files[i].delete();
}
}
}
/**
*获取sd卡可用空间
*/
private int getSdCardFreeSpace()
{
StatFs stat = new StatFs(Environment.getExternalStorageDirectory().getPath());
double freespace = stat.getAvailableBlocks()*stat.getBlockSize();
return (int) (freespace/1024/1024);
}
/**
*判断sd卡是否可用
* @return
*/
private boolean isSdcardAvailable()
{
return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
}
/**
* 将关键字转化为文件名
*/
private String convertKeyToFilename(String key)
{
if(key == null)
{
return "";
}
return key.hashCode()+CACHE_TAIL;
}
/**
* 更新文件最后修改时间
*/
private void updateFileModifiedTime(String path)
{
File file = new File(path);
file.setLastModified(System.currentTimeMillis());
}
private class FileLastModifiedComparator implements Comparator<File>
{
@Override
public int compare(File lhs, File rhs)
{
if(lhs.lastModified() > rhs.lastModified())
{
return 1;
}else if(lhs.lastModified() == rhs.lastModified())
{
return 0;
}else
{
return -1;
}
}
}
}
5.一个图片下载器的类
package cn.edu.chd.myimageloader;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Message;
import cn.edu.chd.utils.BitmapLruCacheHelper;
import cn.edu.chd.utils.BitmapUtils;
import cn.edu.chd.utils.FileCacheUtils;
/**
* @author Rowand jj
*下载图片的工具类
* 通过downloadImage方法下载图片,并将图片保存到缓存中(使用线程池)。对下载得到的图片交由一个回调接口OnImageDownloadListener处理
* 通过showCacheImage方法获取缓存中的图片
*/
public class ImageDownloader
{
/**
* 下载image的线程池
*/
private ExecutorService mImageThreadPool = null;
/**
* 文件缓存的工具类
*/
private FileCacheUtils fileCacheUtils = null;
/**
* 线程池中线程的数量
*/
private static final int THREAD_NUM = 2;
/**
* 缩略图的宽
*/
private static final int REQ_WIDTH = 90;
/**
* 缩略图的高
*/
private static final int REQ_HEIGHT = 90;
protected static final int DOWNLOAD = 1;
private Context context;
/**
* 构造器
* @param context
*/
public ImageDownloader(Context context)
{
this.context = context;
fileCacheUtils = new FileCacheUtils(context);
}
/**
* 下载一张图片,先从内存缓存中找,如果没有则去文件缓存中找,如果还没有就从网络中下载
* @param url
* @param listener
* @return
*/
public Bitmap downloadImage(final String url,final OnImageDownloadListener listener)
{
final String subUrl = url.replaceAll("[^\\w]", "");
Bitmap bitmap = showCacheBitmap(subUrl);
if(bitmap!=null)//缓存中找到
{
return bitmap;
}else//缓存中未找到,则开启线程下载
{
// new AsyncTask<String, Void, Bitmap>()
// {
// @Override
// protected Bitmap doInBackground(String... params)
// {
// Bitmap bitmap = getImageFromUrl(url);//从网络上下载图片
// fileCacheUtils.addBitmapToFile(subUrl,bitmap);//加到文件缓存
// BitmapLruCacheHelper.getInstance().addBitmapToMemCache(subUrl, bitmap);//加到内存缓存
// return bitmap;
// }
// protected void onPostExecute(Bitmap result)
// {
// listener.onImageDownload(url, result);
// }
// }.execute(url);
final Handler handler = new Handler()
{
@Override
public void handleMessage(Message msg)
{
if(msg.what == DOWNLOAD)
{
listener.onImageDownload(url,(Bitmap)msg.obj);//对下载后的图片的操作交由listener实现类处理
}
}
};
getThreadPool().execute(new Runnable()//从线程池中获取一个线程执行下载操作并将下载后的图片加到文件缓存和内存缓存
{
@Override
public void run()
{
Bitmap bitmap = getImageFromUrl(url);//从网络上下载图片
Message msg = Message.obtain(handler, DOWNLOAD, bitmap);
msg.sendToTarget();//发送消息
//加到缓存中
fileCacheUtils.addBitmapToFile(subUrl,bitmap);
BitmapLruCacheHelper.getInstance().addBitmapToMemCache(subUrl, bitmap);
}
});
}
return null;
}
/**
* 显示缓存中的图片
* @param url
* @return
*/
public Bitmap showCacheBitmap(String url)
{
Bitmap bitmap = BitmapLruCacheHelper.getInstance().getBitmapFromMemCache(url);
if(bitmap!=null)//首先从内存缓存中找
{
return bitmap;
}else
{
bitmap = fileCacheUtils.getBitmapFromFile(url);
if(bitmap!=null)//在文件缓存中找到
{
BitmapLruCacheHelper.getInstance().addBitmapToMemCache(url, bitmap);//加入内存缓存
return bitmap;
}
}
return null;
}
/**
* 获取线程池实例
*/
public ExecutorService getThreadPool()
{
if (mImageThreadPool == null)
{
synchronized (ExecutorService.class)
{
if (mImageThreadPool == null)
{
mImageThreadPool = Executors.newFixedThreadPool(THREAD_NUM);
}
}
}
return mImageThreadPool;
}
/**
* 从url中获取bitmap
* @param url
* @return
*/
public Bitmap getImageFromUrl(String url)
{
HttpURLConnection conn = null;
try
{
URL target = new URL(url);
conn = (HttpURLConnection) target.openConnection();
conn.setReadTimeout(3000);
conn.setConnectTimeout(10 * 1000);
conn.setDoInput(true);
if (conn.getResponseCode() == 200)
{
InputStream is = conn.getInputStream();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
int len = 0;
byte[] buf = new byte[1024];
while((len = is.read(buf))!=-1)
{
bout.write(buf, 0, len);
}
is.close();
byte[] data = bout.toByteArray();
return BitmapUtils.decodeSampledBitmapFromByteArray(data,REQ_WIDTH, REQ_HEIGHT);//返回的是压缩后的缩略图
}
} catch (Exception e)
{
e.printStackTrace();
}
return null;
}
/**
* 取消当前的任务
*/
public synchronized void cancellTask()
{
if(mImageThreadPool != null)
{
mImageThreadPool.shutdownNow();
mImageThreadPool = null;
}
}
/**
*操作下载后的图片的回调接口
*/
public interface OnImageDownloadListener
{
void onImageDownload(String url,Bitmap bitmap);
}
}
6.GridView的适配器:
当GridView滑动时停止下载图片,GridView停止滑动时下载图片。
package cn.edu.chd.myimageloader;
import android.content.Context;
import android.graphics.Bitmap;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import cn.edu.chd.myimageloader.ImageDownloader.OnImageDownloadListener;
public class ImageAdapter extends BaseAdapter implements OnScrollListener
{
private GridView gridView;
private Context context;
private String[] imageThumUrls;
private ImageDownloader mImageDownloader;
private boolean isFirstEnter = true;
private int mFirstVisibleItem;
private int mVisibleItemCount;
public ImageAdapter(Context context,String[] imageThumUrls,GridView gridView)
{
this.context = context;
this.gridView = gridView;
this.imageThumUrls = imageThumUrls;
this.mImageDownloader = new ImageDownloader(context);
gridView.setOnScrollListener(this);
}
@Override
public int getCount()
{
return imageThumUrls.length;
}
@Override
public Object getItem(int position)
{
return imageThumUrls[position];
}
@Override
public long getItemId(int position)
{
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
ImageView mImageView;
String imageUrl = imageThumUrls[position];
if(convertView == null)
{
mImageView = new ImageView(context);
}else
{
mImageView = (ImageView) convertView;
}
mImageView.setLayoutParams(new GridView.LayoutParams(90,90));
mImageView.setTag(imageUrl);
//只显示缓存图片,如果缓存中没有则设置一张默认的图片
Bitmap bitmap = mImageDownloader.showCacheBitmap(imageUrl.replaceAll("[^\\w]",""));
if(bitmap != null)
{
mImageView.setImageBitmap(bitmap);
}else
{
mImageView.setImageResource(R.drawable.ic_launcher);
}
return mImageView;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState)
{
if(scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE)//滑动停止时启动下载图片
{
showImage(mFirstVisibleItem, mVisibleItemCount);
}else
{
cancellTask();
}
}
/**
* 滚动时执行此方法
* 第一次进入会调用showImage显示图片
* */
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount)
{
mFirstVisibleItem = firstVisibleItem;
mVisibleItemCount = visibleItemCount;
if(isFirstEnter && visibleItemCount>0)
{
showImage(firstVisibleItem, visibleItemCount);
isFirstEnter = false;
}
}
/**
* 显示图片,先从缓存中找,如果没找到就开启线程下载
* @param firstVisibleItem 第一个可见项的id
* @param visibleItemCount 可见项的总数
*/
private void showImage(int firstVisibleItem,int visibleItemCount)
{
for(int i = firstVisibleItem; i < firstVisibleItem+visibleItemCount;i++)
{
String mImageUrl = imageThumUrls[i];
final ImageView mImageView = (ImageView) gridView.findViewWithTag(mImageUrl);
mImageDownloader.downloadImage(mImageUrl, new OnImageDownloadListener()
{
@Override
public void onImageDownload(String url, Bitmap bitmap)
{
if(mImageView != null && bitmap!=null)
{
mImageView.setImageBitmap(bitmap);//下载后直接设置到view对象上
}
}
});
}
}
/**
* 取消下载任务
*/
public void cancellTask()
{
mImageDownloader.cancellTask();
}
}
7MainActivity
package cn.edu.chd.myimageloader;
import cn.edu.chd.datasource.Images;
import android.app.Activity;
import android.os.Bundle;
import android.widget.GridView;
public class MainActivity extends Activity
{
private GridView gridView = null;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
gridView = (GridView) findViewById(R.id.gridView);
gridView.setAdapter(new ImageAdapter(this, Images.imageThumbUrls, gridView));
}
}
8.布局:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<GridView
android:id="@+id/gridView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:cacheColorHint="@android:color/transparent"
android:columnWidth="90dip"
android:horizontalSpacing="5dip"
android:numColumns="2"
android:verticalSpacing="5dip" >
</GridView>
</RelativeLayout>
9.权限:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
大功告成~