需求分析:
Android的App中加载网络图片是个非常常用的操作,再加上很多情况下,同一个图片极有可能被程序加载很多次。而多次重复的对同一个URL下的图片进行加载,这无疑会增大很多的流量开销,并且用户体验也非常不好。
解决方式:
总体来说分为三步:
1. 当需要加载一张图片时,首先判断程序缓存(以下用AppCache代替)中是否有该张图片,若有,则继续直接取得,否则下一步
2. AppCache中没有该张图片时就开始向文件缓存(以下用FileCache代替)中查找,若有,取之并向AppCache中添加该图片,否则下一步
3. FileCache中也没有时,就开始根据图片的URL来从网上获取(通过类ImageHttp),若有,取之并先向FileCache中添加再向AppCache中添加,否则返回null(此时一般在程序内置一张图片代替,可手动添加,此处略)
以上说的比较粗略,详情看一下代码,注释很详细:
1. 首先看一下关于URL的加密,其中加密的方式有很多,所以在这里为了日后的拓展,就写一个简单的接口,并用自己的MD5加密方式实现了这个接口
接口OnCode
package com.example.image.code;
/**
* @author wzj
* @version 2014-12-10 上午10:44:48
*/
public interface OnCode {
/**
* @param message
* @return 加密的信息
*/
String encode(String message);
/**
* @param message
* @return 解密的信息
*/
String decode(String message);
}
MD5类,实现了该接口
package com.example.image.code;
import com.ndktools.javamd5.Mademd5;
/**
* @author wzj
* @version 2014-12-10 上午10:46:20
*/
public class MD5 implements OnCode {
@Override
public String encode(String message) {
Mademd5 md = new Mademd5();
return md.toMd5(message);
}
@Override
public String decode(String message) {
return null;
}
}
2. 加载图片时用到的回调接口OnBitmap,使用时需要实现该接口即可在接口的方法中得到加载的图片
package com.example.image.util.image.dao;
import android.graphics.Bitmap;
/**
* @author wzj
* @version 2014-12-10 上午11:13:21
*/
public interface OnBitmap {
/**
* 加载结束时回调该方法,该方法是在主线程中执行的,异常或出错时 image为 null
* @param image
*/
void onBitmap(Bitmap image);
}
3. 程序缓存AppCache类,使用了Java中的软引用SoftReference,既能存储数据也不影响Java垃圾回收器的回收
package com.example.image.util.image;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import android.graphics.Bitmap;
/**
* SoftReference: 软引用,可以保存数据,也不妨碍垃圾回收器的回收
* @author wzj
* @version 2014-12-9 下午12:39:50
*/
public final class AppCache {
private static HashMap<String, SoftReference<Bitmap>> data = new HashMap<String, SoftReference<Bitmap>>();
public static void setImage(String url, Bitmap bitmap) {
if(url == null || bitmap == null){
return;
}
if(!data.containsKey(url) || (data.containsKey(url) && data.get(url).get() == null)){
data.put(url, new SoftReference<Bitmap>(bitmap));
}
}
public static Bitmap getImage(String url) {
return data.containsKey(url) ? data.get(url).get() : null;
}
}
4. 文件缓存FileCache类,包含读写图片和清除缓存三个方法
<pre name="code" class="java">package com.jandar.wzj.http.image;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import com.jandar.wzj.http.image.code.OnCode;
import com.jandar.wzj.util.L;
/**
* 关于URL加密的必要性:
* 1. 文件名暴露了图片的网络资源的URL,一般来讲是不安全的
* 2. URL中包含有"/"、":"等文件命名不支持的字符
* @author wzj
* @version 2014-12-9 下午12:21:53
*/
public class FileCache {
private String path;
private OnCode onCode;
public FileCache(String path, OnCode onCode) {
this.path = path;
this.onCode = onCode;
}
// 从本地存储图片
public void setImage(String url, InputStream imageIs) {
File dir = new File(path);
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(dir, onCode.encode(url));
if (file.exists()) {
/**
* 此处,可能同一个url但是网络资源发生改变
*/
new L("该图片已在SD卡中,不需要重复写入..");
return;
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int index = -1;
while ((index = imageIs.read(buffer)) != -1) {
fos.write(buffer, 0, index);
}
new L("SD卡写入成功");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(fos != null){
fos.close();
}
if(imageIs != null){
imageIs.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
// 从本地读取图片
public Bitmap getImage(String url) {
File dir = new File(path);
//若文件目录不存在,则返回null
if(!dir.exists()){
return null;
}
File file = new File(path, onCode.encode(url));
if (file.exists()) {
try {
Bitmap image = BitmapFactory.decodeFile(file.getPath());
// 此时再次向程序缓存存入数据
AppCache.setImage(url, image);
return image;
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
/**
* 清除文件缓存
* @return
*/
public boolean clearFileCache() {
try {
File dir = new File(path);
dir.delete();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
5. 网络加载ImageHttp类,在网络中读取文件,有一点要明白,关于网络加载的InputStream不可重复使用(原因暂不明)
<pre name="code" class="java">package com.jandar.wzj.http.image;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Timer;
import java.util.TimerTask;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import com.jandar.wzj.http.image.dao.OnBitmap;
import com.jandar.wzj.util.L;
/**
* @author wzj
* @version 2014-12-9 下午2:57:44
*/
public class ImageHttp {
/**
* 通过网络加载的方式中,当加载成功时,会向文件缓存存入数据,所以该类中要有与之对应的FileCache类对象的引用
*/
private FileCache fileCache;
public ImageHttp() {
}
public ImageHttp(FileCache fileCache) {
this.fileCache = fileCache;
}
public void loadImage(String url, final int timeOut, final OnBitmap listener) {
new LoadByHttp(timeOut, listener).execute(url);
}
private class LoadByHttp extends AsyncTask<String, Void, Bitmap> {
int timeOut;
OnBitmap listener;
boolean isFinished = false;
Timer timer;
LoadByHttp(final int timeOut, final OnBitmap listener) {
this.timeOut = timeOut;
this.listener = listener;
this.timer = new Timer();
}
@Override
protected Bitmap doInBackground(String... params) {
timer.schedule(new TimerTask() {
@Override
public void run() {
// 超时处理
if (!isFinished) {
isFinished = true;
listener.onBitmap(null);
LoadByHttp.this.cancel(true);
}
}
}, timeOut);
String imageUrl = params[0];
InputStream is = null;
ByteArrayOutputStream baos = null;
try {
// is只能用一次,切记!!!
is = new URL(imageUrl).openStream();
Bitmap image;
if(fileCache != null){
// 存入本地缓存
fileCache.setImage(imageUrl, is);
image = fileCache.getImage(imageUrl);
} else {
image = BitmapFactory.decodeStream(is);
}
new L(image == null ? "image为null" : "image不为null");
// 存入程序缓存
AppCache.setImage(imageUrl, image);
return image;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (baos != null) {
baos.close();
}
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
@Override
protected void onPostExecute(Bitmap result) {
if (!isFinished) {
isFinished = true;
listener.onBitmap(result);
timer.cancel();
}
}
}
}
6. 面向开发者的最终类ImageLoader,包含了三中处理方式类的引用
package com.example.image.util.image;
import android.graphics.Bitmap;
import com.example.image.code.MD5;
import com.example.image.code.OnCode;
import com.example.image.util.L;
import com.example.image.util.image.dao.OnBitmap;
/**
* 加载超时时间的三种获取策略:
* 1. 若调用本类的loadImage方法时含有timeOut参数,则取该参数
* 2. 若调用loadImage不含该参数,而构造方法中含该参数,则取该参数
* 3. 若loadImage方法和构造方法都不含有该参数时,则取该类中的静态常量 TIME_OUT
* @author wzj
* @version 2014-12-9 上午9:36:37
*/
public class ImageLoader {
private FileCache fileCache;
private ImageHttp imageHttp;
private int timeOut;
// 默认超时时间是20秒
private static final int TIME_OUT = 20000;
/**
* 不开启文件缓存
*/
public ImageLoader(){
this(TIME_OUT);
}
/**
* 不开启文件缓存
*/
public ImageLoader(int timeOut){
this.timeOut = timeOut;
imageHttp = new ImageHttp();
}
/**
* 开启文件缓存
* @param path: 缓存存放的路径
*/
public ImageLoader(String path) {
this(path, TIME_OUT);
}
/**
* 开启文件缓存
* @param path: 缓存存放的路径
* @param timeOut: 设置默认超时时间
*/
public ImageLoader(String path, int timeOut) {
this.timeOut = timeOut;
fileCache = new FileCache(path, getCode());
imageHttp = new ImageHttp(fileCache);
}
/**
* 可重写该方法来改变加密方式
* @return
*/
protected OnCode getCode() {
return new MD5();
}
/**
* 加载图片,加载结果会在接口中回调
* @param url
* @param listener
*/
public void loadImage(String url, OnBitmap listener){
loadImage(url, timeOut, listener);
}
/**
* 加载图片,加载结果会在接口中回调
* @param url
* @param listener
* @param timeOut 设置此次加载的超时时间
*/
public void loadImage(String url, int timeOut, OnBitmap listener) {
Bitmap image;
// 1. 读取程序缓存
if ((image = AppCache.getImage(url)) != null) {
listener.onBitmap(image);
new L("程序缓存取得的图片");
return;
}
// 2. 读取文件缓存,并向程序缓存存入数据
if (fileCache != null && (image = fileCache.getImage(url)) != null) {
listener.onBitmap(image);
new L("SD卡缓存取得的图片");
return;
}
// 3. 读取网络资源,并向文件缓存和程序缓存存入数据
imageHttp.loadImage(url, timeOut, listener);
new L("网络取得的图片");
}
/**
* 清除缓存
*/
public boolean clearFileCache() {
return fileCache == null ? false : fileCache.clearFileCache();
}
}<strong style="color: rgb(255, 0, 0);">
</strong>
7. 老规矩,测试类MainActivity
package com.example.image;
import android.app.Activity;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;
import com.example.image.util.image.ImageLoader;
import com.example.image.util.image.dao.OnBitmap;
public class MainActivity extends Activity {
// private EditText _edit;
private ImageView _image;
private Button _button;
private static final String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/cache";
private ImageLoader imageLoader;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageLoader = new ImageLoader(path);
// _edit = (EditText) findViewById(R.id.edit);
_image = (ImageView) findViewById(R.id.image);
_button = (Button) findViewById(R.id.button);
_button.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
final long pre = System.currentTimeMillis();
imageLoader.loadImage("http://www.baidu.com/img/bd_logo1.png", new OnBitmap() {
@Override
public void onBitmap(Bitmap image) {
if(image != null){
long time = System.currentTimeMillis() - pre;
_button.setText("用时: " + time + " 毫秒");
_image.setImageBitmap(image);
} else {
_button.setText("加载失败");
}
}
});
}
});
}
}<strong style="color: rgb(255, 0, 0);">
</strong>