Android——自定义简化版ImageLoader

在上一篇我们已经先后介绍了 Bitmap 的高效加载方式、LruCache 以及 DiskLruCache,那么我们就动手来写一个简化版的 ImageLoader 吧!!!

一般来说,一个优秀的 ImageLoader 应该具备如下功能:

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

声明一下,这里实现的 ImageLoader 并不是为了写一个框架,而是纯粹的加深下三级缓存以及图片的高效加载而写的一个小Demo。

1、图片压缩功能的实现

图片压缩在上一篇博客中已经做了介绍,这里就不在废话了,为了有良好的设计风格,这里单独抽象了一个类似于完成图片的压缩功能,这个类叫 ImageResizer,它的实现如下:

public class ImageResizer {
    private static final String TAG = "ImageResizer";

    public ImageResizer() {

    }

    /**
     * 根据指定的资源文件以及指定的宽/高进行等比例缩放
     * @param res
     * @param resId
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    /**
     * 根据文件流的文件描述符以及指定的宽/高进行等比例缩放
     * @param fileDescriptor
     * @param reqWidth
     * @param reqHeight
     * @return
     *
     */
    public static Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fileDescriptor, int reqWidth, int reqHeight) {

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        Log.e("00000000", reqWidth + "=====" + reqHeight + "======" + options.inSampleSize);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
    }

    /**
     * 根据指定的宽/高进行 2 的指数缩放
     *
     * @param options
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        final int width = options.outWidth;
        int height = options.outHeight;
        int inSampleSize = 1;
        if (reqHeight > 0 || reqHeight > 0) {
            if (width > reqHeight && height > reqHeight) {
                final int halfWidth = width / 2;
                final int halfHeight = height / 2;
                while (halfWidth / inSampleSize >= reqWidth
                        && halfHeight / inSampleSize >= reqHeight) {
                    inSampleSize *= 2;
                }
            }
        }
        return inSampleSize;
    }
}

2、内存缓存的实现

public class MemoryCache {
    private static MemoryCache instance;
    private LruCache<String, Bitmap> mMemoryCache;

    private MemoryCache() {
        //获取当前进程的可用内存
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                //完成bitmap对象大小的计算
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
    }

    public static MemoryCache getInstance() {
        if (instance == null) {
            synchronized (MemoryCache.class) {
                if (instance == null) {
                    instance = new MemoryCache();
                }
            }
        }
        return instance;
    }
    public void addBitmapToMemoryCache(String uri, Bitmap bitmap) {
        String key = MD5.hashKeyFormUrl(uri);
        mMemoryCache.put(key, bitmap);
    }

    public Bitmap getBitmapFromMemoryCache(String key) {
        return mMemoryCache.get(key);
    }
}

没什么好说的,略过…

3、磁盘缓存的实现

public class DiskCache {
    private static DiskCache instance;
    private final long DISK_CACHE_SIZE = 1024 * 1024 * 50;  //可缓存的大小
    private DiskLruCache mDiskLruCache;
    public static final int DISK_CACHE_INDEX = 0;
    private final int IO_BUFFER_SIZE = 8 * 1024;    //缓冲流的大小

    private DiskCache(Context context) {
        File diskCacheDir = FilePath.getDiskCacheDir(context, "bitmap");
        if (!diskCacheDir.exists()) {
            diskCacheDir.mkdirs();
        }
        //判断当前sdk缓存目录可用大小是否满足我们设置的缓存大小
        if (FilePath.getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
            try {
                mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static DiskCache getInstance(Context context) {
        if (instance == null) {
            synchronized (DiskCache.class) {
                if (instance == null) {
                    instance = new DiskCache(context);
                }
            }
        }
        return instance;
    }

    public Bitmap get(String uri) throws IOException {
        FileDescriptor fileDescriptor = getFileDescriptor(uri);
        if (fileDescriptor != null) {
            return ImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, 0, 0);
        } else {
            return null;
        }
    }

    public Bitmap get(String uri, int reqWidth, int reqHeight) throws IOException {
        FileDescriptor fileDescriptor = getFileDescriptor(uri);
        if (fileDescriptor != null) {
            return ImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
        } else {
            return null;
        }
    }

    public void edit(String uri) throws IOException {
        String key = MD5.hashKeyFormUrl(uri);
        DiskLruCache.Editor editor = null;
        //mDiskLruCache在sdk缓存空间小于 DISK_CACHE_INDEX 可能为null
        if (mDiskLruCache != null) {
            editor = mDiskLruCache.edit(key);
        }
        if (editor != null) {
            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
            if (downloadUrlToStream(uri, outputStream)) {
                editor.commit();
            } else {
                editor.abort();
            }
            mDiskLruCache.flush();
        }

    }

    public 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();
            urlConnection.connect();
            InputStream inputStream = urlConnection.getInputStream();
            in = new BufferedInputStream(inputStream, IO_BUFFER_SIZE);
            out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);

            int b = 0;
            while ((b = in.read()) != -1) {
                out.write(b);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return false;
    }

    private FileDescriptor getFileDescriptor(String uri) throws IOException {
        //mDiskLruCache在sdk缓存空间小于 DISK_CACHE_INDEX 可能为null
        if (mDiskLruCache != null) {
            String key = MD5.hashKeyFormUrl(uri);
            Bitmap bitmap = null;
            FileDescriptor fileDescriptor = null;
            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
            if (snapshot != null) {
                FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DiskCache.DISK_CACHE_INDEX);
                fileDescriptor = fileInputStream.getFD();
            }
            return fileDescriptor;
        } else {
            return null;
        }
    }
}

在创建磁盘缓存时,这里做了一个判断,即有可能磁盘剩余空间小于磁盘缓存所需要的大小,一般是指用户的手机空间不足了,因此没有办法创建磁盘缓存,这个时候磁盘缓存就会失效。

磁盘缓存的添加以及读取功能稍微复杂一些,这里在简单说明一下。磁盘缓存的添加需要通过 Editor 来完成,Editor 提供了 commit 和 abort 方法来提交和撤销对文件系统的写操作,具体实现请看 DiskCache 的 edit 方法。磁盘缓存的读取需要通过 Snapshot 来完成,通过 Snapshot 可以得到磁盘缓存对象对应的 FileInputStream,但是 FileInputStream 无法便捷地进行压缩,所以通过 FileDescriptor 来加载压缩后的图片,最后将加载后的 bitmap 添加到内存缓存中,具体实现请参看 DiskCache 的 get 方法

4、同步加载

public Bitmap loadBitmap(String uri, ImageView imageView) {
        Bitmap bitmap = loadBitmapFromMemCache(uri);
        if (bitmap != null) {
            return bitmap;
        }

        bitmap = loadBitmapFromDiskCache(uri, imageView);
        if (bitmap != null) {
            return bitmap;
        }

        bitmap = loadBitmapFromHttp(uri, imageView);
        if (bitmap != null) {
            return bitmap;
        }

        return downloadBitmapFromUrl(uri);

    }

从 loadBitmap 的实现可以看出,其工作过程遵循如下几步:首先尝试从内存缓存中读取图片,接着尝试冲磁盘缓存中读取图片,最后才从网络中拉取图片。另外这个方法不能在主线程中调用,否则就抛出异常。这个执行环境的检测是在各种缓存获取方法中实现的,通过检测当前线程的 Looper 是否为主线程的 Looper 来判断当前线程是否是主线程,如果是主线程就直接抛出异常终止程序。如下所示:

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

5、异步加载

    public void bindBitmap(final String uri, final ImageView imageView) {
        imageView.setTag(TAG_KEY_URI, uri);
        Bitmap bitmap = loadBitmapFromMemCache(uri);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }

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

        THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
    }

从 bindBitmap 的实现来看,bindBitmap 方法会尝试从内存缓存中获取图片,如果获取成功就直接返回结果,否则会在线程池中去调用 loadBitmap 方法,当图片加载成功后再将图片、图片的地址以及需要绑定的 ImageView 封装成一个 LoaderResult 对象,然后再通过 mMainHandler 向主线程发送一个消息,这样就可以在主线程中给 ImageView 设置图片了,之所以通过 Handler 来中转是因为子线程无法访问 UI。

bindBitm 中用到了线程池和 Handler,这里看一下它们的实现,首先看线程池 THREAD_POOL_EXECUTOR 的实现,如下所示。可以看出它的核心线程数为当前设备的 CPU 核心数 +1,最大容量为 CPU 核心数的 2 倍 +1,线程闲置超时时长为 10 秒,关于线程池的详细介绍我们在 Android——线程和线程池 已经详细介绍过。

    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 int KEEP_ALIVE = 10;   //存活时间

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

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

    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>();    //等待队列

    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
            CORE_POOL_SIZE,
            MAXIMUM_POOL_SIZE,
            KEEP_ALIVE,
            TimeUnit.SECONDS,
            sPoolWorkQueue,
            sThreadFactory);

之所以采用线程池是有原因的,首先肯定不能采用普通的线程去做这个事,线程池的好处上篇博客已经说明。如果直接采用普通的线程去加载图片,随着列表的滑动这可能会产生大量的线程,这样并不利于整体效率的提升。另外一点,这里也没有选择采用 AsyncTask,AsyncTask 封装了线程池和 Handler,按道理它应该适合 ImageLoader 的场景。从 Android——线程和线程池 我们对 AsyncTask 的分析可以知道,AsyncTask 在 3.0 的低版本和高版本上具有不同的表现,在 3.0 以上的版本 AsyncTask 无法实现并发的效果,这显然是不能接受的,因为 ImageLoader 就是需要并发特性,虽然可以通过改造 AsyncTask 或者使用 AsyncTask 的 executeOnExecutor 方法的形式来执行异步任务,但是这总归是不太自然的实现f方式。鉴于以上两点原因,这里选择线程池和 Handler 来提供 ImageLoader 的并发能力和访问 UI 的能力。

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

    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.uri)) {
                imageView.setImageBitmap(result.bitmap);
            } else {
                Log.w(TAG, "set image bitmap,but url has changed, ignored!");
            }
        }
    };

到此为止,ImageLoader 的细节都已经做了全面的分析,下面是 ImageLoader的完整代码:

public class ImageLoader {
    private static ImageLoader instance;
    private Context mContext;
    private final int TAG_KEY_URI = R.id.imageloader_uri;
    private final String TAG = "ImageLoader";
    private 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 int KEEP_ALIVE = 10;   //存活时间

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

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

    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>();    //等待队列

    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
            CORE_POOL_SIZE,
            MAXIMUM_POOL_SIZE,
            KEEP_ALIVE,
            TimeUnit.SECONDS,
            sPoolWorkQueue,
            sThreadFactory);
    //更新 ImageView
    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.uri)) {
                imageView.setImageBitmap(result.bitmap);
            } else {
                Log.w(TAG, "set image bitmap,but url has changed, ignored!");
            }
        }
    };


    private ImageLoader() {

    }

    public static ImageLoader getInstance() {
        if (instance == null) {
            synchronized (ImageLoader.class) {
                if (instance == null) {
                    instance = new ImageLoader();
                }
            }
        }
        return instance;
    }

    public void init(Context context) {
        this.mContext = context;
    }

    /**
     * 异步加载
     * @param uri
     * @param imageView
     */
    public void bindBitmap(final String uri, final ImageView imageView) {
        imageView.setTag(TAG_KEY_URI, uri);
        Bitmap bitmap = loadBitmapFromMemCache(uri);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }

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

        THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
    }

    /**
     * 同步加载
     * @param uri
     * @param imageView
     * @return
     */
    public Bitmap loadBitmap(String uri, ImageView imageView) {
        Bitmap bitmap = loadBitmapFromMemCache(uri);
        if (bitmap != null) {
            return bitmap;
        }

        bitmap = loadBitmapFromDiskCache(uri, imageView);
        if (bitmap != null) {
            return bitmap;
        }

        bitmap = loadBitmapFromHttp(uri, imageView);
        if (bitmap != null) {
            return bitmap;
        }

        return downloadBitmapFromUrl(uri);

    }

    /**
     * 从网络获取bitmap
     * @param urlString
     * @return
     */
    private Bitmap downloadBitmapFromUrl(String urlString) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("can not visit network form UI Thread.");
        }
        return DownLoad.downLoadBitmapFromUrl(urlString);
    }

    /**
     * 从网络获取bitmap,并添加到磁盘缓存
     * @param uri
     * @param imageView
     * @return
     */
    private Bitmap loadBitmapFromHttp(String uri, ImageView imageView) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("can not visit network form UI Thread.");
        }
        try {
            DiskCache.getInstance(mContext).edit(uri);
            return loadBitmapFromDiskCache(uri, imageView);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 从磁盘缓存中获取bitmap
     * @param uri
     * @param imageView
     * @return
     */
    private Bitmap loadBitmapFromDiskCache(String uri, ImageView imageView) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("can not visit network form UI Thread.");
        }

        Bitmap bitmap = null;
        try {
            bitmap = DiskCache.getInstance(mContext).get(uri, imageView.getWidth(), imageView.getHeight());
            if (bitmap != null) {

                MemoryCache.getInstance().addBitmapToMemoryCache(uri, bitmap);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }

    /**
     * 从内存缓存中获取bitmap
     * @param uri
     * @return
     */
    private Bitmap loadBitmapFromMemCache(String uri) {
        final String key = MD5.hashKeyFormUrl(uri);
        return MemoryCache.getInstance().getBitmapFromMemoryCache(key);
    }

    /**
     * 返回结果的封装
     */
    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;
        }
    }
}

其他相关类:

//MD5
public class MD5 {
    public static String hashKeyFormUrl(String url){
        String cacheKey = url;
        try {
            MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(url.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        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();
    }
}
//路径获取相关类 FilePath
public class FilePath {
    public static File getDiskCacheDir(Context context, String uniqueName) {
        boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
        final String cachePath;
        if (externalStorageAvailable) {
            cachePath = context.getExternalCacheDir().getParent();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

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

}

源码下载地址:

http://download.csdn.net/detail/akaic/9659585

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值