图片加载机制——压缩,缓存,线程池

  昨天看了《Android开发艺术探索》的第十二章,Bitmap的加载和Cache,里面实现了一个ImageLoader,还是涉及到了不少知道的,Bitmap的压缩,LruCache,DiskLruCache,同步异步加载,线程池,把这些知识综合起来完成了一个图片加载类ImageLoader。根据书中知识来大概分析一下这个类,免得过段时间又啥都记不得了。
  为了高效加载Bitmap,我们有时候需要对Bitmap进行压缩再显示到ImageView上,这里先提供一个图片压缩类ImageResizer。主要利用BitmapFactory.Options及其inSampleSize参数。首先获得图片的原始宽高,然后根据期望的宽高计算出缩放比率inSampleSize值,这里的inSample不会小于1,等于1时就代表不缩放,等于2时就代表长宽各为原来的1/2。且inSampleSize一般为2的倍数,文档中是这样描述的,当然也可以不遵守。这个类比较简单,简单看一下代码,我自己添了一些注释。
  

/**
 *图片缩放
 */
public class ImageResizer {

    public ImageResizer() {
    }

    public Bitmap resizeBitmapFromResources(Resources res, int resId, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        //设置为true之后,不为bitmap分配内存,返回null,但是options会被赋值,以此来获取图片大小
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);
        //计算出合适的缩放比例
        options.inSampleSize = caculateInSampleSize(options, reqWidth, reqHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    public Bitmap resizeBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fd, null, options);
        options.inSampleSize = caculateInSampleSize(options, reqWidth, reqHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fd, null, options);
    }

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

        int inSampleSize = 1;
        //图片的原始宽高
        final int originWidth = options.outWidth;
        final int originHeight = options.outHeight;

        if (originWidth > reqWidth || originHeight > reqHeight) {
            final int halfWidth = originWidth / 2;
            final int halfHeight = originHeight / 2;
            //计算宽高都达到预期标准时的inSampleSize,根据文档描述,一般为2的倍数
            while ((halfWidth / inSampleSize) > reqWidth && (halfHeight / inSampleSize) > reqHeight) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}

  接着说说图片加载,作为用户,我的期望肯定不是每次都去网络上加载图片。作为开发者,我们就必须提供一套缓存系统,当用户要拉取一张图片的时候,我们先检查内存缓存中有没有,然后再检查磁盘缓存中有没有,前两者都没有的话,最后再从网络上拉取。那么如何实现内存缓存和磁盘缓存呢?先说一下内存缓存,使用的是LruCache,从Android3.0开始,LruCache已经成为安卓的源代码,所以这也是Google推荐的内存缓存方式,即Least Recently Used,最近最少使用算法,当缓存达到峰值时,会自动清除最近最少使用的数据。磁盘缓存使用的是DiskLruCache,同样这也是Google推荐的缓存方式,但是还没有写入源码中,这个类我会附在文章末尾。原理和LruCache是一样的。在初始化ImageLoader的时候,会同时创建LruCache和DiskLruCache,看一下代码,
  

  /**
     * 私有构造函数,不能通过new来产生实例
     * 主要完成了mLruCache和mDiskLruCache的初始化
     */
    private ImageLoader(Context context) {
        mContext = context.getApplicationContext();//传入Application级别的context

        //应用程序可用的最大内存
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

        //取最大内存的1/8作为内存缓存的大小
        int cacheSize = maxMemory / 8;

        /**
         * 初始化内存缓存
         * 返回缓存对象的大小,单位要和cacheSize一样
         */
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };

        File diskCacheFile = getDiskCacheFile(mContext, DISK_CACAHE_NAME);
        if (!diskCacheFile.exists()) {
            diskCacheFile.mkdirs();
        }

        //确保该路径下的可用空间大于磁盘缓存需要的最大空间
        if (getUsableSpace(diskCacheFile) > DISK_CACHE_SIZE) {
            try {
                mDiskLruCache = DiskLruCache.open(diskCacheFile, Utils.getAppVersion(mContext), 1, DISK_CACHE_SIZE);
                mIsDiskCacheCreated = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

  这里我们取应用程序最大可用内存的1/8作为内存缓存大小,取50MB作为磁盘缓存大小,当然要判断一下磁盘是否有50MB的剩余空间。初始化两种缓存后,给两种缓存加上添加和读取方法。
  
  内存缓存:
 

 /**
     * 向内存缓存中添加Bitmap
     */
    private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemoryCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    /**
     * 根据key获取内存缓存中的Bitmap
     */
    private Bitmap getBitmapFromMemoryCache(String key) {
        return mMemoryCache.get(key);
    }

  再来看看磁盘缓存的添加和读取。
  

 String key = hashKeyFormUrl(url);
        try {
            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();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
 /**
     * 从磁盘缓存中加载图片,加载成功后添加到内存缓存
     */
    private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            Log.w(TAG, "load bitmap from UI Thread, it's not recommended!");
        }
        if (mDiskLruCache == null) {
            return null;
        }

        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        try {
            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
            if (snapshot != null) {
                FileInputStream inputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
                FileDescriptor descriptor = inputStream.getFD();
                bitmap = mImageResizer.resizeBitmapFromFileDescriptor(descriptor, reqWidth, reqHeight);
                if (bitmap != null) {
                    addBitmapToMemoryCache(key, bitmap);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;

    }

  这里有些地方需要注意,代码中把所有的url经过MD5加密之后当做key,其实并不是为了去加密,因为url中可能包含特殊字符,为了避免不必要的错误,经过MD5加密后使用是一样的。
  接着提供同步下载和异步下载。先来看看同步加载的逻辑,首先尝试从内存缓存中读取图片,若没有就尝试从磁盘缓存中读取,最后从网络拉取。看代码:
  

   /**
     * 同步加载
     */
    public Bitmap loadBitmap(String url, int reqWidth, int reqHeight) {
        Bitmap bitmap = loadBitmapFromMemCache(url);
        if (bitmap != null) {
            Log.d(TAG, "from MemoryCache; url:" + url);
            return bitmap;
        }

        bitmap = loadBitmapFromDiskCache(url, reqWidth, reqHeight);
        if (bitmap != null) {
            Log.d(TAG, "from DiskCache; url:" + url);
            return bitmap;
        }
        bitmap = loadBitmapFromNet(url, reqWidth, reqHeight);
        Log.d(TAG, "from Net; url:" + url);

        if (bitmap == null && !mIsDiskCacheCreated) {
            Log.e("TAG", "encounter error, DiskLruCache is not created.");
            bitmap = downloadBitmapFromUrl(url);
        }
        return bitmap;
    }

  这个方法不能再主线程中调用,看看loadBitmapFromNet中的一段代码,

if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("can not visit network from UI Thread.");
        }

在主线程中调用时会抛出异常。
  接着看看异步加载的逻辑。首先会尝试从内存缓存中读取,成功则直接返回,反之使用线程池的loadBitmap方法,也就是刚才的同步下载的方法,通过Handler+Message更新UI,这样就可以在子线程中调用了。
  

/**
     * 异步下载
     * 根据url将图片显示在ImageView上,依次顺序是内存缓存,磁盘缓存,网络下载
     *
     * @param url       图片url
     * @param imageView 绑定的ImageView
     * @param reqWidth  期望的宽度
     * @param reqHeight 期望的高度
     */
    public void bindBitmap(final String url, final ImageView imageView, final int reqWidth, final int reqHeight) {
        imageView.setTag(TAG_KEY_URI, url);
        Bitmap bitmap = loadBitmapFromMemCache(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }

        Runnable loadBitmapTask = new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = loadBitmap(url, reqWidth, reqHeight);
                if (bitmap != null) {
                    LoaderResult result = new LoaderResult(imageView, url, bitmap);
                    mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
                }
            }
        };

        THREAD_POOR_EXECUTOR.execute(loadBitmapTask);
    }

  这里主要线程池和Handler的知识。先来看看线程池THREAD_POOL_EXECUTOR的实现。我们取CPU核心数+1作为核心线程数,取CPU核心数*2+1作为最大线程数,取10s作为线程闲置超时时间,代码如下:
  

 //线程工厂
    private static final ThreadFactory sThreadFactory = new ThreadFactory() {

        //AtomicInteger,线程安全的加减操作接口
        private final AtomicInteger mCount = new AtomicInteger(1);

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

    //线程池
    public static final Executor THREAD_POOR_EXECUTOR = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(), sThreadFactory);

  在大量并发的情况下使用线程池的优点是很明显的。接着看看里面的Handler机制,这里直接采用主线程的Looper来构造Handler对象,使得ImageLoader可以在非主线程构造,通过发送消息通知更新UI。如下所示:
  

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

  这里采用Tag标记的方式防止列表错位。
  大概就是这么多内容了,里面应该有很多主席书中的话,书不能带在身边,就得做做笔记了。下面贴一下完整的ImageLoader类,我自己添加了注释。
  

/**
 * Created by sun on 2016/1/27.
 * LruCache+DiskLruCache+ThreadPool综合应用
 */
public class ImageLoader {

    private static final String TAG = "ImageLoader";

    private static final int TAG_KEY_URI = R.id.imageloader_uri;

    private static final int DISK_CACHE_INDEX = 0;

    public static final int MESSAGE_POST_RESULT = 1001;

    //CPU核心数
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();

    //核心线程数
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;

    //最大线程数
    private static final int MAX_POOL_SIZE = CPU_COUNT * 2 + 1;

    //线程存活时间
    private static final long KEEP_ALIVE_TIME = 10;

    //磁盘缓存目录名称
    private static final String DISK_CACAHE_NAME = "bitmap";

    //磁盘缓存大小限制,默认为50MB,可修改
    private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;

    //IO流大小
    private static final int IO_BUFFER_SIZE = 8 * 1024;

    //标记内存缓存是否建立
    private boolean mIsDiskCacheCreated = false;

    //内存缓存
    private LruCache<String, Bitmap> mMemoryCache;

    //磁盘缓存
    private DiskLruCache mDiskLruCache;

    //上下文
    private Context mContext;

    //图片缩放类
    private ImageResizer mImageResizer = new ImageResizer();

    //线程工厂
    private static final ThreadFactory sThreadFactory = new ThreadFactory() {

        //AtomicInteger,线程安全的加减操作接口
        private final AtomicInteger mCount = new AtomicInteger(1);

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

    //线程池
    public static final Executor THREAD_POOR_EXECUTOR = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(), sThreadFactory);


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


    /**
     * 私有构造函数,不能通过new来产生实例
     * 主要完成了mLruCache和mDiskLruCache的初始化
     */
    private ImageLoader(Context context) {
        mContext = context.getApplicationContext();//传入Application级别的context

        //应用程序可用的最大内存
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

        //取最大内存的1/8作为内存缓存的大小
        int cacheSize = maxMemory / 8;

        /**
         * 初始化内存缓存
         * 返回缓存对象的大小,单位要和cacheSize一样
         */
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };

        File diskCacheFile = getDiskCacheFile(mContext, DISK_CACAHE_NAME);
        if (!diskCacheFile.exists()) {
            diskCacheFile.mkdirs();
        }

        //确保该路径下的可用空间大于磁盘缓存需要的最大空间
        if (getUsableSpace(diskCacheFile) > DISK_CACHE_SIZE) {
            try {
                mDiskLruCache = DiskLruCache.open(diskCacheFile, Utils.getAppVersion(mContext), 1, DISK_CACHE_SIZE);
                mIsDiskCacheCreated = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 提供ImageLoader实例
     */
    public static ImageLoader build(Context context) {
        return new ImageLoader(context);
    }


    public void bindBitmap(final String url, final ImageView imageView) {
        bindBitmap(url, imageView, 0, 0);
    }

    /**
     * 异步下载
     * 根据url将图片显示在ImageView上,依次顺序是内存缓存,磁盘缓存,网络下载
     *
     * @param url       图片url
     * @param imageView 绑定的ImageView
     * @param reqWidth  期望的宽度
     * @param reqHeight 期望的高度
     */
    public void bindBitmap(final String url, final ImageView imageView, final int reqWidth, final int reqHeight) {
        imageView.setTag(TAG_KEY_URI, url);
        Bitmap bitmap = loadBitmapFromMemCache(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }

        Runnable loadBitmapTask = new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = loadBitmap(url, reqWidth, reqHeight);
                if (bitmap != null) {
                    LoaderResult result = new LoaderResult(imageView, url, bitmap);
                    mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
                }
            }
        };

        THREAD_POOR_EXECUTOR.execute(loadBitmapTask);
    }


    /**
     * 同步加载
     * * @param url       图片url
     * @param reqWidth  期望的宽度
     * @param reqHeight 期望的高度
     */
    public Bitmap loadBitmap(String url, int reqWidth, int reqHeight) {
        Bitmap bitmap = loadBitmapFromMemCache(url);
        if (bitmap != null) {
            Log.d(TAG, "from MemoryCache; url:" + url);
            return bitmap;
        }

        bitmap = loadBitmapFromDiskCache(url, reqWidth, reqHeight);
        if (bitmap != null) {
            Log.d(TAG, "from DiskCache; url:" + url);
            return bitmap;
        }
        bitmap = loadBitmapFromNet(url, reqWidth, reqHeight);
        Log.d(TAG, "from Net; url:" + url);

        if (bitmap == null && !mIsDiskCacheCreated) {
            Log.e("TAG", "encounter error, DiskLruCache is not created.");
            bitmap = downloadBitmapFromUrl(url);
        }
        return bitmap;
    }


    /**
     * 从内存缓存中加载图片
     */
    private Bitmap loadBitmapFromMemCache(String url) {
        final String key = hashKeyFormUrl(url);
        return getBitmapFromMemoryCache(key);
    }

    /**
     * 从磁盘缓存中加载图片,加载成功后添加到内存缓存
     */
    private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            Log.w(TAG, "load bitmap from UI Thread, it's not recommended!");
        }
        if (mDiskLruCache == null) {
            return null;
        }

        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        try {
            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
            if (snapshot != null) {
                FileInputStream inputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
                FileDescriptor descriptor = inputStream.getFD();
                bitmap = mImageResizer.resizeBitmapFromFileDescriptor(descriptor, reqWidth, reqHeight);
                if (bitmap != null) {
                    addBitmapToMemoryCache(key, bitmap);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;

    }

    /**
     * 从网络加载图片流,缓存到磁盘,然后从磁盘缓存中获取图片
     */
    private Bitmap loadBitmapFromNet(String url, int reqWidth, int reqHeight) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("can not visit network from UI Thread.");
        }
        if (mDiskLruCache == null) {
            return null;
        }

        String key = hashKeyFormUrl(url);
        try {
            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();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
    }

    /**
     * 直接从网络下载图片
     */
    private Bitmap downloadBitmapFromUrl(String urlPath) {
        Bitmap bitmap = null;
        HttpURLConnection connection = null;
        BufferedInputStream inputStream = null;

        try {
            URL url = new URL(urlPath);
            connection = (HttpURLConnection) url.openConnection();
            inputStream = new BufferedInputStream(connection.getInputStream(), IO_BUFFER_SIZE);
            bitmap = BitmapFactory.decodeStream(inputStream);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return bitmap;
    }


    private boolean downloadUrlToStream(String imageUrl, OutputStream outputStream) {
        HttpURLConnection connection = null;
        BufferedInputStream in = null;
        BufferedOutputStream out = null;

        try {
            URL url = new URL(imageUrl);
            connection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(connection.getInputStream(), IO_BUFFER_SIZE);
            out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
            int b;
            if ((b = in.read()) != -1) {
                out.write(b);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    /**
     * 向内存缓存中添加Bitmap
     */
    private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemoryCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    /**
     * 根据key获取内存缓存中的Bitmap
     */
    private Bitmap getBitmapFromMemoryCache(String key) {
        return mMemoryCache.get(key);
    }

    /**
     * 获取磁盘缓存路径
     * 放在此路径下当程序卸载时同时会清楚缓存数据
     * 当然你也可以自定义其他缓存路径
     */
    private File getDiskCacheFile(Context context, String uniqeName) {
        String cachePath = null;
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            cachePath = mContext.getExternalCacheDir().getPath();
        } else {
            cachePath = mContext.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqeName);
    }

    /**
     * 获取可用空间
     */
    @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 (long) statFs.getBlockSize() * (long) statFs.getAvailableBlocks();
    }

    /**
     * 以url作为tag防止加载时候错乱,为防止url有特殊字符,一律转化为MD5
     *
     * @param url 图片url
     * @return MD5字符串
     */
    private String hashKeyFormUrl(String url) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(url.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(url.hashCode());
        }
        return cacheKey;
    }

    private 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();
    }

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

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

  为了方便大家,提供一个可以直接使用的DiskLruCache类。下载链接:http://download.youkuaiyun.com/detail/sunluyao_/9420403

示例代码托管地址:https://github.com/lulululbj/DailyAndroid

有任何疑问,欢迎加群讨论:261386924
  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值