Android图片加载框架之PictureCache

本框架目前还在迭代阶段,并不适合于直接用于项目,只是我用来学习的小东西,源码地址: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);   
 
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268