图片加载框架

之前实现了一个选择本地图片进行加载显示的选择器,利用Glide作为图片加载器,Glide是一个十分方便的图片加载库,在项目中使用Glide也十分方便。由于项目需要自己搭建一个图片加载框架,实现功能比较简单,查阅资料后开始着手实现。

一、 实现目标

要实现的图片加载器主要是加载网络图片进行显示,加入LruCache进行内存缓存,在进行列表显示的时候加载重复图片可以直接从缓存中取图片进行显示,节约了网络资源,不用再重复下载。但是这个内存缓存只能在程序运行时分配到内存才能进行缓存,在程序结束时缓存也就释放了,下一次打开程序还是要重新下载。利用硬盘缓存DiskLruCache作为一个二级缓存目录,将下载的图片保存到本地缓存,由于本地缓存目录是默认创建的可以随程序卸载而删除,也可以显示调用删除缓存的方法进行清除,因此不用担心缓存占用很大空间的问题。

二、 框架实现

1. 线程池

在进行图片下载的时候要开辟一个新线程进行耗时操作,往往在加载列表显示的图片时会一次性开启很多线程,这里使用线程池来进行管理线程,利用一个任务Map进行管理,如果该图片地址存在线程正在进行下载,就不会创建新的线程,而是等待线程的下载完成。之前已经进行过线程池的使用,在之前的基础上进行开发。

2. LruCache

上一篇日记是关于LruCache的使用的,在使用内存缓存进行下载任务的缓存已经搞懂了,具体的实现代码见上一篇。

3. DiskLruCache

硬盘缓存是在LruCache的基础上进行改进的缓存机制,相比于内存缓存,硬盘缓存可以把缓存保存到本地,利用生成的key进行保存和获取,这个key可以根据传入的网络地址进行生成,得到一个哈希值,在取值的时候传入要查找的key就可以找到缓存文件,进行解码显示。
首先新建一个类,在这个类的基础上进行

public class ImageDiskCache {

    /**
     * 得到硬盘缓存目录
     * @param context
     * @param uniqueName
     * @return
     */
    public static File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

    /**
     * 得到版本号
     * @param context
     * @return
     */
    public static int getAppVersion(Context context) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            return info.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }

    /**
     * 获得缓存文件hash值
     * @param key
     * @return
     */
    public static String hashKeyForDisk(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    private static String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }
}

创建一个DiskLruCache对象不能直接new,要调用open方法进行

try {
            File cacheDir = ImageDiskCache.getDiskCacheDir (context, "bitmap");
            if(!cacheDir.exists ()) {
                cacheDir.mkdirs ();
            }
            //创建硬盘缓存实例
            mDiskLruCache = DiskLruCache.open (cacheDir, ImageDiskCache.getAppVersion (context), 1, 10 * 1024 * 1024);
        } catch (IOException e) {
            e.printStackTrace ();
        }

下载完成时把缓存文件保存到本地

String key = ImageDiskCache.hashKeyForDisk (imageUrl);
                    DiskLruCache.Editor editor = mDiskLruCache.edit (key);
                    if(editor != null) {
                        OutputStream outputStream = editor.newOutputStream (0);
                        if(downloadStream (imageUrl, outputStream)) {
                            editor.commit ();
                        } else {
                            editor.abort ();
                        }
                    }
                    mDiskLruCache.flush ();

加载图片时首先访问内存缓存,如果内存缓存有就调用,没有继续访问硬盘缓存,如果硬盘缓存也没用再开启一个新的下载线程进行下载显示

public void loadImage(String imageUrl, ImageView iv) {
        iv.setTag (imageUrl);
        Bitmap bitmap = getBitmapFromCache (imageUrl);
        if(bitmap != null) {
            iv.setImageBitmap (bitmap);
            Log.i (TAG, "cache image");
        } else {
            try {
                String key = ImageDiskCache.hashKeyForDisk (imageUrl);
                DiskLruCache.Snapshot snapShot = mDiskLruCache.get (key);
                if(snapShot != null) {
                    InputStream is = snapShot.getInputStream (0);
                    bitmap = BitmapFactory.decodeStream (is);
                    iv.setImageBitmap (bitmap);
                    Log.i (TAG, "disk cache image");
                }
            } catch (IOException e) {
                e.printStackTrace ();
            }
        }
        if(bitmap == null) {
            if(!mLoadMap.containsKey (imageUrl)) {
                ImageDownloaderTask task = new ImageDownloaderTask (imageUrl, iv);
                mLoadMap.put (imageUrl, task);
                try {
                    mThreadPoolExecutor.execute (task);
                } catch (Exception e) {
                    mLoadMap.remove (imageUrl);
                }
            }else {
                ImageDownloaderTask task = mLoadMap.get (imageUrl);
                task.run ();
            }
            Log.i (TAG, "download image");
        }
    }

4. 自定义ImageView

从网络上下载的图片尺寸不一,在使用一般的ImageView进行显示的时候不能实现按图片比例进行填充屏幕的显示,在显示图片的时候宽度固定为手机屏幕尺寸,高度随图片比例进行显示。

public class ImageLoadView extends android.support.v7.widget.AppCompatImageView {
    public ImageLoadView(Context context) {
        super (context);
    }

    public ImageLoadView(Context context, @Nullable AttributeSet attrs) {
        super (context, attrs);
    }

    public ImageLoadView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super (context, attrs, defStyleAttr);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Drawable drawable = getDrawable ();
        if(drawable != null){
            int width = drawable.getMinimumWidth();
            int height = drawable.getMinimumHeight();
            //确定显示比例,宽高比不变
            float scale = (float)height/width;
            int widthMeasure = MeasureSpec.getSize(widthMeasureSpec);
            int heightMeasure = (int)(widthMeasure*scale);
            heightMeasureSpec =  MeasureSpec.makeMeasureSpec(heightMeasure, MeasureSpec.EXACTLY);
        }
        super.onMeasure (widthMeasureSpec, heightMeasureSpec);
    }


}

三、 完整代码

package com.ruadong.utils.imageloader;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView;
import android.widget.Toast;

import com.jakewharton.disklrucache.DiskLruCache;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
* @author ruandong
* @create 2018/8/18
* @package com.ruadong.utils
* @Describe 图片加载器
*/
public class ImageLoader {

   private static final String TAG = "ImageLoader";
   /**
    * 返回Java虚拟机可用的处理器数量
    */
   private static final int CPU_COUNT = Runtime.getRuntime ().availableProcessors ();
   /**
    * 线程池基本大小
    */
   private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
   /**
    * 线程池最大线程数
    */
   private static final int MAXIMUM_POOL_SIZE = CPU_COUNT + 1;
   /**
    * 空闲线程存活时间为2秒
    */
   private static final int KEEP_ALIVE_TIME = 2;
   /**
    * 最大任务队列数
    */
   private static final int MAX_TASK_COUNT = 1000;
   /**
    * 内存缓存最大值
    */
   private static final int MAX_CACHE_SIZE = (int) (Runtime.getRuntime ().maxMemory () / 8);
   /**
    * 图片加载器实例
    */
   private static volatile ImageLoader mImageLoader;
   /**
    * 图片加载线程池
    */
   private ThreadPoolExecutor mThreadPoolExecutor;
   /**
    * 任务队列
    */
   private BlockingQueue<Runnable> mBlockingDeque;
   /**
    * 下载集合
    */
   private Map<String, ImageDownloaderTask> mLoadMap;
   /**
    * 主线程handler
    */
   private Handler mMainHandler;
   /**
    * 上下文
    */
   private Context mContext;
   /**
    * 内存缓存LruCache
    */
   private static LruCache<String, Bitmap> mLruCache;
   /**
    * 硬盘缓存DiskLruCache作为图片加载器的二级缓存,可以缓存从内存缓存中remove的bitmap,利用open方法创建实例
    */
   private DiskLruCache mDiskLruCache;

   /**
    * 图片加载器构造器
    */
   private ImageLoader() {
   }

   /**
    * 单例模式,获取图片加载器实例
    *
    * @return
    */
   public static ImageLoader getInstance() {
       if(mImageLoader == null) {
           synchronized (ImageLoader.class) {
               if(mImageLoader == null) {
                   mImageLoader = new ImageLoader ();
               }
           }
       }
       return mImageLoader;
   }

   /**
    * 初始化ImageLoader
    *
    * @param context
    */
   public void initImageLoader(Context context) {
       this.mContext = context.getApplicationContext ();
       mBlockingDeque = new LinkedBlockingDeque<> ();
       mThreadPoolExecutor = new ThreadPoolExecutor (CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, mBlockingDeque);
       mMainHandler = new Handler ();
       mLruCache = new LruCache<String, Bitmap> (MAX_CACHE_SIZE) {
           @Override
           protected int sizeOf(String key, Bitmap value) {
               return value.getByteCount ();
           }
       };
       try {
           File cacheDir = ImageDiskCache.getDiskCacheDir (context, "bitmap");
           if(!cacheDir.exists ()) {
               cacheDir.mkdirs ();
           }
           //创建硬盘缓存实例
           mDiskLruCache = DiskLruCache.open (cacheDir, ImageDiskCache.getAppVersion (context), 1, 10 * 1024 * 1024);
       } catch (IOException e) {
           e.printStackTrace ();
       }
   }

   /**
    * 获取内存缓存
    *
    * @return
    */
   public LruCache<String, Bitmap> getLruCache() {
       return mLruCache;
   }

   /**
    * 清除内存缓存
    */
   public void clearLruCache() {
       mLruCache.evictAll ();
   }

   /**
    * 保存bitmap到内存缓存
    *
    * @param key
    * @param bitmap
    */
   public void saveBitmapToCache(String key, Bitmap bitmap) {
       if(getBitmapFromCache (key) == null) {
           mLruCache.put (key, bitmap);
           Log.i (TAG, "cache size is " + mLruCache.size ());
       }
   }

   /**
    * 查询内存缓存是否已经存在当前bitmap
    *
    * @param key
    * @return
    */
   public Bitmap getBitmapFromCache(String key) {
       return mLruCache.get (key);
   }

   /**
    * 从内存缓存中移除该bitmap
    */
   public void clearDiskLruCache() {
       try {
           mDiskLruCache.delete ();
           Log.i (TAG, "clear disk cache");
       } catch (IOException e) {
           e.printStackTrace ();
       }
   }

   /**
    * 从外部开启任务线程加载图片
    *
    * @param imageUrl
    * @param iv
    */
   public void loadImage(String imageUrl, ImageView iv) {
       iv.setTag (imageUrl);
       Bitmap bitmap = getBitmapFromCache (imageUrl);
       if(bitmap != null) {
           iv.setImageBitmap (bitmap);
           Log.i (TAG, "cache image");
       } else {
           try {
               String key = ImageDiskCache.hashKeyForDisk (imageUrl);
               DiskLruCache.Snapshot snapShot = mDiskLruCache.get (key);
               if(snapShot != null) {
                   InputStream is = snapShot.getInputStream (0);
                   bitmap = BitmapFactory.decodeStream (is);
                   iv.setImageBitmap (bitmap);
                   Log.i (TAG, "disk cache image");
               }
           } catch (IOException e) {
               e.printStackTrace ();
           }
       }
       if(bitmap == null) {
           if(!mLoadMap.containsKey (imageUrl)) {
               ImageDownloaderTask task = new ImageDownloaderTask (imageUrl, iv);
               mLoadMap.put (imageUrl, task);
               try {
                   mThreadPoolExecutor.execute (task);
               } catch (Exception e) {
                   mLoadMap.remove (imageUrl);
               }
           }else {
               ImageDownloaderTask task = mLoadMap.get (imageUrl);
               task.run ();
           }
           Log.i (TAG, "download image");
       }
   }


   /**
    * 图片加载任务类
    */
   private class ImageDownloaderTask implements Runnable {

       private static final int CONNECT_TIMEOUT = 5 * 1000;
       private static final int READ_TIMEOUT = 20 * 1000;
       protected static final String ALLOWED_URI_CHARS = "@#&=*+-_.,:!?()/~'%";
       private Bitmap bitmap;
       private String imageUrl;
       private ImageView ivLoader;

       public ImageDownloaderTask(String imageUrl, ImageView ivLoader) {
           this.imageUrl = imageUrl;
           this.ivLoader = ivLoader;
       }

       @Override
       public void run() {
           boolean flag = true;
           try {
               while (flag) {
                   bitmap = downloadBitmap (imageUrl);
                   mImageLoader.saveBitmapToCache (imageUrl, bitmap);
                   String key = ImageDiskCache.hashKeyForDisk (imageUrl);
                   DiskLruCache.Editor editor = mDiskLruCache.edit (key);
                   if(editor != null) {
                       OutputStream outputStream = editor.newOutputStream (0);
                       if(downloadStream (imageUrl, outputStream)) {
                           editor.commit ();
                       } else {
                           editor.abort ();
                       }
                   }
                   mDiskLruCache.flush ();
                   Log.i (TAG, "disk cache success");
                   //图片下载完成,handler通知主线程更新界面
                   if(mMainHandler != null) {
                       mMainHandler.post (new Runnable () {
                           @Override
                           public void run() {
                               if(ivLoader != null && imageUrl.equals (ivLoader.getTag ())) {
                                   ivLoader.setImageBitmap (bitmap);
                                   Toast.makeText (mContext, "download success", Toast.LENGTH_SHORT).show ();
                               }
                           }
                       });
                   }
                   flag = false;
               }
           } catch (Exception e) {
               e.printStackTrace ();
           }
       }

       /**
        * 解析图片流为bitmap
        *
        * @param imageUrl
        * @return
        */
       protected Bitmap downloadBitmap(String imageUrl) {
           Bitmap bitmap;
           InputStream is = getInputStreamFromURL (imageUrl);
           bitmap = BitmapFactory.decodeStream (is);
           return bitmap;
       }

       /**
        * 根据传入的图片地址获取网络连接
        *
        * @param imageUrl
        * @return
        */
       protected HttpURLConnection getConnection(String imageUrl) {
           HttpURLConnection connection = null;
           String encodedUrl = Uri.encode (imageUrl, ALLOWED_URI_CHARS);
           try {
               if(connection != null) {
                   connection.disconnect ();
               } else {
                   URL url = new URL (encodedUrl);
                   connection = (HttpURLConnection) url.openConnection ();
                   connection.setConnectTimeout (CONNECT_TIMEOUT);
                   connection.setReadTimeout (READ_TIMEOUT);
               }
           } catch (MalformedURLException e) {
               e.printStackTrace ();
           } catch (IOException e) {
               e.printStackTrace ();
           }
           return connection;
       }

       /**
        * 从网络连接获取图片流
        *
        * @param imageUrl
        * @return
        */
       protected InputStream getInputStreamFromURL(String imageUrl) {
           InputStream inputStream = null;
           HttpURLConnection connection = getConnection (imageUrl);
           try {
               inputStream = connection.getInputStream ();
           } catch (IOException e) {
               e.printStackTrace ();
               if(inputStream != null) {
                   try {
                       inputStream.close ();
                   } catch (IOException e1) {
                       e1.printStackTrace ();
                   }
               }
           }
           return inputStream;
       }

       /**
        * @param imageUrl
        * @param outputStream
        * @return
        */
       protected Boolean downloadStream(String imageUrl, OutputStream outputStream) {
           BufferedOutputStream out = null;
           BufferedInputStream in = null;
           try {
               in = new BufferedInputStream (getInputStreamFromURL (imageUrl), 8 * 1024);
               out = new BufferedOutputStream (outputStream, 8 * 1024);
               int b;
               while ((b = in.read ()) != -1) {
                   out.write (b);
               }
               return true;
           } catch (final IOException e) {
               e.printStackTrace ();
           } finally {
               if(getConnection (imageUrl) != null) {
                   getConnection (imageUrl).disconnect ();
               }
               try {
                   if(out != null) {
                       out.close ();
                   }
                   if(in != null) {
                       in.close ();
                   }
               } catch (final IOException e) {
                   e.printStackTrace ();
               }
           }
           return false;
       }
   }
}

使用也很方便

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

推荐阅读更多精彩内容