在移动设备上,用户访问网络需要消耗宝贵的流量,因此缓存策略就变得尤为重要。例如当用户第一次从网络加载图片后,就将其缓存到存储设备上,这样当下一次使用到这张图片时就不再从网络中获取。很多时候往往还在内存中缓存一份。这样,当用户加载图片时,首先会从内存中获取,如果内存中没有,那么就从存储设备中获取,最后才从网络下下载这张图片。
目前常用的一种算法是LRU(Least Recently Used),近期最少使用算法,采用LRU算法的缓存有两种:LruCache和DiskLruCache,分别用来实现内存缓存 和存储设备缓存。那么就先来使用第一种,从内存中获取图片。
LruCache是一个泛型类,它内部采用一个LinkedHashMap以强引用的方式存储外界缓存对象,其提供get和put的方法来完成缓存的获取和添加操作。
下面的ImageCache类完成了LruCache的创建过程,和实现了缓存的取get方法和缓存的存put方法。
/**
* 图片缓存类
* Created by lhc on 2017/7/31.
*/
public class ImageCache {
//图片缓存
LruCache mImageCache;
public ImageCache(){
initImageCache();
}
private void initImageCache() {
int maxMemory= (int) (Runtime.getRuntime().maxMemory()/1024);
int cacheSize = maxMemory / 4;
mImageCache = new LruCache(cacheSize){
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes()*bitmap.getHeight()/1024;
}
};
}
public void put(String url, Bitmap bitmap){
mImageCache.put(url,bitmap);
}
public Bitmap get(String url){
return mImageCache.get(url);
}
}
接着便是图片加载的类,这里使用到同步的方式加载图片,在加载图片的时候,先判断内存中是否有,如果有则从内存中获取,如果没有再从网络中获取,这里使用到线程池开启新的线程下载图片,然后再在主线程中得到设置图片,更新UI。
**
* 图片加载类
* Created by lhc on 2017/7/27.
*/
public class ImageLoader {
Handler mUiHandle = new Handler(Looper.getMainLooper());
ImageCache mImageCache;
ExecutorService executorService;
public ImageLoader(){
mImageCache= new ImageCache();
//只有核心线程并且这些线程不会被回收,能更加快速的响应外界的请求
executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
}
/**
* 显示图片
* @param url
* @param imageView
*/
public void displayImage(final String url, final ImageView imageView) {
//从内存中获取
Bitmap bitmap = mImageCache.get(url);
if (bitmap != null) {
Log.e("ImageLoader", "从内存中获取");
imageView.setImageBitmap(bitmap);
return;
}
imageView.setTag(url);
executorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(url);
if (bitmap == null) {
return;
}
//更新url
if (imageView.getTag().equals(url)) {
Log.e("ImageLoader", "从网络获取");
updateImageView(imageView, bitmap);
}
mImageCache.put(url, bitmap);
}
});
}
/**
* 显示图片 在主线程
*
* @param imageView
* @param bitmap
*/
private void updateImageView(final ImageView imageView, final Bitmap bitmap) {
mUiHandle.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
}
/**
* 下载图片 从网络
*
* @param imageurl
* @return
*/
private Bitmap downloadImage(String imageurl) {
Bitmap bitmap = null;
HttpURLConnection conn = null;
InputStream is = null;
try {
URL url = new URL(imageurl);
conn = (HttpURLConnection) url.openConnection();
is = conn.getInputStream();
bitmap = BitmapFactory.decodeStream(is);
} catch (Exception e) {
e.printStackTrace();
} finally {
conn.disconnect();
try {
if (is != null)
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return bitmap;
}
}
最后是我们的测试类,在onCreate方法中new一个ImageLoader,点击Button获取图片,可以发现,第一次从网络下获取,第二次从内存中获取。
public class MainActivity extends AppCompatActivity {
ImageLoader loader;
private final String url = "http://www.zhlzw.com/UploadFiles/Article_UploadFiles/201204/20120412123929231.jpg";
private ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = (ImageView) findViewById(R.id.imageVIew);
loader = new ImageLoader();
}
public void button(View view) {
loader.displayImage(url,imageView);
}
}
DiskLruCache用于实现存储设备缓存,Disk不属于Android SDK一部分,它的源码可以可以从这里获得。
下面的类完成了DiskLruCache的创建以及从缓存中获取,存入缓存的方法。Disk的创建通过DiskLruCache.open(cachefile,appVersion,valueCount,maxSize)静态方法获得。要存入缓存要通过Editor完成,要获得缓存要通过diskLruCache得到Snapshot对象,再得到输入流,从而获得图片。
注意这里存入缓存要通过访问网络得到输入流,然后存入文件的输出流中,要在子线程中进行,而设置图片更改UI要在主线程中,因此使用到了AsyncTask.
注意添加访问网络以及读写权限。
public class ImageDiskCache {
DiskLruCache diskLruCache;
public ImageDiskCache(Context context) {
try {
File file = getDiskCacheDir(context, "bitmap");
if (!file.exists()) {
file.mkdirs();
}
diskLruCache = DiskLruCache.open(file, 1, 1, 10 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 根据url存入缓存
*
* @param urlstring
*/
private void put(final String urlstring) {
try {
String key = hashKeyForDisk(urlstring);
DiskLruCache.Editor editor = diskLruCache.edit(key);
if (editor != null) {
OutputStream os = editor.newOutputStream(0);
if (downloadUrlToStream(urlstring, os)) {
editor.commit();
} else {
editor.abort();
}
}
//这里记得刷新一下,同步到journal文件
diskLruCache.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 设置图片 异步线程
*
* @param urlstring
* @return
*/
public void get(final String urlstring, final ImageView imageView) {
imageView.setTag(urlstring);
new AsyncTask() {
@Override
protected Bitmap doInBackground(String... params) {
Bitmap bitmap = null;
try {
String key = hashKeyForDisk(urlstring);
DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
if (snapshot == null) {
//如果没有找到对应的缓存,则准备从网络上请求数据,并写入缓存
put(urlstring);
snapshot = diskLruCache.get(key);
}
InputStream is = snapshot.getInputStream(0);
bitmap = BitmapFactory.decodeStream(is);
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageView.getTag().equals(urlstring)) {
imageView.setImageBitmap(bitmap);
}
}
}.execute();
}
/**
* 删除指定url的缓存
*
* @param urlstring
*/
public void remove(String urlstring) {
try {
String key = hashKeyForDisk(urlstring);
diskLruCache.remove(key);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 删除全部缓存
*/
public void delete() {
try {
diskLruCache.delete();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 必须运行在子线程
* 当下载网络图片时候,图片就可以通过这个文件输入流写到文件系统
*
* @param urlstring
* @param os
* @return
*/
private boolean downloadUrlToStream(final String urlstring, final OutputStream os) {
HttpURLConnection conn = null;
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
URL url = new URL(urlstring);
conn = (HttpURLConnection) url.openConnection();
InputStream is = conn.getInputStream();
bis = new BufferedInputStream(is, 8 * 1024);
bos = new BufferedOutputStream(os, 8 * 1024);
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
return true;
} catch (Exception e) {
e.printStackTrace();
} finally {
conn.disconnect();
try {
if (bis != null)
bis.close();
if (bos != null)
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
/**
* 缓存存入的位置
*
* @param context
* @param filename
* @return
*/
private File getDiskCacheDir(Context context, String filename) {
String cacheFile;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
cacheFile = context.getExternalCacheDir().getPath();
} else {
cacheFile = context.getCacheDir().getPath();
}
return new File(cacheFile);
}
/**
* 把url转换成key,因为图片的url很可能存在特殊字符
* 一般使用url的md5值作为key
*
* @param url
* @return
*/
private String hashKeyForDisk(String url) {
String cacheKey = null;
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.update(url.getBytes());
cacheKey = byteToHexString(messageDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
/**
* 将字节转成十进制
*
* @param digest
* @return
*/
private String byteToHexString(byte[] digest) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < digest.length; i++) {
String hex = Integer.toHexString(0xff & digest[i]);
if (hex.length() == 1) {
builder.append('0');
}
builder.append(hex);
}
return builder.toString();
}
}
然后 在主activity通过 diskCache.get(url,imageView);就能实现硬盘缓存了。
在以上,分别实现了内存缓存和硬盘缓存,在实际开发过程中,往往使用两者相结合的方式,能更高效的加载图片,节省用户流量。使用例子见下一篇,Android照片墙,高效加载图片,敬请期待。