今天跟大家讲解一下图片的三级缓存
开发中我们经常涉及到图片的处理,几乎每个开发者都会碰到这个问题,我们经常在开发工程中使用各种各样的第三方框架做图片缓存,比如最简单的picasso,但是今天我们就要模仿他,并自己写一个轻量版的图片三级缓存工具MyImageTool,好的下面我们就开始步入正题.
所谓三级缓存就是涉及到三个方面
- 1.内存
- 2.本地存储
- 3.网络
原理就是如下方案(给大家提供两个方案,大家人选一个即可.建议第二个):
- 方案一:
- 1.先从内存”读取”照片,有就显示照片,没有就第2步;
- 2.从磁盘”获取”照片,如果有照片,就把图片存到内存,再从内存”读取”显示,没有就第3步;
- 3.从网络”获取”如果有照片,就存到磁盘,然后存到内存,在从内存读取显示,没有就给用户显示一个错误的图片
方案一的流程图如下:
流程图
- 方案二:
- 1.先从内存”读取”照片,有就显示照片,没有就第2步;
- 2.从磁盘”获取”照片,如果有照片就显示,并把图片存到内存,没有就第3步;
- 3.从网络”获取”如果有照片,如果有照片就显示,并存到磁盘,然后存到内存;没有就给用户显示一个错误的图片
方案二的流程图如下:
流程图
两种方案对比,并做出选择
方案一:从内存读取(有就显示)->从磁盘读取(有就存内存,内存再显示)->从网络(有就存磁盘(没有就显示错误图片)->再存内存->再显示)
(PS:一些老师为了方便同学理解,用了方案一,但是效率上每次都是存到了内存,再显示,都要跨几层,用方案二更合理(高效,且迅速))
方案二:从内存读取(有就显示)->从磁盘读取(有就转换并显示,再存内存)->从网络(有就显示,然后存磁盘->再存内存)->没有显示错误图片
其实一般磁盘有,那么肯定都存到了内存,出现三级缓存的目的也是为了开关机(内存的消失),或者清理了所有数据(内置卡程序数据的清空)
代码详情和内部讲解
package goodjobtome.com.lrucechepic;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.util.Log;
import android.widget.ImageView;
import android.widget.Toast;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
/**
* 做成单例,安全
*/
public class MyImageTool {
private static MyImageTool myImageTool = new MyImageTool();
//HashMap当内存.
private HashMap<String, Bitmap> mMCeche;
private MyImageTool() {
mMCeche = new HashMap();
}
//1.上下文
static Context mContext;
public static MyImageTool with(Context context) {
mContext = context;
return myImageTool;
}
//2.路径
public RequestCreator load(String path) {
RequestCreator creator = new RequestCreator(path);
return creator;
}
Handler mHandler = new Handler();
class RequestCreator {
String mUrl = null;
ImageView mIv;
RequestCreator(String path) {
mUrl = path;
}
//3.控件
public void into(ImageView iv) {
mIv = iv;
load();
}
private void load() {
/*-从内存里读取图片 HashMap-*/
Bitmap bitmapCeche = mMCeche.get(mUrl);
if (bitmapCeche != null) {
Log.d("RequestCreator", "显示的是内存的图");
mIv.setImageBitmap(bitmapCeche);
return;
}
/*--从磁盘读取"文件"存入磁盘的是文件file-MD5统一名字,在内存中才是图片bitmap-用url当做名字就行,所以从磁盘读取需要转换成图片;--*/
File file = getFileName();
try {
if (file.exists()) {
Bitmap bitmapDisk = BitmapFactory.decodeStream(new FileInputStream(file));
if (bitmapDisk != null) {
Log.d("RequestCreator", "显示的是磁盘的图");
mIv.setImageBitmap(bitmapDisk);
//立马且存到内存
saveCeche(bitmapDisk);
return;
}
}
} catch (Exception e) {
e.printStackTrace();
}
/*--从网络获取,开启线程,然后子线程显示即可,再存磁盘,存内存--*/
new Thread(new Runnable() {
@Override
public void run() {
try {
HttpURLConnection conn = (HttpURLConnection) new URL(mUrl).openConnection();
conn.setConnectTimeout(4 * 1000);
conn.setReadTimeout(4 * 1000);
InputStream is = conn.getInputStream();
final Bitmap bitmap = BitmapFactory.decodeStream(is);
if (bitmap == null) {
//没有就显示错误图片
showErrorPic();
return;
}
//子线程->主线程显示
mHandler.post(new Runnable() {
@Override
public void run() {
Log.d("RequestCreator", "显示的是网络的图");
mIv.setImageBitmap(bitmap);
}
});
//存到磁盘,存到磁盘的是文件格式
saveDisk(bitmap);
//存到内存的是纯bitmap格式.
saveCeche(bitmap);
conn.disconnect();
is.close();
} catch (IOException e) {
//异常显示错误图片
showErrorPic();
e.printStackTrace();
}
}
}).start();
}
//磁盘里的file文件名字
@NonNull
private File getFileName() {
String packageName = mContext.getPackageName();
File dir = new File("data/data/" + packageName + "/" + "myimage/");//自定义的
if (!dir.exists()) {
dir.mkdir();//没有就创建
}
String name = MD5Tool.getMD5(mUrl);
return new File(dir, name);
}
//图片保存到磁盘,要弄成文件的
private void saveDisk(Bitmap bitmap) {
File fileName = getFileName();
try {
//图片变成文件,注意方法 bitmap->file:压缩成文件
FileOutputStream fos = new FileOutputStream(fileName);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
//图片存到内存的方法
private void saveCeche(Bitmap bitmap) {
mMCeche.put(mUrl, bitmap);
}
//显示错误图片
private void showErrorPic() {
mHandler.post(new Runnable() {
@Override
public void run() {
Bitmap error = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.errorpic);
Toast.makeText(mContext, "Error", Toast.LENGTH_SHORT).show();
mIv.setImageBitmap(error);
}
});
}
}
}
下面到我们精彩的演示过程了
一.演示从网络获取并显示:
1.点开电脑网络上的一张图片
2.,然后复制地址,粘贴到程序的地址栏
3.点击按钮就会出现结果
4.打印的日志:
通过打印的日志,发现我们此次是从网络获取,因为是第一次访问
二.演示从内存获取并显示:
- 1.再次点击请求,或者点返回键回到手机主界面
- 2.再进去,再同样请求
- 3.打印的日志:
通过日志发现,我们此次是从内存读取,因为刚刚网络显示后已经把缓存放到磁盘再到内存了
三.演示从内存存储(相当磁盘)获取并显示:
- 1.点击程序任务键,滑动并关闭程序.
- 2.再进去,再同样请求
- 3.打印的日志:
重点内容
四.显示“获取错误图片”,即没有该图片,证明输入地址错误或者服务器有问题.
- 输入错误的地址
显示效果:
优化和拓展
大家有没有发现,我们这里的内存,我所用的是HashMap,但是大家有没有想过,如果有成千上百张,如果我们都这么存的话,那么就出现大问题了,可能内存就会崩掉,说以内存这一块我们要做出改进,就要用到我们的LruCeche();
LruCeche():全称:Least Recently Used 最近最少使用缓存.他是一个缓存集合**.其实底层还是LinkedHashMap.
特点:
当缓存的图片达到了预先设定的值的时候,那么近期使用次数最少的图片就会被回收掉。
使用方法:
- 1.我们先定义好字节变成kb.
- 2.缓存集合对象创建的时候构造传入大小,并重写方法sizeOf
- 3.返回获取的值的字节大小,或者自定义单位转换.
代码如下:
//改用缓存集合
private static LruCache<String, Bitmap> mMCeche;
private MyImageTool2() {
//这个是定义大小
//int maxSize = 1024*1024*4;
//注意这个地方及其的容易出错,因为我写成了freeMemory()/4+0.5f,导致大半天都集合都无法存入东西,因为两者不一样大了,而用int maxSize = 1024*1024*4;里面出现效果
int maxSize = (int) (Runtime.getRuntime().totalMemory());//四舍五入,用总内存就没错
mMCeche = new LruCache<String,Bitmap>(maxSize){
@Override
protected int sizeOf(String key, Bitmap value) {
//这个是对应上面的byte,注意单位对应,如果这里想作为M那么上面就写4,这里就写/1024/1024
return value.getByteCount();
}
};
}
好的下面给出大家所有的代码
- 1.主界面代码:MainActivity.java
package goodjobtome.com.lrucechepic;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
public class MainActivity extends AppCompatActivity {
private EditText mAdress;
private Button mGo;
private ImageView mIv;
private ImageView mIv2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
private void init() {
initView();
initEvent();
}
private void initView() {
mAdress = (EditText) findViewById(R.id.et);
mGo = (Button) findViewById(R.id.bt);
mIv = (ImageView) findViewById(R.id.iv);
mIv2 = (ImageView) findViewById(R.id.iv2);
}
private void initEvent() {
mGo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String path = mAdress.getText().toString().trim();
//使用框架
//Picasso.with(MainActivity.this).load(path).into(mIv);
UseTimeTool.getInstance().start();
//我们自己做的: 上下文->路径->控件
MyImageTool2.with(MainActivity.this).load(path).into(mIv);
UseTimeTool.getInstance().stop();
}
});
}
}
- 2.清单文件:注册,并添加网络权限即可
- 3.封装更好的控件代码:
package goodjobtome.com.lrucechepic;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView;
import android.widget.Toast;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* 做成单例,安全
*/
public class MyImageTool2 {
private static MyImageTool2 myImageTool = new MyImageTool2();
//改用缓存集合
private static LruCache<String, Bitmap> mMCeche;
private MyImageTool2() {
//这个是定义大小
//int maxSize = 1024*1024*4;
//注意这个地方及其的容易出错,因为我写成了freeMemory()/4+0.5f,导致大半天都集合都无法存入东西,因为两者不一样大了,而用int maxSize = 1024*1024*4;里面出现效果
int maxSize = (int) (Runtime.getRuntime().totalMemory());//四舍五入,用总内存就没错
mMCeche = new LruCache<String,Bitmap>(maxSize){
@Override
protected int sizeOf(String key, Bitmap value) {
//这个是对应上面的byte,注意单位对应,如果这里想作为M那么上面就写4,这里就写/1024/1024
return value.getByteCount();
}
};
}
//1.上下文
static Context mContext;
public static MyImageTool2 with(Context context) {
mContext = context;
return myImageTool;
}
//2.路径
public RequestCreator load(String path) {
RequestCreator creator = new RequestCreator(path);
return creator;
}
Handler mHandler = new Handler();
class RequestCreator {
String mUrl = null;
ImageView mIv;
RequestCreator(String path) {
mUrl = path;
}
//3.控件
public void into(ImageView iv) {
mIv = iv;
load();
}
private void load() {
/*-从内存里读取图片 HashMap-*/
if (loadFromCache()) return;
/*--从磁盘读取"文件"存入磁盘的是文件file-MD5统一名字,在内存中才是图片bitmap-用url当做名字就行,所以从磁盘读取需要转换成图片;--*/
if(loadFromDisk()) return;
/*--从网络获取,开启线程,然后子线程显示即可,再存磁盘,存内存--*/
loadFromNet();
}
//从内存读取
private boolean loadFromCache() {
Bitmap bitmapCeche = mMCeche.get(mUrl);
if (bitmapCeche != null) {
Log.d("RequestCreator", "显示的是内存的图");
mIv.setImageBitmap(bitmapCeche);
return true;
}
return false;
}
//从磁盘获取
private boolean loadFromDisk() {
File file = getFileName();
try {
if (file.exists()) {
Bitmap bitmapDisk = BitmapFactory.decodeStream(new FileInputStream(file));
if (bitmapDisk != null) {
Log.d("RequestCreator", "显示的是磁盘的图");
mIv.setImageBitmap(bitmapDisk);
//立马且存到内存
saveCeche(bitmapDisk);
return true;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
//从网络获取,并存如磁盘和内存
private void loadFromNet() {
new Thread(new Runnable() {
@Override
public void run() {
try {
HttpURLConnection conn = (HttpURLConnection) new URL(mUrl).openConnection();
conn.setConnectTimeout(4 * 1000);
conn.setReadTimeout(4 * 1000);
InputStream is = conn.getInputStream();
final Bitmap bitmap = BitmapFactory.decodeStream(is);
if (bitmap == null) {
//没有就显示错误图片
showErrorPic();
return;
}
//子线程->主线程显示
mHandler.post(new Runnable() {
@Override
public void run() {
Log.d("RequestCreator", "显示的是网络的图");
mIv.setImageBitmap(bitmap);
}
});
//存到磁盘是file格式,所以内部要处理
saveDisk(bitmap);
//存到内存的是纯bitmap格式.
saveCeche(bitmap);
conn.disconnect();
is.close();
} catch (IOException e) {
//异常显示错误图片
showErrorPic();
e.printStackTrace();
}
}
}).start();
}
//磁盘里的file文件名字
@NonNull
private File getFileName() {
String packageName = mContext.getPackageName();
File dir = new File("data/data/" + packageName + "/" + "myimage/");//自定义的
if (!dir.exists()) {
dir.mkdir();//没有就创建
}
String name = MD5Tool.getMD5(mUrl);
return new File(dir, name);
}
//图片存到磁盘
private void saveDisk(Bitmap bitmap) {
File fileName = getFileName();
//bitmap->file,图片转成文件
try {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, new FileOutputStream(fileName));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
//图片存到内存的方法
private void saveCeche(Bitmap bitmap) {
mMCeche.put(mUrl, bitmap);
System.out.println("putSize---------------------------------------" + mMCeche.size());
}
//显示错误图片
private void showErrorPic() {
mHandler.post(new Runnable() {
@Override
public void run() {
Bitmap error = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.errorpic);
Toast.makeText(mContext, "Error", Toast.LENGTH_SHORT).show();
mIv.setImageBitmap(error);
}
});
}
}
}
至于布局代码,时间工具,MD5工具,错误图片,布局文件,都比较简单,大家自己添加即可.
总结
1.三级缓存:
- 1.内存: HashMap或者LruCache(底层其实是linkedHaskmap)
- 1.注意的是maxSize这个的大小,否则画一天都找不到原因所在.
- 2.存放的是:存bitMap格式的,获取是根据key:ulr获取
2.磁盘:使用的是data/data/包名/myimage自定义文件夹下
- 1.存放的是:存file格式的文件,为了统一姓名用md5加密ulr后的key
- 2.图片转成文件即:bitmap.copress方法进行压缩即可.
- 3.取出的时候用图片工厂解析流即可.
3.网络:获取成功就显示,不成功就显示本地错误提示图片,异常也显示
- 1.存磁盘
- 2.存网络