ImageLoader的实现

一般来说一个好的图片库应该具备如下功能

图片的同步加载;
图片的异步加载;
图片压缩;
内存缓存;
磁盘缓存;
网络拉取;

1.图片压缩功能的实现

public class ImageReSizer {

    public Bitmap decodeSampleBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight){
        //首先用inJustDecodeBounds = true 解码去确认dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res,resId,options);

        //计算imSampleSize
        options.inSampleSize = calculateSampleSize(options, reqWidth, reqHeight );

        //用inSampleSIze解码bitmap
        options.inJustDecodeBounds =false;
        return BitmapFactory.decodeResource(res,resId,options);
    }

    public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight){
        //首先用inJustDecodeBounds = true 解码去确认dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fd,null,options);
        //计算inSampleSize
        options.inSampleSize = calculateSampleSize(options, reqWidth, reqHeight);

        //用inSampleSize集合去解码Bitmap
        options.inJustDecodeBounds = false;

        return BitmapFactory.decodeFileDescriptor(fd,null,options);

    }

    private int calculateSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        if (reqWidth==0 || reqHeight==0) {
            return 1;
        }

        //未处理的高度和宽度
        final int height = options.outHeight;
        final int width = options.outWidth;

        int inSampleSize = 1;

        if (height > reqHeight || width >reqWidth) {
            final int halfHeight = height/2;
            final int halfWidth = width/2;

            //计算最大的inSampleSize的2次方
            //两个都保存
            //宽高都比要求的宽高大
            while ((halfHeight / inSampleSize) >= reqHeight &&
                    (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *=2;
            }
        }

        return inSampleSize;

    }
}

**2.内存缓存和磁盘缓存的实现

这里选择LruCache和DiskLruCache来分别完成内存缓存和磁盘缓存的工作,在imageLoader初始化时 会创建LruCache和DiskLruCache。**

2.1 LruCache

LruCache是一个AndroidSdk提供的一个缓存泛型类, 它内部采用一个LinkedHashMap以强引用的方式储存外界的缓存对象,其提供了get和push方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除掉较早使用的缓存对象,然后再添加新的缓存对象。

强引用: 直接的对象引用
软引用: 当一个对象只有软引用存在时, 系统内存不足时此对象会被gc回收。
弱引用: 当一个对象只有弱引用存在时,此对象会随时被gc回收

一般初始化LruCache时, 指需要提供缓存的总容量大小并重写sizeOf方法即可。sizeOf方式是计算缓存对象的大小,这里大小的单位需要和总容量单位一样。

2.2 DiskLruCache

DiskLruCache 用户实现储存设备缓存,即磁盘缓存,它通过将缓存对象写入文件系统来实现缓存效果。 它的源码可以从如下网址得到:
官方地址

2.2.1 DiskLruCache的创建
DiskLruCache并不能通过构造方法来创建, 它提供了open方法来用于创建自身:

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

其中由四个参数分别是:

  1. 表示磁盘缓存在文件系统中的储存路径。 这里使用比较灵活, 当应用写在后就希望删除缓存文件,那么就选择sd卡上的缓存目录,如果希望保留缓存,就放在sd卡上的其他特定目录。
  2. 第二个参数表示的是应用的版本号,一般设置为1就好。
  3. 第三个参数表示单个节点所对应的数据个数。一般设置为1.
  4. 第四个参数表示缓存的总大小,比如50mb,当缓存大小超出这个设定后,缓存就会自动清楚一些缓存,保证大小不大于这个设定值。

2.3 在imageLoader中的实现

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Environment;
import android.os.StatFs;
import android.util.LruCache;

import com.sihan.myview.libcore.io.DiskLruCache;

import java.io.File;
import java.io.IOException;

public class ImageLoader {
    //磁盘缓存大小 50MB
    private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
    //磁盘缓存是否创建
    private  boolean mIsDiskLruCacheCreated = false;
    private LruCache<String, Bitmap> mMemoryCache;
    private DiskLruCache mDiskLruCache;
    private Context mContext;

    /**
     * 构造方法
     * @param context
     */
    private ImageLoader(Context context){
        mContext = context.getApplicationContext();
        //计算最大内存
        int  maxMemory = (int) (Runtime.getRuntime().maxMemory()/1024);
        //设置内存缓存大小为内存的1/8
        int cacheSize = maxMemory/8;
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize){
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return  bitmap.getRowBytes()*bitmap.getHeight()/1024;
            }
        };
        File diskCacheDir = getDiskCacheDir(mContext,"bitmap");
        if (!diskCacheDir.exists()){
            diskCacheDir.mkdirs();
        }
        if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE){
            try{
                mDiskLruCache = DiskLruCache.open(diskCacheDir,1,1,DISK_CACHE_SIZE);
                mIsDiskLruCacheCreated = true;
            }catch (IOException e){
                e.printStackTrace();
            }
        }

    }


    /**
     * 获取磁盘缓存地址
     * @param context
     * @param uniqueName
     * @return
     */
    private File getDiskCacheDir(Context context, String uniqueName) {
        //获取sdk状态 是否已经挂载并且拥有可读可写权限
        boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
        final String cachePath;
        if (externalStorageAvailable){
            cachePath = context.getExternalCacheDir().getPath();
        }else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator +uniqueName);
    }

    /**
     * 获取剩余空间
     * @param path
     * @return
     */
    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
    private long getUsableSpace(File path) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD){
            return path.getUsableSpace();
        }
        final StatFs statFs = new StatFs(path.getPath());
        return statFs.getBlockSizeLong() * statFs.getAvailableBlocksLong();
    }
}

在创建磁盘缓存是做一判断, 由可能剩余空间小于磁盘缓存所需的,意思就是手机空间不足,无法创建磁盘缓存,这个时候磁盘缓存就会失效。

2.4 图片的获取

2.4.1 LruCache的添加和读取

private void addBitmapToMemoryCache(String key, Bitmap bitmap){
        if (getBitmapFromMemCache(key) ==null){
            mMemoryCache.put(key,bitmap);
        }
    }

    private Bitmap getBitmapFromMemCache(String key) {
        return mMemoryCache.get(key);
    }

很简单如果要获取就直接去LruCache中拿, 在添加的时候先判断缓存中是否存在,不存在的话就添加。

2.4.2 硬盘缓存的添加和读取

DiskLruCache的缓存添加的操作是通过Editor来完成的,Editor表示一个缓存对象的编辑对象。 这里仍然以图片缓存举例, 首先需要获取图片url所对应的key,然后根据key来可以通过edit()来获取Editor对象, 如果这个缓存正在编辑,那么edit()就会返回null, 即DiskLruCache不允许同时编辑一个缓存对象。 之所以要把url转换成key, 是因为图片的url中很可能由特殊自负,浙江影响url在安卓中的使用,一般去采用MD5作为key表示。

private String hashKeyFormUrl(String url) {
        String cacheKey = null;
        try{
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(url.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return cacheKey;
    }

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

将图片的url转化key以后,就可以获取Editor对象了。对于这个key来说,如果当前不存在其他Editor对象,那么edit()就会返回一个新的Editor对象,通过它就可以得到一个文件输出流。 需要注意的是,由于前面在DiskLruCache的open方法中设置了一个节点只能储存一个数据,因此下面的Disk_Cache_Index敞亮直接设为0即可:

有了文件输出流,当从网络中下载图片时,图片就可以通过这个文件输出流写入到文件系统中。代码如下所示:

private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
        HttpURLConnection urlConnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;
        try{
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream());
            out = new BufferedOutputStream(urlConnection.getOutputStream());
            int b;
            while((b=in.read()) != -1){
                out.write(b);
            }
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        finally {
            if (urlConnection !=null){
                urlConnection.disconnect();
                MyUtils.close(in);
                MyUtils.close(out);
            }
        }
        return false;
    }

文件读写

private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException {
        if (Looper.myLooper()==Looper.getMainLooper()){
            Log.w(TAG, "can not visit network from UI thread" );
        }
        if (mDiskLruCache == null){
            return null;
        }
        String key = hashKeyFormUrl(url);
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);
        if (editor != null){
            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
            if (downloadUrlToStream(url,outputStream)){
                editor.commit();
            }else {
                editor.abort();
            }
            mDiskLruCache.flush();
        }
        return loadBitmapFromDiskCache(url,reqWidth,reqHeight);

经过下载的文件并没有直接写在磁盘里,通过commit 去保存操作或者abort去撤销

硬盘缓存查找

private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException {
        //如果是用主线程去加载就提示
        if (Looper.myLooper()==Looper.getMainLooper()){
            Log.w(TAG, "loadBitmapFromDiskCache: load bitmap from UI Thread, it's not recommended!" );
        }
        //如果缓存不存在返回null
        if (mDiskLruCache ==null){
            return null;
        }
        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
        if (snapshot != null){
            FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = mImageReSizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,reqWidth,reqHeight);
            if (bitmap !=null ){
                addBitmapToMemoryCache(key, bitmap);
            }
        }
        return bitmap;
    }

硬盘缓存查找需要需要将url转换为key,然后通过DiskLruCache的get方法得到一个Snapshot对象,接着在通过Snaphot对象即可得到缓存的文件输入流就可以得到bitmap对象了。这里通过文件流来饿到对应文件描述符,然后再通过BitmapFactory,decodeFileDescriptor方法来加载一张缩放后的图片。

3. 同步加载和异步加载接口的设计

3.1 同步加载

private Bitmap loadBitmap(String uri, int reqWidth, int reqHeight){
        Bitmap bitmap = getBitmapFromMemCache(uri);
        if (bitmap != null){
            return bitmap;
        }
        try{
            bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
            if (bitmap != null){
                return bitmap;
            }
            bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (bitmap == null && !mIsDiskLruCacheCreated){
            bitmap = downloadBitmapFromUrl(uri);
        }
        return bitmap;
    }

其工作原理遵循如下:

  1. 尝试其内存缓存中获取图片
  2. 其磁盘缓存中获取图片
  3. 最后才从网络中拉去图片
    另外这个方法不能在主线程中调用 否则就抛出异常。

3.2异步加载接口

private void bindBitmap(final String uri, final ImageView imageView, final int reqWidth, final int reqHeight){
        imageView.setTag(TAG_KEY_URI, uri);
        Bitmap bitmap = getBitmapFromMemCache(uri);
        if (bitmap !=null){
            imageView.setImageBitmap(bitmap);
            return;
        }
        Runnable loadBitmapTask = new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = loadBitmap(uri,reqWidth,reqHeight);
                if (bitmap !=null){
                    LoaderResult result = new LoaderResult(imageView, uri, bitmap);
                    mMainHandler.obtainMessage(MESSAGE_POST_RESULT,result).sendToTarget();
                }
            }
        };
        Thread_POOL_EXECUTOR.execute(loadBitmapTask);
    }

从bindBitmao的实现来看,bindBitmap方法会尝试从那个内存缓存中读取图片,如果读取成功就直接返回结果,否则会在线程池中其调用loadBitmap方法,当图片呢加载成功后再将图片,图片的地址及需要绑定的imageView封装成一个LoaderResult对象,在通过hanlder向主线程发送一个消息,这样就可以在主线程中给image.view设置图片了。

LoaderResult类

private static class LoaderResult {
        public ImageView imageView;
        public String uri;
        public Bitmap bitmap;

        public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
            this.imageView = imageView;
            this.uri = uri;
            this.bitmap = bitmap;
        }
    }

再来看看handler和线程池的实现

3.2.1 创建线程池

//线程池参数
 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 * 2 + 1;
    private static final long KEEP_ALIVE = 10L;
    
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
        }
    };

    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
            KEEP_ALIVE, TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(), sThreadFactory);

之所以要用到线程池是因为ImageLoader需要并发性。

3.2.2 创建Handler

再来看看handler

private Handler mMainHandler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(@NonNull Message msg) {
            LoaderResult result = (LoaderResult) msg.obj;
            ImageView imageView = result.imageView;
            String uri = (String) imageView.getTag(TAG_KEY_URI);
            if (uri.equals(result.uri)) {
                imageView.setImageBitmap(result.bitmap);
            } else {
                Log.w(TAG, "set image bitmap,but url has changed, ignored!");
            }
        };
        
    };

ImageLoader采用主线程的Looper来构造Handler对象,这就使得ImageLoader可以在非主线程中构造了。 另外为了解决由于View 复用所导致的列表错位这一问题,在给ImageView设置图片之前都会检查它的url有没有发生改变,如果发生改变就不再给他设置图片,这样就解决了列表错位的问题。

4.总结: 一个完整的图片加载框架就完成了。

package com.sihan.myview.imageLoader;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.StatFs;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView;

import androidx.annotation.NonNull;

import com.sihan.myview.R;
import com.sihan.myview.libcore.io.DiskLruCache;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import static android.content.ContentValues.TAG;

public class ImageLoader {
    //磁盘缓存大小 50MB
    private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
    private static final int DISK_CACHE_INDEX = 0;
    private static final int IO_BUFFER_SIZE = 8 * 1024;
    public static final int MESSAGE_POST_RESULT = 1;
    //线程池参数
    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 * 2 + 1;
    private static final long KEEP_ALIVE = 10L;

    //磁盘缓存是否创建
    private  boolean mIsDiskLruCacheCreated = false;
    private LruCache<String, Bitmap> mMemoryCache;
    private DiskLruCache mDiskLruCache;
    private Context mContext;
    private ImageReSizer mImageReSizer = new ImageReSizer();
    private static final int TAG_KEY_URI = R.id.imageloader_uri;
    private Handler mMainHandler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(@NonNull Message msg) {
            LoaderResult result = (LoaderResult) msg.obj;
            ImageView imageView = result.imageView;
            String uri = (String) imageView.getTag(TAG_KEY_URI);
            if (uri.equals(result.uri)) {
                imageView.setImageBitmap(result.bitmap);
            } else {
                Log.w(TAG, "set image bitmap,but url has changed, ignored!");
            }
        };
        
    };
    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
        }
    };

    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
            KEEP_ALIVE, TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(), sThreadFactory);
    

    /**
     * 构造方法
     * @param context
     */
    private ImageLoader(Context context){
        mContext = context.getApplicationContext();
        //计算最大内存
        int  maxMemory = (int) (Runtime.getRuntime().maxMemory()/1024);
        //设置内存缓存大小为内存的1/8
        int cacheSize = maxMemory/8;
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize){
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return  bitmap.getRowBytes()*bitmap.getHeight()/1024;
            }
        };
        File diskCacheDir = getDiskCacheDir(mContext,"bitmap");
        if (!diskCacheDir.exists()){
            diskCacheDir.mkdirs();
        }
        if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE){
            try{
                mDiskLruCache = DiskLruCache.open(diskCacheDir,1,1,DISK_CACHE_SIZE);
                mIsDiskLruCacheCreated = true;
            }catch (IOException e){
                e.printStackTrace();
            }
        }

    }

    /**
     * build a new instance of ImageLoader
     * @param context
     * @return a new instance of ImageLoader
     */
    public static ImageLoader build(Context context) {
        return new ImageLoader(context);
    }

    private void addBitmapToMemoryCache(String key, Bitmap bitmap){
        if (getBitmapFromMemCache(key) ==null){
            mMemoryCache.put(key,bitmap);
        }
    }

    private Bitmap getBitmapFromMemCache(String key) {
        return mMemoryCache.get(key);
    }


    public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight){
        Bitmap bitmap = getBitmapFromMemCache(uri);
        if (bitmap != null){
            return bitmap;
        }
        try{
            bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
            if (bitmap != null){
                return bitmap;
            }
            bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (bitmap == null && !mIsDiskLruCacheCreated){
            bitmap = downloadBitmapFromUrl(uri);
        }
        return bitmap;
    }

    private void bindBitmap(final String uri, final ImageView imageView, final int reqWidth, final int reqHeight){
        imageView.setTag(TAG_KEY_URI, uri);
        Bitmap bitmap = getBitmapFromMemCache(uri);
        if (bitmap !=null){
            imageView.setImageBitmap(bitmap);
            return;
        }
        Runnable loadBitmapTask = new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = loadBitmap(uri,reqWidth,reqHeight);
                if (bitmap !=null){
                    LoaderResult result = new LoaderResult(imageView, uri, bitmap);
                    mMainHandler.obtainMessage(MESSAGE_POST_RESULT,result).sendToTarget();
                }
            }
        };
        THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
    }

    private Bitmap downloadBitmapFromUrl(String urlString) {
        Bitmap bitmap = null;
        HttpURLConnection urlConnection = null;
        BufferedInputStream in = null;

        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(),
                    IO_BUFFER_SIZE);
            bitmap = BitmapFactory.decodeStream(in);
        } catch (final IOException e) {
            Log.e(TAG, "Error in downloadBitmap: " + e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            MyUtils.close(in);
        }
        return bitmap;
    }


    private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException {
        //如果是用主线程去加载就提示
        if (Looper.myLooper()==Looper.getMainLooper()){
            Log.w(TAG, "loadBitmapFromDiskCache: load bitmap from UI Thread, it's not recommended!" );
        }
        //如果缓存不存在返回null
        if (mDiskLruCache ==null){
            return null;
        }
        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
        if (snapshot != null){
            FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = mImageReSizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,reqWidth,reqHeight);
            if (bitmap !=null ){
                addBitmapToMemoryCache(key, bitmap);
            }
        }
        return bitmap;
    }

    private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException {
        if (Looper.myLooper()==Looper.getMainLooper()){
            Log.w(TAG, "can not visit network from UI thread" );
        }
        if (mDiskLruCache == null){
            return null;
        }
        String key = hashKeyFormUrl(url);
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);
        if (editor != null){
            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
            if (downloadUrlToStream(url,outputStream)){
                editor.commit();
            }else {
                editor.abort();
            }
            mDiskLruCache.flush();
        }
        return loadBitmapFromDiskCache(url,reqWidth,reqHeight);
    }

    private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
        HttpURLConnection urlConnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;
        try{
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream());
            out = new BufferedOutputStream(urlConnection.getOutputStream());
            int b;
            while((b=in.read()) != -1){
                out.write(b);
            }
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        finally {
            if (urlConnection !=null){
                urlConnection.disconnect();
                MyUtils.close(in);
                MyUtils.close(out);
            }
        }
        return false;
    }

    private String hashKeyFormUrl(String url) {
        String cacheKey = null;
        try{
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(url.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return cacheKey;
    }

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

    /**
     * 获取磁盘缓存地址
     * @param context
     * @param uniqueName
     * @return
     */
    private File getDiskCacheDir(Context context, String uniqueName) {
        //获取sdk状态 是否已经挂载并且拥有可读可写权限
        boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
        final String cachePath;
        if (externalStorageAvailable){
            cachePath = context.getExternalCacheDir().getPath();
        }else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator +uniqueName);
    }

    /**
     * 获取剩余空间
     * @param path
     * @return
     */
    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
    private long getUsableSpace(File path) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD){
            return path.getUsableSpace();
        }
        final StatFs statFs = new StatFs(path.getPath());
        return statFs.getBlockSizeLong() * statFs.getAvailableBlocksLong();
    }

    private static class LoaderResult {
        public ImageView imageView;
        public String uri;
        public Bitmap bitmap;

        public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
            this.imageView = imageView;
            this.uri = uri;
            this.bitmap = bitmap;
        }
    }
}