Universal ImageLoader源码分析

毋举
2023-12-01

转载请注明出处谢谢:http://blog.csdn.net/u011504118/article/details/44794819

引言

第一次认识Universal-Image-Loader(以下简称ImageLoader)是因为在RecyclerView中加载图片时候出现了大规模的OOM,所以在网上搜到了这个开源框架(ImageLoader). 试了一下,果然有效果。可是总不能老是依赖外部库吧。于是想自己研究一下这个框架是怎么实现的。所以先搜一下有没有人研究过这个框架,发现了github上面有人对这个框架做了详细介绍的,附上github地址(源码分析). 但还是没有很详细。所以就打算自己研究一下了。

刚打开源码的时候被吓到了,感觉封装得好厉害,一点点慢慢看也不知道要看到什么时候,再加上看了这忘了那的,所以前几次分析失败了。

痛定思痛之后,决定重新打开源码来研究,这一次我不是毫无目的的看了,而是只看这个框架中最重要的一个函数,即图像的加载。好了,废话就说到这了,接下来开始分析吧。

说明:这里会抛弃很多的细节,只关注如何从网络上得到Bitmap对象

正文

首先打开ImageLoader.java并找到display函数,这个函数有很多重载函数,只看那个实际实现的就好了。下面是里面我认为最核心的代码:

// 如果图像缓存在内存中
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp != null && !bmp.isRecycled()) {
    ....
} else {
    ....
    // 加载图片的信息,这里不去关注它
    ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,options, listener, progressListener, engine.getLockForUri(uri));
    // 实现了Runnable接口
    LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,defineHandler(options));
    // 同步加载
    if (options.isSyncLoading()) {
        displayTask.run();
    } else {
        // 我们要找的函数。
        engine.submit(displayTask);
    }
}

接着,找到ImageLoaderEnginesubmit(LoadAndDisplayImageTask task)函数,如下:

void submit(final LoadAndDisplayImageTask task) {
    taskDistributor.execute(new Runnable() {
        @Override
        public void run() {
            File image = configuration.diskCache.get(task.getLoadingUri());
            boolean isImageCachedOnDisk = image != null && image.exists();
            // 先跳过这个函数
            initExecutorsIfNeed();
            // 如果图像缓存在硬盘上
            if (isImageCachedOnDisk) {
                taskExecutorForCachedImages.execute(task);
                } else {
                    // 我们要找的函数
                    taskExecutor.execute(task);
                }
            }
        });
    }

taskExecutor的类型是java.util.concurrent.Executor,关于这个类,我就不做介绍了,因为我也只是大概知道用它可以很方便的实现多线程。调用execute方法后会执行task中的run方法。上面注释中提到,LoadAndDisplayImageTask类实现了Runnable接口,所以我们就去找到它实现的run方法吧。

@Override
public void run() {
    ....

    // 这一部分我想应该是将图像地址锁起来,防止错位的。
    // 先忽略它,以后再来研究。
    ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
    L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);
    if (loadFromUriLock.isLocked()) {
        L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);
    }
    loadFromUriLock.lock();

    ....
    // 先判断图片是否在内存缓存上
    bmp = configuration.memoryCache.get(memoryCacheKey);
    if (bmp == null || bmp.isRecycled()) {
        // 我们要找的函数
        bmp = tryLoadBitmap();
        ....
    }
}

接着在同一个类中找到tryLoadBitmap函数。

....

// 判断是否在磁盘上
File imageFile = configuration.diskCache.get(uri);
if (imageFile != null && imageFile.exists()) {
    L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
    loadedFrom = LoadedFrom.DISC_CACHE;

    checkTaskNotActual();
    bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
}
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
    L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey);
    loadedFrom = LoadedFrom.NETWORK;

    String imageUriForDecoding = uri;
    if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
        imageFile = configuration.diskCache.get(uri);
        if (imageFile != null) {
            imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
        }
    }

    checkTaskNotActual();
    bitmap = decodeImage(imageUriForDecoding);

    // 图片加载失败
    if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
        fireFailEvent(FailType.DECODING_ERROR, null);
    }
}

在这里我有一点还没弄明白的是,在上面提到的,它已经对图片是否在磁盘上判断了一次,这里又判断了一次。希望有知道的朋友可以跟我说下哈。

接着找到decode函数

private Bitmap decodeImage(String imageUri) throws IOException {
    ViewScaleType viewScaleType = imageAware.getScaleType();
    ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
    getDownloader(), options);
    return decoder.decode(decodingInfo);
}

找到decode函数后,发现它是封装在Decoder的唯一接口,所以我们就只能找到decoder对应的实现类了。

decoder= configuration.decoder;

找到后,发现它是从configuration传进来的。而configuration又是我们自己定义的配置,但问题是我们没有自己去定义Decoder的继承类啊。看到这里,是不是有些晕了呢?别急,既然我们没有自定义,那么这个框架肯定给我们提供了一个默认的。我们的任务就是要找到这个默认的类。所以找到ImageLoaderConfiguration这个类,找一下它有没有帮我们做什么。

public Builder imageDecoder(ImageDecoder imageDecoder) {
    this.decoder = imageDecoder;
    return this;
}

看到这里,有点失望了,还是找不到。但在build()找到了这个:

public ImageLoaderConfiguration build() {
    initEmptyFieldsWithDefaultValues();
    return new ImageLoaderConfiguration(this);
}

顺藤摸瓜,找到initEmptyFieldsWithDefaultValues函数。

private void initEmptyFieldsWithDefaultValues() {
    ....

    if (decoder == null) {
        // 在这里`writeLogs`的值为`false`
        decoder = DefaultConfigurationFactory.createImageDecoder(writeLogs);
    }
    ....

}

找了这么久,终于找到了~~~
接着找它的实现:

public static ImageDecoder createImageDecoder(boolean loggingEnabled) {
    return new BaseImageDecoder(loggingEnabled);
}

最后在BaseImageDecoder类中找到了decode方法。

@Override
    public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
    Bitmap decodedBitmap;
    ImageFileInfo imageInfo;

    // 获取输入流
    InputStream imageStream = getImageStream(decodingInfo);
    try {
        imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
        imageStream = resetStream(imageStream, decodingInfo);
        Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
        // 解析输入流
        decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
    } finally {
        IoUtils.closeSilently(imageStream);
    }

    if (decodedBitmap == null) {
        L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
    } else {
        decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
        imageInfo.exif.flipHorizontal);
    }
    return decodedBitmap;
}

在这里,我们终于见到了熟悉的InputStreamBitmapFactory!!!
你以为就完了吗?还没找到它得到输入流的方式呢!接着继续找:

protected InputStream getImageStream(ImageDecodingInfo decodingInfo) throws IOException {
    return decodingInfo.getDownloader().getStream(decodingInfo.getImageUri(), decodingInfo.getExtraForDownloader());
}

再次跟踪,发现getStreamImageDownloader的一个接口,有了上面找decoder的思路,downloader就容易找了。
这里我就直接给出downloader的实现类BaseImageDownloader了。

@Override
public InputStream getStream(String imageUri, Object extra) throws IOException {
    switch (Scheme.ofUri(imageUri)) {
        case HTTP:
        case HTTPS:
            return getStreamFromNetwork(imageUri, extra);
    }
}
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
    HttpURLConnection conn = createConnection(imageUri, extra);

    int redirectCount = 0;
    while (conn.getResponseCode() / 100 == 3 && redirectCount < MAX_REDIRECT_COUNT) {
        conn = createConnection(conn.getHeaderField("Location"), extra);
        redirectCount++;
    }

    InputStream imageStream;
    try {
        imageStream = conn.getInputStream();
    } catch (IOException e) {       
        IoUtils.readAndCloseStream(conn.getErrorStream());
        throw e;
    }
    return new ContentLengthInputStream(new BufferedInputStream(imageStream, BUFFER_SIZE), conn.getContentLength());
}

终于,终于,终于找到它的下载方式了,没错,就是通过HttpURLConnection实现的!!!

总结

找了这么久总算是找到了Bitmap对象的获取方法。最大的感受就是这个框架封装得太厉害了。要用我之前的方法一点一点去理解,去吃透整个框架是很难的。所以我觉得最好的方法就是找到你的关注点,然后抛弃其它杂音,顺藤摸瓜找下去。生活不也需要如此吗?

温馨提示:查看源码的工具强烈推荐Source Insight, 简直就是神器!

第一次追踪源码,如果有写得不好的地方希望大家能够提出来~

 类似资料: