Android高效ImageLoader的实现

在android开发过程图片加载和显示基本上是每个项目中都会包含的功能,这就导致每个项目里面ImageLoader是标配。当然我们在使用的过程中有很多牛逼的(性能好,使用简单方便)开源框架可供挑选。但是如果自己手动实现一个高效的ImageLoader那给自己的技术树里面又添加了一个靓丽的枝干。ok,接下来我们一起来分析和探讨一下高效ImageLoder的实现。

一般来说,优秀的ImageLoader都具有以下几个共性:

  • 图片的同步加载
  • 图片的异步加载
  • 图片按需要压缩
  • 内存缓存
  • 磁盘缓存
  • 网络拉取
  • 使用方便简单
  • 性能好

虽然我们可能做不到优秀,但是我们也得往这个目标和方向上使劲。所以接下来的实现中,我们也会尽力去做到这些。

  • 前两年很多同行使用软引用和弱引用来实现图片的多级缓存,但是现在使用软引用和弱引用已经变得不再可靠,它主要存在以下几点弊端和风险 因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。

  • 另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩。

基于这些问题和风险,我们使用在3.0以后被Android引入的LruCache来进行图片内存缓存,(这个类是3.1版本中提供的,如果你是在更早的Android版本中开发,则需要导入android-support-v4的jar包)

LruCache在处理图片释放时使用的原则是最久未使用原则,即:当LruCache内存储的图片总大小大于指定内存时,自动释放未使用时长最久的图片。ok,内存缓存就它了。

我们说过优秀ImageLoader还应该包含磁盘缓存,那么我们在磁盘缓存中我们可以考虑用最基本的文件存取来实现,因为一般来说,我们的磁盘缓存在我看来就是对我们的数据进行一下备份,方便需要的时候获取,不太需要多么优秀的算法来控制它们。但是现在业内的普遍做法是使用DisckLruCache来做磁盘缓存。好吧,虽然笔者不太清楚这样做的原理,但是我们照猫画虎,也就用这个吧。毕竟我们是奔着优秀去的,业内的一些反响比较好的图片加载框架都用的它,肯定是有使用的价值所在。

ok,分析完基本的技术选型,我们开始进入框架编写的正题。

我们先从调用开始讲起,我们这边提供两种调用的方式:同步调用和异步异步调用,我们把这两个方法定义如下。

    /** * 图片异步加载 */
    public void bind(String resource,ImageView imageView,int reqWidth,int reqHeight){

    }

    /** *图片同步加载 */
    public Bitmap load(String resource,int reqWidth,int reqHeight){
        Bitmap bitmap = null;
        return bitmap;
    }

两种加载方式的实现,我们接下来一步一步地写。图片使用和加载的效率有高到低的顺序为:内存缓存 >SD卡缓存>网络请求

ok,接下来我们对内存缓存和SD卡缓存进行初始化配置。在这里我们提供一个ImgLoaderConfig来设置相关配置,这个主要包括以下几个属性(需要的话再进行进一步扩展):

    /** * 内存缓存大小 */
    private int memoryCacheSize;
    /** * SD卡缓存大小 */
    private long diskCacheSize;
    /** * SD卡缓存路径 */
    private String diskCachePath;

好了,有个这个配置类,我们可以编写我们的初始化配置方法了:

/** * 初始化方法 * * @param config 图片加载框架相关配置 * @param context 上下文 */
    public void init(ImgLoaderConfig config, Context context) {
        if (null == context) {
            throw new IllegalArgumentException("the context could not is null");
        }

        if (config.getMemoryCacheSize() <= 0) {
            int maxMemory = (int) Runtime.getRuntime().maxMemory() / 1024;
            memoryCacheSize = maxMemory / 8;
        } else {
            memoryCacheSize = config.getMemoryCacheSize();
        }

        if (config.getDiskCacheSize() <= 0) {
            diskCacheSize = DISK_CACHE_SIZE;
        } else {
            diskCacheSize = config.getDiskCacheSize();
        }

        mMemoryCache = new LruCache<String, Bitmap>(memoryCacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //计算bitmap所占内存,使用高版本api时可以使用 value.getByteCount();
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };

        if (TextUtils.isEmpty(config.getDiskCachePath())) {
            diskCachePath = context.getCacheDir().getAbsolutePath();
        } else {
            diskCachePath = config.getDiskCachePath();
        }

        File file = new File(diskCachePath);
        if (!file.exists()) {
            file.mkdirs();
        }

        try {
            mDiskLruCache = DiskLruCache.open(file, 1, 1, diskCacheSize);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

初始化配置完成后,我们来编写优先级最高的从内存缓存存、取图片:

/** * 将bitmap加入内存缓存中 * * @param key 缓存的key * @param bitmap 待缓存的bitmap */
    private void addImg2Memory(String key, Bitmap bitmap) {
        if (null == mMemoryCache) {
            throw new IllegalArgumentException("the memoryCache could not be null");
        }

        if (null == mMemoryCache.get(key)) {
            mMemoryCache.put(key, bitmap);
        }
    }

    /** * 根据key从内存缓存中获取bitmap * * @param key 缓存的key * @return 缓存的bitmap */
    private Bitmap loadImgFromMemory(String key) {
        if (null == mMemoryCache) {
            throw new IllegalArgumentException("the memoryCache could not be null");
        }

        if (mMemoryCache.size() < 1) {
            return null;
        }

        Bitmap bitmap = mMemoryCache.get(key);
        return bitmap;
    }

内存缓存内如果无法取到图片,我们就尝试从SD缓存内取(SD卡缓存存取时需要注意一个细节问题,使用网络请求链接直接做存取的key是不可取的,因为链接内可能带有特殊字符,所以需要把它们转换成MD5的字符串做为存取的key)

/** * 将图片存入SD卡 * * @param key 存取的key * @param inputStream 文件输入流 */
    private void addImg2Disk(String key, InputStream inputStream) {

        String MD5key = MD5Util.getMD5Str(key);
        try {
            DiskLruCache.Editor editor = mDiskLruCache.edit(MD5key);
            OutputStream outputStream = editor.newOutputStream(0);
            if (writeImgToDisk(outputStream, inputStream)) {
                editor.commit();
            } else {
                editor.abort();
            }
            mDiskLruCache.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /** * 将图片写入SD卡 * * @param outputStream 文件的输出流 * @param inputStream 数据的输入流 * @return 写入操作是否成功 */
    private boolean writeImgToDisk(OutputStream outputStream, InputStream inputStream) {
        BufferedInputStream bis = new BufferedInputStream(inputStream);
        BufferedOutputStream bos = new BufferedOutputStream(outputStream);
        int b;
        try {
            while ((b = bis.read()) != -1) {
                bos.write(b);
            }
            bis.close();
            bos.flush();
            bos.close();
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
    /** * 从SD卡内加载所需图片 * @param key 存取key * @param reqWidth * @param reqHeight * @return */
    private Bitmap loadImgFromDisk(String key, int reqWidth, int reqHeight) {
        Bitmap bitmap = null;
        String MD5key = MD5Util.getMD5Str(key);
        try {
            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(MD5key);
            if (null != snapshot) {
                //该处传入参数0的意义不做赘述,技术细节延后讨论
                FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(0);
                FileDescriptor fd = fileInputStream.getFD();
                bitmap = ImgResizer.decodeFromFileDescriptor(fd, reqWidth, reqHeight);
                if (null != bitmap) {
                    addImg2Memory(key, bitmap);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }

如果在SD卡缓存内同样没有找到图片,则需要从网络进行加载(在这里我们不做网络加载的优化的说明和探讨,只给出最简单的网络请求加载方式。网络请求的优化包括使用线程池调度请求和断点续传等留待下次专门开一篇博客进行探讨)

    /** * 从网络获取图片 * @param url 获取图片的链接 * @return 网络请求得到的输入流 */
    private InputStream reqImgFromHttp(String url) {
        try {
            URL httpUrl = new URL(url);
            InputStream inputStream = httpUrl.openConnection().getInputStream();
            return inputStream;
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

至此,我们的三级缓存是编写完了。我们来完善我们最开始定义的同步加载和异步加载的方法(这里只对异步加载的方法进行说明,同步加载就不做赘述了):

    /** * 异步的方式将图片绑定到控件上 * * @param source 图片来源 * @param img 需要绑定的控件 */
    public void bind(final String source, final ImageView img, final int reqWidth, final int reqHeight) {
        if (TextUtils.isEmpty(source) || img == null) {
            return;
        }
        Bitmap bitmap = loadImgFromMemory(source);
        if (null != bitmap) {
            img.setImageBitmap(bitmap);
        } else {
            bitmap = loadImgFromDisk(source, reqWidth, reqHeight);
            if (null != bitmap) {
                img.setImageBitmap(bitmap);
            } else {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        InputStream inputStream = reqImgFromHttp(source);
                        addImg2Disk(source, inputStream);
                        final Bitmap bitmap = ImgResizer.decodeFromStream(inputStream, reqWidth, reqHeight);
                        if (bitmap == null) {
                            return;
                        }
                        addImg2Memory(source, bitmap);
                        Looper looper = Looper.getMainLooper();
                        Handler handler = new Handler(looper);
                        handler.post(new Runnable() {
                            @Override
                            public void run() {
                                img.setImageBitmap(bitmap);
                            }
                        });
                    }
                }).start();
            }
        }

    }

至此,我们的图片加载框架主体就编写完成了,当然我们还需要看一个问题就是图片的按需缩放,这边就不过多说明了,提供一下源码吧。

package com.york.devbase.imgload;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import java.io.FileDescriptor;
import java.io.InputStream;

/** * Created by york_zhang on 2016/3/24. */
public class ImgResizer {
    /** * 计算bitmap的缩放比例 * * @param options bitmap的原始相关信息 * @param reqWidth 所需的图片的宽 * @param reqHeight 所需的图片的高 * @return bitmap的缩放比例 */
    public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        if (reqWidth == 0 || reqHeight == 0) {
            return 1;
        }

        final int width = options.outWidth;
        final int height = options.outHeight;
        int inSampleSize = 1;
        if (height > reqHeight || width > reqWidth) {
            int halfWidth = width / 2;
            int halfHeight = height / 2;
            /** * 确保缩放比例能同时适用控件的宽高 * */
            while ((halfWidth / inSampleSize) > reqWidth) {
                inSampleSize++;
            }

            while ((halfHeight / inSampleSize) > reqHeight) {
                inSampleSize++;
            }
        }
        return inSampleSize;
    }

    /** * 从文件内读取bitmap * ps:该处不调用BitmapFactory.decodeFile()原因在于,FileInputStream是一种有序的文件流, * 两次调用decode方法会影响文件流的位置属性,导致第二次调用的时候得到的bitmap为null * * @param fd 文件 * @param reqWidth 需要加载图片的宽度 * @param reqHeight 需要加载图片的高度 * @return 缩放后的bitmap */
    public static Bitmap decodeFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        /** * 设置inJustDecodeBounds为true,只读取bitmap的相关参数,不会真正解析bitmap */
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fd, null, options);
        int inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        options.inSampleSize = inSampleSize;
        /** * 设置inJustDecodeBounds为false,真正解析bitmap */
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd, null, options);
        return bitmap;
    }

    /** * 从流内解析并按需求压缩bitmap * * @param inputStream 待解析的流 * @param reqWidth 需要加载图片的宽度 * @param reqHeight 需要加载图片的高度 * @return 缩放后的bitmap */
    public static Bitmap decodeFromStream(InputStream inputStream, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        /** * 设置inJustDecodeBounds为true,只读取bitmap的相关参数,不会真正解析bitmap */
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(inputStream, null, options);
        int inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        options.inSampleSize = inSampleSize;
        /** * 设置inJustDecodeBounds为false,真正解析bitmap */
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
        return bitmap;
    }

}

下面我们看一下这个框架的使用(使用起来特别方便):

ImgLaodManager imgLaodManager = new ImgLaodManager();
imgLaodManager.init(new ImgLoaderConfig(),this);
imgLaodManager.bind("http://upload.news.cecb2b.com/2014/1209/1418100017289.jpg", mImageView,ScreenUtils.dip2px(100),ScreenUtils.dip2px(100));

到这里我们整个图片三级缓存和加载框架就算完成了雏形,接下来我们抽时间把它做进一步完善,方便我们把它用到我们实际的项目开发中去。

你可能感兴趣的:(android,图片)