着手实现一个优秀的 ImageLoader

Cover image
🔖 Table of Contents

Generally speaking, a perfect ImageLoader should have serval functions as following:

  • 图片的同步加载
  • 图片的异步加载
  • 图片压缩
  • 三级缓存机制

    • 内存缓存
    • 磁盘缓存
    • 网络请求

所以,搭配 Bitmap,使用 LruCache 以及 DiskLruCache 的三级缓存机制,就可以实现一个简单的、优秀的 ImageLoader。

压缩功能

图片压缩的作用毋庸置疑,是降低 OOM 的有效手段之一,一个 ImageLoader 必须合理地处理图片压缩问题。

public class ImageResizer {

    private static final String TAG = "ImageResizer";

    public ImageResizer() {

    }

    /**
     * 从 resource 中进行 decode
     *
     * @param res
     * @param resId
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public Bitmap decodeSampleBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        // 检查尺寸
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);

        // 计算大小
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

        // decode
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    /**
     * 从 FileDescriptor 中进行 decode
     *
     * @param fd
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public Bitmap decodeSampleBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fd, null, options);

        // 计算大小
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

        // decode
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fd, null, options);
    }

    /**
     * 计算尺寸
     *
     * @param options
     * @param reqWidth     需要压缩到的尺寸宽度
     * @param reqHeight    需要压缩到的尺寸高度
     * @return
     */
    public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        if (reqWidth == 0 || reqHeight == 0) {
            return 1;
        }

        // 原生尺寸大小
        final int height = options.outHeight;
        final int width = options.outWidth;
        Log.d(TAG, "origin, w=" + width + " h=" + height);
        int inSampleSize = 1;

        // 如果原生尺寸比目标尺寸大
        if (height > reqHeight || width > reqWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }

        Log.d(TAG, "sampleSize:" + inSampleSize);
        return inSampleSize;
    }
}

图片同步加载

同步加载就是在主线程中加载图片。加载机制仍然是遵循三级缓存机制,首先是内存缓存,然后是磁盘缓存,最后是从网络获取图片。

public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
    // 先尝试从内存缓存中读取图片
    Bitmap bitmap = loadBitmapFromMemCache(uri);
    if (bitmap != null) {
        Log.d(TAG, "loadBitmapFromMemCache, url:" + uri);
        return bitmap;
    }

    try {
        // 再尝试从磁盘缓存中读取图片
        bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
        if (bitmap != null) {
            Log.d(TAG, "loadBitmapFromDisk, url:" + uri);
            return bitmap;
        }
        // 最后从网络中获取图片,因为网络请求是异步加载的,所以使用封装好的方法
        bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
        Log.d(TAG, "loadBitmapFromHttp, url:" + uri);
    } catch (IOException e) {
        e.printStackTrace();
    }

    // 如果 bitmap 仍然为空,并且磁盘缓存未创建
    if (bitmap == null && !mIsDiskLruCacheCreated) {
        Log.w(TAG, "encounter error, DiskLruCache is not created.");
        // 根据提供的 uri 下载图片
        bitmap = downloadBitmapFromUrl(uri);
    }
    return bitmap;
}

图片异步加载

public void bindBitmap(final String uri, final ImageView imageView, final int reqWidth, final int reqHeight) {
    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, reqWidth, reqHeight);
            if (bitmap != null) {
                // 将 ImageView、URI、Bitmap 封装成一个 LoaderResult 对象
                LoaderResult result = new LoaderResult(imageView, uri, bitmap);
                // 通过 Handler 向主线程中发送一个消息
                mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
            }
        }
    };
    // 在线程池中调用 loadBitmapTask 线程
    THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}

bindBitmap()方法是在线程池中调用loadBitmap方法加载,加载成功后将图片、图片的地址以及 imageView 封装成一个对象,通过 Handler 向主线程发送一个消息,主线程就可以给 imageView 设置图片。

那么,线程池是如何实现的呢?

private static final ThreadFactory sThreadFactory = new ThreadFactory() {

    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_POOL_EXECUTOR = new ThreadPoolExecutor(
        CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS,
        new LinkedBlockingQueue<Runnable>(), sThreadFactory
);

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

通过线程池,可以避免产生大量的线程去加载图片,从而不利于整体效率的提升。所以,这里选择线程池和 Handler 来提供 ImageLoader 的并发能力。因为 AysncTask 的底层也是通过线程池和 Handler 实现的,所以也可以使用 AsyncTask。

其余方法

除上述描述之外,还有一些基本的方法,例如将图片添加进缓存,从网络下载图片的方法等。

// 将图片添加进内存缓存中
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}

// 从内存缓存中获取图片
private Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}

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

// 从磁盘缓存中加载图片
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException {
    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);
    // 磁盘缓存的读取需要通过 Snapshot 来完成,通过 Snapshot 可以得到磁盘缓存对象对应的 FileInputStream
    DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
    if (snapshot != null) {
        FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
        // 因为 FileInputStream 无法便捷地进行压缩,通过 FileDescriptor 来加载压缩后的图片
        FileDescriptor fileDescriptor = fileInputStream.getFD();
        bitmap = mImageResizer.decodeSampleBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
        if (bitmap != null) {
            // 将加载过后的 Bitmap 添加到内存缓存中
            addBitmapToMemoryCache(key, bitmap);
        }
    }
    return bitmap;
}

关于从磁盘缓存中加载图片,又涉及到文件的读写,同时需要导入一个 Android SDK 中没有的 DiskLruCache 类。

接下来是从三级缓存机制中的最后一个,从网络中拉取图片。

// 从网络中拉取图片
private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException {
    // 因为耗时复杂的请求不能在主线程中调用,所以需要检查是否为主线程
    // 通过检查当前线程的 Looper 是否为主线程的 Looper 来判断
    if (Looper.myLooper() == Looper.getMainLooper()) {
        throw new RuntimeException("can not visit network from UI Thread.");
    }
    if (mDiskLruCache == null) {
        return null;
    }

    String key = hashKeyFormUrl(url);
    // 磁盘缓存的添加需要通过 Editor 来完成
    // Editor 提供了 commit 和 abort 方法来提交和撤销对文件系统的写操作
    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);
}
// 根据 url 连接,以输出流的方式下载
public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
    HttpURLConnection connection = null;
    BufferedOutputStream out = null;
    BufferedInputStream in = null;
    try {
        final URL url = new URL(urlString);
        connection = (HttpURLConnection) url.openConnection();
        in = new BufferedInputStream(connection.getInputStream(), IO_BUFFER_SIZE);
        out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);

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

最后的方法就是假如三级缓存中并未找到该图片,那么需要重新从网络中,根据提供的 uri 下载图片,使用的是原生的HttpURLConnection

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();
        }
    }
    return bitmap;
}
◀ DevOps 初学者的入门指南CustomNet:一个简单网络框架的设计与实现 ▶