首先是思路:
为什么要缓存图片?
:因为每次界面显示时都会从网络加载图片,如果已经加载过了,而还去加载图像的话,就会浪费客户的流量,流量就是钱,所有已经加载的图片不应该再次加载,所以要缓存起来,而且从缓存读取图片会速度很快;
为什么要压缩图片?
: 如果不对图像进行压缩,由于加载的图片过大,一:会导致程序反映过慢,进而影响客户的体验,二:有可能会让程序崩溃即内存溢出;
加载:
由于对listview进行了优化(即item 的复用),即当listview进行滑动时隐藏的item会在下面进行出现(不会出现新的item);而这时会出现item头像的乱闪,就想”动画”一样;所以需要加载缓存;
加载,缓存压缩图像的思路:
1.每个item都会发起一个加载任务请求,所以需要一个任务队列 taskQueue
2.需要一个取任务的Handler:pollHandler
3.当加载完成后需要一个更新UI的Handker:uiHandler
4.从时刻循环等待着取任务的一个线程:Thread:pollThread
5.因为i加载量可能比较大,需要线程池进行多线程并发执行加载任务
线程池:ExecutorService :exec;
6.任务队列发起网络加载任务;
7.加载完毕的图像进行压缩和缓存
/**
* 工具类:加载图像
* @author fz
*
*/
public class ImageLoader {
//下载完毕后,message里面的what
public static final int LOADER_FINISH=101;
public static final int NEW_TASK=102;
//线程池中的线程数量 (具体多少根据设备(几核手机)来定)
public static int Thread_count;
//上下文
public static Context context;
//内存缓存
public static LruCache<String, Bitmap> lcache;
//任务队列 (Deque:双向队列:未来可以根据需求来决定是FIFO还是LIFO)
public static LinkedBlockingDeque<Runnable> taskQueue;
//线程池
public static ExecutorService exec;
//从任务队列中取任务的handler
public static Handler pollHandler;
//任务下载完成后,用来刷新UI(listview)的 UI的Handler
public static Handler uihandler;
//"养活" Pollhandler
public static Thread pollThread;
//磁盘缓存
public static DiskLruCache diskcache;
//标示的作用,确保Imageloader 只进行一次初始化操作即可
public static boolean isFirstTime= true;
/**
* 初始化几个属性
* @param c
*/
public static void init(Context c){
if(!isFirstTime){
return;
}
context=c;
Thread_count=getNumberOfCores();
//初始化缓存空间
lcache=new LruCache<String, Bitmap>((int) (Runtime.getRuntime().maxMemory()/8)){
@Override
protected int sizeOf(String key, Bitmap value) {
// TODO Auto-generated method stub
return value.getHeight()*value.getRowBytes();
}
};
//初始化磁盘缓存
try {
diskcache=DiskLruCache.open(directory(), appVersion(), 1, 1024*1024*10);
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
//初始化任务队列
taskQueue=new LinkedBlockingDeque<Runnable>();
exec=Executors.newFixedThreadPool(Thread_count);
//初始化Handler
//pollhandler,uihandler
uihandler=new Handler(Looper.getMainLooper()){
@Override
public void handleMessage(Message msg) {
if(msg.what==101){
//TODO msg.obj 到底放什么?
//msg.obj 放什么能解决Listview的加载"动画"效果
ValueObject vo=(ValueObject) msg.obj;
ImageView iv=vo.iv;
String url=vo.url;
Bitmap b=vo.bitmap;
if(iv.getTag().toString().equals(url)){
iv.setImageBitmap(b);
}
}else{
super.handleMessage(msg);
}
}
};
/**
* 初始化工作线程("养活"pollHandler)
* 任务队列中只要有任务pollHandler就会取任务
*/
pollThread=new Thread(){
public void run() {
Looper.prepare();
pollHandler=new Handler(){
@Override
public void handleMessage(Message msg) {
if(msg.what==NEW_TASK){
try {
//现在任务队列中中被放入了新任务;
//去队列中去取任务
Runnable task = taskQueue.takeFirst();
//将取出的任务放到线程池中去执行
exec.execute(task);
} catch (Exception e) {
e.printStackTrace();
}
}else{
super.handleMessage(msg);
}
}
};
Looper.loop();
};
};
//取任务的线程启动
pollThread.start();
//表示 ,初始化完成了
isFirstTime=false;
}
/**
* 获得程序的版本号
* @return
*/
private static int appVersion() {
try {
PackageInfo info=context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return info.versionCode;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
/**
* 存储路径
* @return
*/
private static File directory() {
String path=context.getCacheDir().getPath();
return new File(path, "imageloadercache");
}
/**
* 获取url指向的图像,放到v中显示
* @param url 图像路径
* @param v 显示图像的imageview
*/
public static void loadImage(final String url,final ImageView iv){
//先判断是否已经初始化
if(isFirstTime){
throw new RuntimeException("ImageLoader未做初始化");
}
//将URL转为MD5格式的字符串
final String MD5=getMD5(url);
//设置Tag,到时候要与vo中 的url进行对比
iv.setTag(MD5);
Bitmap bm=lcache.get(MD5);
//先判断,URL所指向的图像是否在缓存中有保存
if(bm!=null){
Log.i("TAG", "图像时从内存中缓存加载的");
iv.setImageBitmap(bm);
return;
}
//从磁盘缓存中取出url对应的图片
try {
Snapshot snap=diskcache.get(MD5);
if(snap!=null){
Log.i("TAG", "图像时从磁盘缓存中取的");
InputStream is= snap.getInputStream(0);
bm=BitmapFactory.decodeStream(is);
//将从磁盘缓存中获取的图片放到内存缓存存储一份
lcache.put(MD5, bm);
iv.setImageBitmap(bm);
return;
}
} catch (IOException e1) {
e1.printStackTrace();
}
//从网络中下载,任务队列执行下载任务
taskQueue.add(new Runnable() {
@Override
public void run() {
// TODO 进行网络加载
//发起网络连接,获得图像资源
try{
URL u=new URL(url);
HttpURLConnection con=(HttpURLConnection) u.openConnection();
con.setRequestMethod("GET");
con.setDoInput(true);
con.connect();
InputStream is=con.getInputStream();
//要对图像进行压缩
Bitmap bm=compress(is,iv);//压缩图像的方法,自己创建的;
is.close();//关闭流,
//放入缓存中
lcache.put( MD5, bm);
//将压缩后的图像放到磁盘缓存中
Editor or=diskcache.edit(MD5);
OutputStream out =or.newOutputStream(0);
bm.compress(CompressFormat.JPEG, 100, out);
or.commit();
//写日志
diskcache.flush();
//封装item的TAG的实体类
ValueObject vo=new ValueObject(iv, MD5, bm);
//发消息,表示加载完成
Message.obtain(uihandler,LOADER_FINISH,vo).sendToTarget();
}catch(Exception e){
e.printStackTrace();
}
}
});
Message.obtain(pollHandler,NEW_TASK).sendToTarget();
}
/**
* 根据要显示的imageview的大小对图像进行压缩
* @param is 图像源
* @param iv 要显示图像的imageview
* @return 压缩后的图像
*/
protected static Bitmap compress(InputStream is, ImageView iv) {
Bitmap b=null;
try{
//1)先获得原始(图像源)图像的尺寸大小
//借助options 来获取图像的大小
//is------byte[]
ByteArrayOutputStream out=new ByteArrayOutputStream();
byte[] buff=new byte[4096];
int len=-1;
while((len=is.read(buff))!=-1){
out.write(buff,0,len);
}
Options opts=new Options();
//opts,一旦设置此属性true,BitmapFactory是不会有返回值得,是null;
opts.inJustDecodeBounds=true;
BitmapFactory.decodeByteArray(out.toByteArray(), 0, out.toByteArray().length, opts);
int width=opts.outWidth;//图像的宽度(像素数)
int height=opts.outHeight;//图像的高度(像素)
//2)获取希望的宽高值
int tagWidth=iv.getWidth();//imageview 的宽度
int tagHeight=iv.getHeight();//imageview 的高度
//如果imageview的宽高取不到
if(tagWidth==0||tagHeight==0){
//可以手动的指定值(80dp,100dp);
//拿到设备的屏幕的宽高
tagWidth=context.getResources().getDisplayMetrics().widthPixels;
tagHeight=context.getResources().getDisplayMetrics().heightPixels;
}
//3)计算压缩比
int sampleSize=1;
if(width*1.0/tagWidth>1||height*1.0/tagHeight>1){
sampleSize=(int) Math.ceil(Math.max(width*1.0/tagWidth, height*1.0/tagHeight));
}
//4) 压缩图像
opts.inSampleSize=sampleSize;
opts.inJustDecodeBounds=false;
b=BitmapFactory.decodeByteArray(out.toByteArray(), 0, out.toByteArray().length, opts);
out.close();
}catch(Exception e){
e.printStackTrace();
}
return b;
}
/**
* 使用MD5加密
* @param url 原字符串
* @return 把MD5格式转换成16进制的字符串
*/
private static String getMD5(String url) {
String res="";
try {
MessageDigest md=MessageDigest.getInstance("md5");
md.update(url.getBytes());
byte[] bs=md.digest();
StringBuilder sb=new StringBuilder();
for(byte b:bs){
String str=Integer.toHexString(b & 0xFF);
if(str.length()==1){
sb.append("0");
}
sb.append(str);
}
res=sb.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return res;
}
/**
* 线程池中的线程数量
* @return
*/
private static int getNumberOfCores() {
//判断手机cpu 是几核
File f=new File("/sys/devices/system/cpu/");
File[] fs=f.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String filename) {
if(filename.contains("cpu[0-9]")){
return true;
}else{
return false;
}
}
});
//数组的长度就是核数
if(fs.length>0){
return fs.length;
}else{
return 1;
}
}
/**
* 封装 item 的标签,即用url作为每个发起加载任务的
* imageview的Tag,用来标示是否已经加载
* @author fz
*
*/
private static class ValueObject{
ImageView iv;//发起下载任务的那个Imageview
String url;
Bitmap bitmap;
public ValueObject(ImageView iv, String url, Bitmap bitmap) {
super();
this.iv = iv;
this.url = url;
this.bitmap = bitmap;
}
}
}
其中增加了一点磁盘缓存的小知识:
为什么要使用磁盘缓存:
主要是可以减少网络的访问,节省流量,为客户着想;
什么时候会使用到磁盘缓存:
当程序完全关闭后,再次启动时,会使用;
*程序启动后读取图像的顺序:
内存(即缓存)如果没有–>外存(即SD卡)如果没有–>才会去从网络加载图像*
DiskLruCache(是个工具类,属于第三方工具类)
1.创建
DiskLruCache.open(
存储路径,
软件版本号,
1个key 对应几个value.//比如说一条新闻可以有几张图片
存储空间的大小);
2.存储
edit(key);方法,获得一个Editor对象,通过该Editor对象可以获得一个输出流,把你要保存的内容以流的形式
存储起来即可,当流操作完毕以后,要调用一下commit方法,进行最终的保存,保存后调用flush()打印一个日志,系统需求,
将来版本更新时会用到
3.读取
get(key);方法,获得一个Snap对象,通过该Snap对象获得一个输入流,对该输入流进行解析;即可获得DiskLruCache中key 所
对应的value
其中还有一段MD5的知识:
MD5是一种加密方式,有兴趣的可用度娘一下;
我还是一个Android的菜鸟,所以欢迎各位同行来进行交流及指点!