Universal-Image-Loader 源码的一些理解和分析

本文详细分析了Universal-Image-Loader框架的源码,包括整体架构、图片下载、磁盘与内存缓存策略、图像解码、位图处理及显示等关键步骤。通过对源码的深入理解,揭示了框架如何高效地处理图片加载,特别是其灵活的缓存机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这就是一个非常好用的图片加载框架,目前我们项目中用的就是它。
源码地址:https://github.com/nostra13/Android-Universal-Image-Loader

一.整体架构:

先说怎么看整个源码:我是根据下面这种图开始看的,另外,我觉得作者源码包的分类也非常明确清晰,这个都可以帮助我们一步一步来看整个框架是怎么构建而成的。
这里写图片描述

二.分步解析

1. Download Image:

这里写图片描述
这个包中,主要是将不同图片来源(http/https, file,contentprovider,asserts,drawable目录),都解析成InputStream 。针对多媒体也有获取到帧的方法。作者建议是对于drawable目录的图片,调用android原生方法就好,因为这个框架针对这种资源也是在调用android原生方法。
详见如下代码:

@Override
    public InputStream getStream(String imageUri, Object extra) throws IOException {
        switch (Scheme.ofUri(imageUri)) {
            case HTTP:
            case HTTPS:
                return getStreamFromNetwork(imageUri, extra);
            case FILE:
                return getStreamFromFile(imageUri, extra);
            case CONTENT:
                return getStreamFromContent(imageUri, extra);
            case ASSETS:
                return getStreamFromAssets(imageUri, extra);
            case DRAWABLE:
                return getStreamFromDrawable(imageUri, extra);
            case UNKNOWN:
            default:
                return getStreamFromOtherSource(imageUri, extra);
        }
    }

我们日常中的图片都是从网络中获取的,那么针对http/https图片的获取,框架底层是通过HttpURLConnection 进行的。

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) {
            // Read all data to allow reuse connection (http://bit.ly/1ad35PY)
            IoUtils.readAndCloseStream(conn.getErrorStream());
            throw e;
        }
        if (!shouldBeProcessed(conn)) {
            IoUtils.closeSilently(imageStream);
            throw new IOException("Image request failed with response code " + conn.getResponseCode());
        }

        return new ContentLengthInputStream(new BufferedInputStream(imageStream, BUFFER_SIZE), conn.getContentLength());
    }

不同资源途径下的图片如何解析成InputStream就差不多分析完毕,也就是图片下载这个工作差不多告一段落,现在看图片加载完毕之后,怎么保存的问题。

2.图片的缓存:

看包的目录,可知作者做了两种类型的缓存,一种是磁盘缓存,一种是内存缓存。并且针对这些缓存,提供了不同算法实现方式。

这里写图片描述

2.1 磁盘缓存:

以LimitedAgeDiskCache 分析为例:
这个机制是缓存大小不受限制,但是会有缓存时限。

    @Override
    public File get(String imageUri) {
        File file = super.get(imageUri);
        if (file != null && file.exists()) {
            boolean cached;
            Long loadingDate = loadingDates.get(file);//获取缓存的文件的时间
            if (loadingDate == null) {//没有缓存的情况
                cached = false;
                loadingDate = file.lastModified();
            } else {//有缓存的情况
                cached = true;
            }

            if (System.currentTimeMillis() - loadingDate > maxFileAge) {//如果文件已经超过了缓存的期限,删掉该文件
                file.delete();
                loadingDates.remove(file);
            } else if (!cached) {//没有缓存的情况下,并且还在期限内:将该文件缓存下来
                loadingDates.put(file, loadingDate);
            }
        }
        return file;
    }

//文件存储:
@Override
    public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
        boolean saved = super.save(imageUri, imageStream, listener);
        rememberUsage(imageUri);
        return saved;
    }

    private void rememberUsage(String imageUri) {
        File file = getFile(imageUri);
        long currentTime = System.currentTimeMillis();
        file.setLastModified(currentTime);//更新文件的最新时间
        loadingDates.put(file, currentTime);
    }

另外还有一种:LruDiskCache ,android原生中有LruCache,里面封装了一个LinkedHashMap,这种缓存策略是有大小限制的,再次用过的元素放在队尾,当大小超过maxsize的时候,将队头元素删除,这样既不会超过尺寸,也保证删除的是最近最少使用的,原理大致如此。

2.2 内存缓存:

内存缓存:其中的LRULimitedMemoryCache,里面封装了一个LinkedHashMap,按照访问顺序排列,既在get方法 的时候,重新将该元素放在队尾:

private final Map<String, Bitmap> lruCache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(INITIAL_CAPACITY, LOAD_FACTOR, true));

@Override public V get(Object key) {
        /*
         * This method is overridden to eliminate the need for a polymorphic
         * invocation in superclass at the expense of code duplication.
         */
        if (key == null) {
            HashMapEntry<K, V> e = entryForNullKey;
            if (e == null)
                return null;
            if (accessOrder)
                makeTail((LinkedEntry<K, V>) e);
            return e.value;
        }

        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
                e != null; e = e.next) {
            K eKey = e.key;
            if (eKey == key || (e.hash == hash && key.equals(eKey))) {
                if (accessOrder)
                    makeTail((LinkedEntry<K, V>) e);
                return e.value;
            }
        }
        return null;
    }

private void makeTail(LinkedEntry<K, V> e) {
        // Unlink e
        e.prv.nxt = e.nxt;
        e.nxt.prv = e.prv;

        // Relink e as tail
        LinkedEntry<K, V> header = this.header;
        LinkedEntry<K, V> oldTail = header.prv;
        e.nxt = header;
        e.prv = oldTail;
        oldTail.nxt = header.prv = e;
        modCount++;
    }
3.Image Decoder

这个业务是将图像解码为Bitmap,并将其缩放到所需大小。
这里写图片描述

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

        InputStream imageStream = getImageStream(decodingInfo);//获取图片流,这里就回到了第一次的分析:采用imageloader下载图片流
        if (imageStream == null) {
            L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey());
            return null;
        }
        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;
    }
4.Bitmap Processor:

在这个步骤,就是关于图片如何处理的问题了。
不过这个是一个接口,由调用者自己去实现。

5. Bitmap Display:

图片展示过程有两种方法:
第一种是针对图片的展示displayImage ,调用如下的方法:

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
            ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
        checkConfiguration();//检查ImageLoaderConfiguration  配置,如果用户不想自己去配置的话,作者提供了一个DefaultConfigurationFactory ,里面有默认做线程,缓存dowmloadimage,display等相关配置。
        if (imageAware == null) {
            throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
        }
        if (listener == null) {
            listener = defaultListener;
        }
        if (options == null) {//图片的展示配置,比如图片uri异常问题,图片处理等等
            options = configuration.defaultDisplayImageOptions;
        }

        ......
        ......
        //获取图片缓存相关代码
        ......
        ......

        if (bmp != null && !bmp.isRecycled()) {//有缓存的图片且没有被回收
            L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);

            if (options.shouldPostProcess()) {//如果图片需要后期处理的话
                ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                        options, listener, progressListener, engine.getLockForUri(uri));
                ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
                        defineHandler(options));//底层调用LoadAndDisplayImageTask 
                if (options.isSyncLoading()) {
                    displayTask.run();
                } else {
                    engine.submit(displayTask);
                }
            } else {//如果图片不需要处理的话,直接展示图片
                options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
                listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
            }
        } else {//图片没有缓存过
            if (options.shouldShowImageOnLoading()) {
                imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
            } else if (options.isResetViewBeforeLoading()) {
                imageAware.setImageDrawable(null);
            }


            ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                    options, listener, progressListener, engine.getLockForUri(uri));

                //下载图片任务
            LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
                    defineHandler(options));
            if (options.isSyncLoading()) {
                displayTask.run();
            } else {
                engine.submit(displayTask);
            }
        }
    }

第二种是loadImage,往线程池中 增加了图片下载的任务,最后可以看到会调用上面的displayImage方法。

public void loadImage(String uri, ImageSize targetImageSize, DisplayImageOptions options,
            ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
        checkConfiguration();
        if (targetImageSize == null) {
            targetImageSize = configuration.getMaxImageSize();
        }
        if (options == null) {
            options = configuration.defaultDisplayImageOptions;
        }

        NonViewAware imageAware = new NonViewAware(uri, targetImageSize, ViewScaleType.CROP);
        displayImage(uri, imageAware, options, listener, progressListener);
    }

图片加载一般是耗时的,所以也是异步的,不过该框架也提供了一个同步的加载方法:

public Bitmap loadImageSync(String uri, ImageSize targetImageSize, DisplayImageOptions options) {
        if (options == null) {
            options = configuration.defaultDisplayImageOptions;
        }
        options = new DisplayImageOptions.Builder().cloneFrom(options).syncLoading(true).build();

        SyncImageLoadingListener listener = new SyncImageLoadingListener();
        loadImage(uri, targetImageSize, options, listener);
        return listener.getLoadedBitmap();
    }


//从loadImage中可以追踪到displayimage 中一个方法:
    if (options.isSyncLoading()) {//如果是同步,交给这个线程去执行,执行完毕再执行下一个任务
                    displayTask.run();
                } else {//如果是异步,交给线程池去执行,多个任务可以同时执行
                    engine.submit(displayTask);
                }

还有最后一个补充点就是关于bitmap display:
框架中提供了如下几种方法:默认是用SimpleBitmapDisplayer ,不做任何处理,但是如果需要对图片四个角做圆角处理的话,可以选择用RoundedBitmapDisplayer

这里写图片描述

总结:

从整体来看,该框架中最为重要的是图片加载和图片缓存,特别是图片缓存这块。不过为了满足开发需求,作者也已经提供了多种算法的缓存策略以满足实际开发需要。我对这个的源码分析可能有些不准确和完整,但是却是自己第一次尝试去做这件事,看的过程中对作者框架的构思也有了一定的了解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值