Android图片加载框架之PictureCache
2019-11-25 本文已影响0人
beizhi
本框架目前还在迭代阶段,并不适合于直接用于项目,只是我用来学习的小东西,源码地址:https://github.com/xiangmingzhe/PictureCache
在Android开发中,如果图片过多,而我们又没有对图片进行有效的缓存,就很容易导致OOM(Out Of Memory)错误。因此,图片的缓存是非常重要的,尤其是对图片非常多的应用。现在很多框架都做了很好的图片缓存处理,如【Fresco】、【Glide】等。
1.说一下三级缓存的流程:
当我们的APP中想要加载某张图片时,先去LruCache中寻找图片,如果LruCache中有,则直接取出来使用,如果LruCache中没有,则去SoftReference中寻找,如果SoftReference中有,则从SoftReference中取出图片使用,同时将图片重新放回到LruCache中,如果SoftReference中也没有图片,则去文件系统中寻找,如果有则取出来使用,同时将图片添加到LruCache中,如果没有,则连接网络从网上下载图片。图片下载完成后,将图片保存到文件系统中,然后放到LruCache中。
2.实现:
(1)网络访问工具类NetCache:
package com.picture.lib_rhythm.cache;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;
import android.widget.ImageView;
import com.picture.lib_rhythm.R;
import com.picture.lib_rhythm.RequestCreator;
import com.picture.lib_rhythm.bean.TagInfo;
import com.picture.lib_rhythm.utils.BitmapUtils;
import com.picture.lib_rhythm.widgets.gif.GifImageView;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Iterator;
import java.util.Map;
/**
* Time:2019/11/7
* Author:xmz-dell
* Description:网络缓存类
*/
public class NetCache {
private LruCache lruCache;
private LocalCache localCache;
private static final String TAG="NetCache";
private Drawable errorDrawable;
private Context mContext;
private float radius=0f;
public BitmapTask bitmapTask;
public NetCache(Cache lruCache,LocalCache localCache){
this.lruCache=(LruCache) lruCache;
this.localCache=localCache;
}
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
Object[] objects = (Object[]) msg.obj;
ImageView imageView = (ImageView) objects[0];
String mUrl = (String) objects[1];
imageView.setTag(mUrl);
setErrorView(objects,imageView);
}
};
/**
* 错误视图
* @param errorDrawable
* @return
*/
public NetCache error(Drawable errorDrawable){
this.errorDrawable=errorDrawable;
return this;
}
/**
* 设置圆角
* @param radius 弧度
* @return
*/
public NetCache transform(float radius) {
this.radius=radius;
return this;
}
/**
* 是否需要设置圆角
* @return
*/
private boolean isRoundCorner(){
return radius!=0f?true:false;
}
/**
* 设置错误视图
* @param objects
* @param imageView
*/
private void setErrorView(Object[] objects,ImageView imageView){
if(errorDrawable!=null){
boolean isSuccess=(boolean) objects[2];
if(!isSuccess){
if(isRoundCorner()){
Bitmap bitmap=BitmapUtils.toRoundCorner(errorDrawable,radius,0);
imageView.setImageBitmap(bitmap);
}else{
imageView.setImageDrawable(errorDrawable);
}
}
}
}
/**
* 加载图片
* @param iv
* @param url
*/
public void loadBitmap(final ImageView iv, final String url, Context context){
if(iv==null||TextUtils.isEmpty(url)||context==null){
throw new NullPointerException("ImageView Can not be empty || url Can not be empty || context Can not be empty");
}
this.mContext=context;
new Handler().post(new Runnable() {
@Override
public void run() {
Bitmap bitmap=loadBitmap(url);
if(bitmap!=null){
sendBitmap(bitmap);
}else{
bitmapTask=new BitmapTask();
pushTaskToMap(url,bitmapTask);
bitmapTask.execute(iv, url);// 启动AsyncTask,
}
}
});
}
/**
* 将所有异步任务存储
* @param url
* @param bitmapTask
*/
private void pushTaskToMap(String url,BitmapTask bitmapTask){
RequestCreator.getInstance().taskMap.put(url,bitmapTask);
}
/**
* 取消单个请求
*/
public void cancleTask(String tag){
Map<String,NetCache.BitmapTask>taskMap= RequestCreator.getInstance().taskMap;
Iterator<String> iterator =taskMap.keySet().iterator();// map中key(键)的迭代器对象
while (iterator.hasNext()){// 循环取键值进行判断
String key = iterator.next();// 键
if(tag.equals(key)){
BitmapTask bitmapTask = taskMap.get(key);
if(bitmapTask!=null){
bitmapTask.cancel(true);
iterator.remove();
break;
}
}
}
}
/**
* 取消全部请求
*/
public void cancleAllTask(){
Map<String,NetCache.BitmapTask>taskMap= RequestCreator.getInstance().taskMap;
Iterator<String> iterator =taskMap.keySet().iterator();// map中key(键)的迭代器对象
while (iterator.hasNext()){// 循环取键值进行判断
String key = iterator.next();// 键
BitmapTask bitmapTask = taskMap.get(key);
if(bitmapTask!=null){
bitmapTask.cancel(true);
iterator.remove();
}
}
}
/**
* 尝试下加载内存-->磁盘
* @param url
* @return
*/
private Bitmap loadBitmap(String url){
if(lruCache.get(url)!=null){
return lruCache.get(url);
}
if(localCache.getBitmapFromLocal(url)!=null){
return localCache.getBitmapFromLocal(url);
}
return null;
}
/**
* Handler和线程池的封装
* <p/>
* 第一个泛型: 参数类型
* 第二个泛型: 更新进度的泛型,
* 第三个泛型是onPostExecute的返回结果
*/
public class BitmapTask extends AsyncTask<Object, Void, Bitmap> {
private ImageView ivPicture;
private String url;
private Object[] objects;
private Message message;
/**
* 后台耗时方法在此执行, 子线程
*/
@Override
protected Bitmap doInBackground(Object... params) {
ivPicture = (ImageView) params[0];
url = (String) params[1];
message = handler.obtainMessage();
objects = new Object[]{ivPicture, url,true
};
message.obj = objects;
handler.sendMessage(message);
return downloadBitmap(url,ivPicture,objects,message);
}
/**
* 更新进度, 主线程
*/
@Override
protected void onProgressUpdate(Void... values) {
super.onProgressUpdate(values);
}
/**
* 耗时方法结束后,执行该方法, 主线程
*/
@Override
protected void onPostExecute(Bitmap result) {
if (result != null) {
String bindUrl = (String) ivPicture.getTag();
if (url.equals(bindUrl)) {// 确保图片设定给了正确的imageview
sendBitmap(result);
localCache.setBitmapToLocal(url, result);// 将图片保存在本地
lruCache.set(url, result);// 将图片保存在内存
Log.d(TAG,"从网络缓存读取图片啦");
}
}else{
Log.d(TAG,"图片网络加载失败");
message = handler.obtainMessage();
objects = new Object[]{ivPicture, url,false
};
message.obj = objects;
handler.sendMessage(message);
}
}
}
/**
* 下载图片
*
* @param url
* @return
*/
private Bitmap downloadBitmap(String url,ImageView iv,Object[]objects,Message message) {
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL(url).openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setRequestMethod("GET");
conn.connect();
int responseCode = conn.getResponseCode();
if (responseCode == 200) {
InputStream inputStream = conn.getInputStream();
//图片压缩处理
BitmapFactory.Options option = new BitmapFactory.Options();
// option.inSampleSize = 2;//宽高都压缩为原来的二分之一, 此参数需要根据图片要展示的大小来确定
option.inPreferredConfig = Bitmap.Config.RGB_565;//设置图片格式
Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, option);
return bitmap;
}
} catch (Exception e) {
Log.d(TAG,"图片加载失败,准备加载错误视图");
message = handler.obtainMessage();
objects = new Object[]{iv, url,false
};
message.obj = objects;
handler.sendMessage(message);
e.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
}
}
return null;
}
/**
* 发送一个bitmap给上游
* 下一个版本要替换成自定义rxandroid来实现。
*
*/
private void sendBitmap(Bitmap bitmap){
if(onLoadSuccessListener!=null){
onLoadSuccessListener.loadBitmapSuccess(bitmap);
}
}
public OnLoadSuccessListener onLoadSuccessListener;
public NetCache setOnLoadSuccessListener(OnLoadSuccessListener onLoadSuccessListener) {
this.onLoadSuccessListener = onLoadSuccessListener;
return this;
}
public interface OnLoadSuccessListener{
void loadBitmapSuccess(Bitmap bitmap);
}
}
(2)本地文件访问类LocalCache :
package com.picture.lib_rhythm.cache;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import com.picture.lib_rhythm.utils.Utils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* Time:2019/11/7
* Author:xmz-dell
* Description:本地文件
* */
public class LocalCache {
private String cachePath;
public LocalCache(Context context, String uniqueName) {
if(TextUtils.isEmpty(uniqueName)){
throw new NullPointerException("uniqueName Can not be empty");
}
cachePath = getCacheDirString(context, uniqueName);
}
/**
* 根据url获取bitmap
* @param url
* @return
*/
public Bitmap getBitmapFromLocal(String url){
try{
File file = new File(cachePath, encode(url));
if (file.exists()) {
// 如果文件存在
Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(file));
return bitmap;
}
}catch (Exception e){
e.printStackTrace();
}
return null;
}
/**
* 获取缓存目录的路径
* @param context
* @param uniqueName
* @return
*/
private String getCacheDirString(Context context, String uniqueName) {
File file = null;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
file = new File(context.getExternalCacheDir(), uniqueName);
} else {
file = new File(context.getCacheDir(), uniqueName);
}
if (!file.exists()) {
file.mkdirs();
}
return file.getAbsolutePath();
}
/**
* 设置Bitmap数据到本地
*
* @param url
* @param bitmap
*/
public void setBitmapToLocal(String url, Bitmap bitmap) {
if(TextUtils.isEmpty(url)||bitmap==null){
throw new NullPointerException("url Can not be empty || bitmap Can not be empty");
}
if(Utils.calculateSdCardCacheSize()<Utils.getBitmapKB(bitmap)){
throw new IllegalArgumentException("sdcard Insufficient space left");
}
FileOutputStream fos = null;
try {
String fileName = encode(url);
File file = new File(cachePath, fileName);
File parentFile = file.getParentFile();//获取上级所有目录
if (!parentFile.exists()) {
// 如果文件不存在,则创建文件夹
parentFile.mkdirs();
}
Log.d("保存的地址:","file.getAbsolutePath():"+file.getAbsolutePath());
fos = new FileOutputStream(file);
// 将图片压缩到本地
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
} catch (Exception e) {
e.printStackTrace();
}finally {
if (fos != null) {
try {
fos.close();//关闭流
fos = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/***
* md5 加密
* @param pwd
* @return
*/
public String encode(String pwd) {
StringBuffer sb = new StringBuffer();
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] bytes = digest.digest(pwd.getBytes("UTF-8"));
for (int i = 0; i < bytes.length; i++) {
String s = Integer.toHexString(0xff & bytes[i]);
if (s.length() == 1) {
sb.append("0" + s);
} else {
sb.append(s);
}
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return sb.toString();
}
}
(3)内存工具类LruCache :
package com.picture.lib_rhythm.cache;
import android.content.Context;
import android.graphics.Bitmap;
import android.text.TextUtils;
import com.picture.lib_rhythm.utils.Utils;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Time:2019/11/7
* Author:xmz-dell
* Description:
*/
public class LruCache implements Cache{
private int size;
private int maxSize;
private int cacheCount; //缓存计数
private int evictionCount;//清除计数
private final LinkedHashMap<String, Bitmap> map;
public LruCache(Context context){
this(Utils.calculateMemoryCacheSize(context));
}
public LruCache(int maxSize){
if(maxSize<=0){
throw new IllegalArgumentException("Max size Cannot be less than or equal to 0");
}
this.maxSize=maxSize;
this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
}
@Override
public Bitmap get(String key) {
if(TextUtils.isEmpty(key)){
throw new NullPointerException("key Can not be empty");
}
synchronized (this){
Bitmap bitmap;
bitmap=map.get(key);
if(bitmap!=null){
cacheCount++;
return bitmap;
}
}
return null;
}
@Override
public void set(String key, Bitmap bitmap) {
if(TextUtils.isEmpty(key)||bitmap==null){
throw new NullPointerException("key Can not be empty||bitmap Can not be empty");
}
Bitmap readyMoveBitmap;//等待移动的bitmap
size+= Utils.getBitmapBytes(bitmap);//得到当前bitmap大小并叠加
readyMoveBitmap=map.put(key,bitmap);
if(readyMoveBitmap!=null){
size-=Utils.getBitmapBytes(readyMoveBitmap);
}
reduceMemoryPressure();
}
/**
* //如果当前size超过maxsize 就从前往后移除bitmap 来减少内存
*/
public void reduceMemoryPressure(){
while(true){
String key;
Bitmap bitmap;
synchronized (this){
if(size<0||map.isEmpty()){
throw new NullPointerException("size Cannot be less than 0 ||map Can not be empty");
}
if(size<=maxSize||map.isEmpty()){//如果没有超过阈值就结束整个死循环;
break;
}
Map.Entry<String, Bitmap> decompose = map.entrySet().iterator().next();
key=decompose.getKey();
bitmap=decompose.getValue();
map.remove(key);
size-=Utils.getBitmapBytes(bitmap);
evictionCount++;
}
}
}
@Override
public int size() {
return size;
}
@Override
public int maxSize() {
return maxSize;
}
@Override
public void clear() {
reduceMemoryPressure();
}
@Override
public void clearKeyUri(String keyPrefix) {
}
}
目前已经实现的功能如下图:
IMG20191125142151.jpg
最后调用方式如下
## 设置图片四周圆角:
Rhythm.with(context).load(url).transform(10.0f).into(imageView);
## 设置占位图:
Rhythm.with(context).load(url).placeholder(R.drawable.xx).into(imageView);
## 加载错误视图:
Rhythm.with(context).load(url).error(R.drawable.xx).into(imageView);
## 开启加载gif图片(正在优化)
Rhythm.with(context).load(url).openGif(false).into(imageView);
## 设置成圆形图片
Rhythm.with(context).load(url).style(TypeEnum.CIRCLE).into(imageView);
## 设置圆形or圆角图片边框
Rhythm.with(context).load(url).style(TypeEnum.CIRCLE).boarder(2).into(imageView);
Rhythm.with(context).load(url).transform(10.0f).boarder(2).into(imageView);
## 增加图片渐变
## 图片懒加载
## 取消单个加载
Rhythm.with(ListActivity.this).cancleTask(tag);
## 取消所有加载
Rhythm.with(context).cancleAllTask();
## 加载图片高斯模糊效果
Rhythm.with(context).bitmapTransform(new BlurTransformation(10)).into(imageView);
其中BlurTransformation中分别包括模糊半径和指定模糊前缩小的倍数。
# 动画相关
### 图片淡入淡出效果
Rhythm.with(context).load(url).crossFade().into(imageView);
### 支持图片自定义动画
Rhythm.with(context).load(url).Animation(R.anim.xx).into(imageView);
### 设置无动画
Rhythm.with(context).load(url).dontAnimation().into(imageView);
# 增加图片水印功能,详细功能:分为居中,左上角,右上角,左下角,右下角
### (1)居中水印
Rhythm.with(context).load(url).watermark(new WatermarkInfo(Watermark.CENTER,watermakeBitmap)).into(imageView);
### (2)左上角水印
Rhythm.with(context).load(url)
.watermark(new WatermarkInfo(Watermark.LEFT_TOP,watermakeBitmap,paddingLeft,paddingTop,paddingRight,paddingBottom))
.into(imageView);
### (3)右上角水印
Rhythm.with(context).load(url)
.watermark(new WatermarkInfo(Watermark.RIGHT_TOP,watermakeBitmap,paddingLeft,paddingTop,paddingRight,paddingBottom))
.into(imageView);
### (4)左下角水印
Rhythm.with(context).load(url)
.watermark(new WatermarkInfo(Watermark.LEFT_BOTTOM,watermakeBitmap,paddingLeft,paddingTop,paddingRight,paddingBottom))
.into(imageView);
### (5)右下角水印
Rhythm.with(context).load(url)
.watermark(new WatermarkInfo(Watermark.RIGHT_BOTTOM,watermakeBitmap,paddingLeft,paddingTop,paddingRight,paddingBottom))
.into(imageView);