Android之okHttpClient+handler+LruCache缓存网络图片(通用MVP模式)
上一次我在学习过程中,写了一篇关于缓存网络图片的学习笔记,在那一篇博客中使用的是AsyncTask异步任务请求的方式缓存的,这一次我从学习中,学会了一种新的缓存方法,就是通过LruCache去缓存数据,LruCache是一种内存缓存机制,采用了最近最少LRU算法,这样的效率比直接去判断从本地出数据效率相对更高一点。下面开始贴代码,具体的功能在代码中注释。
整体框架:
涉及的类有点多,但是,为了养成使用MVP模式的习惯,也只能拼了。这里只写几个具体有实际操作的类,因为代码会上传,大家可以下载源码查看。
model层:
MainActivityModelImp.java
public class MainActivityModelImp implements MainActivityModelInter {
/**
* 处理具体的数据,
* @param path json地址
* @param listener presenter中回调接口
*/
@Override
public void getData(String path,OnCompleteNetDataListener listener) {
//这里发送一个json数据请求
MyNetUtils.getNetData(path,listener, GloableInterface.JSON);
}
}
view层:
MainActivity.java
public class MainActivity extends BaseActivity<MainActivityPresenter,MainActivity> implements MainActivityInter<List<PicBean.NewslistBean>> {
//请求图片的json地址,这里是百度为我们提供的一个美女图片接口
private String path = "http://apis.baidu.com/txapi/mvtp/meinv?num=10";
//自定义的adpter
private MyListAdapter adapter;
//显示的数据集
private List<PicBean.NewslistBean> data;
//ListView控件
private ListView mList;
//ListView布局的item布局id
@LayoutRes
private int layoutId;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
event();
//请求数据
basePresenter.load(path);
}
private void event() {
}
/**
* 初始化控件或对象
*/
private void init() {
layoutId = R.layout.list_item;
data = new ArrayList<>();
mList = (ListView)findViewById(R.id.mList);
//文件管理的布局
MyFileUtils.context = MainActivity.this;
}
/**
* 向adapter提供数据,设置ListView的数据
* @param data
*/
private void showListData(List<PicBean.NewslistBean> data){
if(adapter == null){
adapter = new MyListAdapter(data,layoutId);
mList.setAdapter(adapter);
}else{
data.addAll(data);
adapter.notifyDataSetChanged();
}
}
/**
* 关联MainActivityPresenter
* @return
*/
@Override
public MainActivityPresenter getPresenter() {
return new MainActivityPresenter();
}
/**
* presenter返回的数据
* @param newslistBeen 数据集
*/
@Override
public void showData(List<PicBean.NewslistBean> newslistBeen) {
data = newslistBeen;
showListData(data);
}
}
presenter层:
MainActivityPresenter.java
public class MainActivityPresenter extends BasePresenter<MainActivity, MainActivityModelImp> {
@Override
public MainActivityModelImp createModel() {
return new MainActivityModelImp();
}
/**
* 处理view的请求,获取model的数据返回给view
* @param path
*/
public void load(String path) {
model.getData(path, new MainActivityModelInter.OnCompleteNetDataListener() {
@Override
public void onCompleteNetData(byte[] bytes, String path) {
if (bytes != null) {
PicBean picBean = new Gson().fromJson(new String(bytes), PicBean.class);
getView().showData(picBean.getNewslist());
}else{
Log.i("IT_Real", "onCompleteNetData: 数据没有访问到");
}
}
});
}
}
callback包:
OnLoadImage.java
public class OnLoadImage implements MainActivityModelInter.OnCompleteNetDataListener{
private ImageView imageView;
public OnLoadImage(ImageView imageView){
this.imageView = imageView;
}
@Override
public void onCompleteNetData(byte[] bytes, String path) {
//避免图片对应位置请求错误。出现一闪一闪的图片或者,连续不间断显示图片的问题
if(bytes != null && imageView.getTag().equals(path)){
Bitmap bm = BitmapFactory.decodeByteArray(bytes,0,bytes.length);
//TODO 缓存图片、
//添加到内存缓存中
MyLruCache.getInstance().put(path.replaceAll("\\W",""),bm);
//缓存到本地
MyFileUtils.saveCache(imageView.getContext().getExternalCacheDir(),bm,path);
//设置图片数据
imageView.setImageBitmap(bm);
}
}
}
adapter包:
MyListAdapter.java
public class MyListAdapter extends MyBaseAdapter<PicBean.NewslistBean> {
public MyListAdapter(List<PicBean.NewslistBean> data, @LayoutRes int layoutId) {
super(data, layoutId);
}
/**
* 具体的设置item数据操作
* @param holder ViewHolder持有对象
* @param picBean 数据集
*/
@Override
public void setData(ViewHolder holder, PicBean.NewslistBean picBean) {
//获取图片对应的标题
String title = picBean.getTitle();
//获取图片的地址
String path = picBean.getPicUrl();
//去掉非法的字符,然后作为文件名保存到本地
String fileName = path.replaceAll("\\W","");
//设置标题
holder.setText(R.id.mTvTitle, title);
//通过内存缓存机制去获取Bitmap数据
Bitmap bm = MyLruCache.getInstance().get(fileName);
//自定义一个标记,看看数据是从内存缓存中拿到的还是从本地拿到的
String tag = "从内存缓存中拿了数据";
//如果内存缓存中没有数据
if (bm == null) {
//先看看本地内存中有没有数据,如果有就直接从本地拿数据
if(MyFileUtils.isCachedToLocal(path)){
tag = "从本地拿了数据";
fileName = fileName + path.substring(path.lastIndexOf("."),path.length());
bm = BitmapFactory.decodeFile(MyFileUtils.context.getExternalCacheDir() + "/" + fileName);
}else{//否则就去网络请求数据
if(MyFileUtils.isEnoughSpace())//判断内存空间是否足够,足够就去请求数据,然后缓存
holder.setImageURL(R.id.mImage, path);
else{//空间不足则删除一些已经缓存的图片
Toast.makeText(MyFileUtils.context,"空间不足了,缓存不了图片了,正在请求删除图片...",Toast.LENGTH_SHORT).show();
MyFileUtils.deletePic(holder.getConverView().getContext().getExternalCacheDir());
}
}
}
//不为空,就直接去设置图片
if(bm != null){
Toast.makeText(MyFileUtils.context, tag, Toast.LENGTH_SHORT).show();
holder.setImageBitmap(R.id.mImage,bm);
}
}
}
ViewHolder.java
public class ViewHolder {
private View converView;
private int position;
private Context context;
private SparseArray<View> views = new SparseArray<>();
public ViewHolder(Context context, int position, @LayoutRes int layoutId) {
if (context == null)
this.context = context;
this.position = position;
converView = LayoutInflater.from(context).inflate(layoutId, null);
converView.setTag(this);
}
public <T extends View> T getView(int viewId) {
View view = views.get(viewId);
if (view == null) {
view = converView.findViewById(viewId);
views.put(viewId, view);
}
return (T) view;
}
public ViewHolder setText(int tvId, String data) {
TextView textView = getView(tvId);
textView.setText(data);
return this;
}
/**
* 通过图片地址请求图片数据
* @param viewId 设置图片控件的id
* @param path 请求图片的地址
* @return 返回ViewHolder,为了链式操作
*/
public ViewHolder setImageURL(int viewId, String path) {
final ImageView imageView = getView(viewId);
imageView.setImageBitmap(null);
//每个id控件对应一张图片的地址,只有一一对应才会请求网络,避免了不必要的网络请求
imageView.setTag(path);
//请求网络数据
MyNetUtils.getNetData(path, new OnLoadImage(imageView), GloableInterface.IMAGEURL);
return this;
}
/**
* 通过Bitmap设置图片
* @param viewId 图片控件id
* @param bm 图片数据
* @return
*/
public ViewHolder setImageBitmap(int viewId, Bitmap bm) {
ImageView imageView = getView(viewId);
imageView.setImageBitmap(bm);
return this;
}
public static ViewHolder getViewHolder(View convertView, Context context, int position, @LayoutRes int layoutId) {
ViewHolder holder = null;
if (convertView == null) {
holder = new ViewHolder(context, position, layoutId);
} else {
holder = (ViewHolder) convertView.getTag();
holder.position = position;
}
return holder;
}
public View getConverView() {
return converView;
}
}
utils包:
MyFileUtils.java
public class MyFileUtils {
//上下文布局
public static Context context;
//获取指定位置存储状态的类对象
private static StatFs statFs;
//私有化该对象,外部不能创建该实例,大量的操作只需要通过静态方法获取即可
private static MyFileUtils myFileUtils = new MyFileUtils();
private MyFileUtils(){
}
/**
* 为外部提供一个获取实例的方法
* @return
*/
public static MyFileUtils getInstance(){
return myFileUtils;
}
/**
* 判断本地内存中是否有当前文件名的缓存路径
* @param path 图片的地址,将图片地址去掉非法字符作为文件名
* @return
*/
public static boolean isCachedToLocal(String path){
String fileName = path.replaceAll("\\W","");
fileName = fileName + path.substring(path.lastIndexOf("."),path.length());
File file = new File(context.getExternalCacheDir(),fileName);
return isMount() && file.exists();
}
/**
* 是否有足够的空间
* @return
*/
public static boolean isEnoughSpace(){
if(statFs == null){
statFs = new StatFs(Environment.getExternalStorageDirectory().getPath());
}
int availableBlocks = statFs.getAvailableBlocks();
int blockSize = statFs.getBlockSize();
return availableBlocks * blockSize >= 10 * 1024 * 1024;
}
/**
* 判断是否判断内存卡是否具有读写权限,是不是挂载状态
* @return
*/
public static boolean isMount(){
return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
}
/**
* 递归删除缓存的文件
* @param file 文件路径
*/
public static void deletePic(File file){
if(file == null) return;
if(!file.isFile()){//如果不是文件就遍历
File[] files = file.listFiles();
if(file != null){
for (File file1 : files) {
deletePic(file1);
}
}
}else {
file.delete();
}
}
/**
* 保存缓存的图片
* @param cacheDir 缓存的路径
* @param bm
* @param path 文件的名字
*/
public static void saveCache(File cacheDir, Bitmap bm, String path) {
//去掉所有非单词的字符
String fileName = path.replaceAll("\\W","");
fileName = fileName + path.substring(path.lastIndexOf("."),path.length());
try {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(new File(cacheDir, fileName)));
bm.compress(Bitmap.CompressFormat.JPEG, 100, bos);
bos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
MyLruCache.java
/**
* 内存缓存类,这个类使用了最近最少算法来操作缓存
*/
public class MyLruCache extends LruCache<String,Bitmap> {
private static MyLruCache myLruCache = new MyLruCache((int)Runtime.getRuntime().maxMemory()/1024/8);
public static MyLruCache getInstance(){
return myLruCache;
}
/**
* 缓存的大小,默认给他分配最大空间的1/8即可
* @param maxSize
*/
private MyLruCache(int maxSize) {
super(maxSize);
}
/**
* 返回每个缓存对象
* @param key 存放的key
* @param value 对应的图片数据
* @return
*/
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;//返回大小 KB单位
}
}
MyNetUtils.java
/**
* 网络请求工具
*/
public class MyNetUtils {
//创建okHttpClient对象
private static OkHttpClient client = new OkHttpClient();
//创建一个运行在主线程的handler
private static Handler handler = new Handler(Looper.getMainLooper());
private MyNetUtils(){
}
/**
* 获取网络数据
* @param path
* @param listener
* @param flag
*/
public static void getNetData(final String path, final MainActivityModelInter.OnCompleteNetDataListener listener, String flag){
//创建一个builder实例
Request.Builder builder = new Request.Builder();
Request request = null;
if("json".equals(flag)){
//设置对应的请求地址,header
request = builder.url(path).get().addHeader("apikey","自己申请的百度apiStore中的APIKEY").build();
}else if("imageURL".equals(flag)){
request = builder.url(path).get().build();
}
//开始请求数据
client.newCall(request).enqueue(new Callback() {
/**
* 当服务器中断,或者出现不可避免的故障,不能正常访问服务器,执行该方法
*
* @param call
* @param e
*/
@Override
public void onFailure(Call call, IOException e) {
}
/**
* 请求成功,或者404等错误都会执行这个方法
* @param call
* @param response
* @throws IOException
*/
@Override
public void onResponse(Call call, Response response) throws IOException {
//获取到请求的字节数组,这个请求是在异步任务中请求的
final byte[] bytes = response.body().bytes();
//通过handler将数据回调到主线程
handler.post(new Runnable() {
@Override
public void run() {
if(listener != null){
listener.onCompleteNetData(bytes,path);
}
}
});
}
});
}
}
以上就是一些具体的实现。。。上面涉及的知识点不是很多,只要理解了就会觉得很简单了。如果不懂通用Adapter封装的,请点击这里 , 如果不懂通用MVP模式的,请点击这里,这里只是一些具体的实现,还又很多类没有贴上,因为大部分都是一些重复的类,大家想看的下载源码去看吧。