Android-Universal-Image-loader源码解析

本文详细解读了Android-Universal-Image-Loader的源码,包括其缓存结构、参数配置、下载功能以及图片处理流程。重点分析了内存缓存、磁盘缓存的工作原理及实现细节,展示了如何通过配置参数实现高效图片下载与显示,同时介绍了如何利用暂停监听优化性能。文章以实例代码展示了整个下载流程,适合开发者深入了解图片加载库的内部实现。

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

Android-Universal-Image-loader作为看一款老牌的图片下载库,整体代码清晰读起来比较简易可以用来学习图片下载的功能逻辑是一个不错的选择,所以我选择解读Android-Universal-Image-loader的源码。
Android-Universal-Image-loader的github地址:https://github.com/nostra13/Android-Universal-Image-Loader

源码缓存结构

首先我们看一下源码的文件结构:
这里写图片描述

这里写图片描述

我们可以看出*Cache缓存提供了很多种的算法进行缓存图片,我们开发者可以根据具体的情况来选择最好的一种算法去缓存图片。最常见的是LruMemoryCache和LruDiskCache我们需要注意的是Android-Universal-Image-loader中并没有使用Google提供的LruCache源码,而是自己实现了一个跟LruCache一样的缓存来存储图片。

我们先看下memory缓存:
首先定义了一个MemoryCache接口用于规范了memory缓存的提供的功能。

public interface MemoryCache {
    /**
     * Puts value into cache by key
     *
     * @return <b>true</b> - if value was put into cache successfully, <b>false</b> - if value was <b>not</b> put into
     * cache
     */
    boolean put(String key, Bitmap value);

    /** Returns value by key. If there is no value for key then null will be returned. */
    Bitmap get(String key);

    /** Removes item by key */
    Bitmap remove(String key);

    /** Returns all keys of cache */
    Collection<String> keys();

    /** Remove all items from cache */
    void clear();
}

然后我们定义了LruMemoryCache来统一了基本的memory缓存功能,通过构造接口让代码进行初始化它。

/** @param maxSize Maximum sum of the sizes of the Bitmaps in this cache */
    public LruMemoryCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
    }

我们通过LinkedHashMap来存放需要放到内存中的Bitmap对象值,在初始化的时候由于我们设置了maxSize最大容量,所以每次put一个新的Bitmap对象的时候我们就需要计算下占据空间如果占据值都大于maxSize那么就按Lru<最近最少使用>的算法将Bitmap移除出内存。

private void trimToSize(int maxSize) {
        while (true) {
            String key;
            Bitmap value;
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
                }

                if (size <= maxSize || map.isEmpty()) {
                    break;
                }

                Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
                if (toEvict == null) {
                    break;
                }
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= sizeOf(key, value);
            }
        }
    }

sizeof返回的是值是value这个Bitmap对象所占据的内存值,当size > maxSize的时候就把该最近比较少使用的对象移除。
当然每一个*Cache都提供了get、put、remove、clear等操作。memory还提供了keys()这个方法用于读取每个缓存的Bitmap对应的key值。

磁盘缓存源码
磁盘源码根据开源的DiskLruCache作为缓存工具然后提供方法封装了DiskLruCache的每个基础功能操作。所以先定义了一个接口DiskCache定义了几个方法。

public interface DiskCache {
    /**
     * Returns root directory of disk cache
     */
    File getDirectory();
    /**
     * Returns file of cached image
     */
    File get(String imageUri);
    /**
     * Saves image stream in disk cache.
     * Incoming image stream shouldn't be closed in this method.
     */
    boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException;

    /**
     * Saves image bitmap in disk cache.
     */
    boolean save(String imageUri, Bitmap bitmap) throws IOException;
    /**
     * Removes image file associated with incoming URI
     */
    boolean remove(String imageUri);
    /** Closes disk cache, releases resources. */
    void close();
    /** Clears disk cache. */
    void clear();
}

这里仅仅对LruDiskCache类进行解读,LruDiskCache这个implements DiskCache并实现了其中的方法。
save()这个方法提供了方法实现了把InputStream或者Bitmap保存到磁盘中去,使用的方法都是通过DiskLruCache.Editor类的newOutputStream将OutputStream传入实现write到磁盘的功能。

@Override
    public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
        DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
        if (editor == null) {
            return false;
        }

        OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
        boolean copied = false;
        try {
            copied = IoUtils.copyStream(imageStream, os, listener, bufferSize);
        } finally {
            IoUtils.closeSilently(os);
            if (copied) {
                editor.commit();
            } else {
                editor.abort();
            }
        }
        return copied;
    }

    @Override
    public boolean save(String imageUri, Bitmap bitmap) throws IOException {
        DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
        if (editor == null) {
            return false;
        }

        OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
        boolean savedSuccessfully = false;
        try {
            savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
        } finally {
            IoUtils.closeSilently(os);
        }
        if (savedSuccessfully) {
            editor.commit();
        } else {
            editor.abort();
        }
        return savedSuccessfully;
    }

这里面涉及了一个方法getkey()是将图片的地址拿去加密防止直接图片的url地址暴露出来,显得可以更加安全点。

private String getKey(String imageUri) {
        return fileNameGenerator.generate(imageUri);
    }

fileNameGenerator这个对象是从LruDisCache的构造函数中读取得到。我们需要看下构造函数中的initCache方法:

private void initCache(File cacheDir, File reserveCacheDir, long cacheMaxSize, int cacheMaxFileCount)
            throws IOException {
        try {
            cache = DiskLruCache.open(cacheDir, 1, 1, cacheMaxSize, cacheMaxFileCount);
        } catch (IOException e) {
            L.e(e);
            if (reserveCacheDir != null) {
                initCache(reserveCacheDir, null, cacheMaxSize, cacheMaxFileCount);
            }
            if (cache == null) {
                throw e; //new RuntimeException("Can't initialize disk cache", e);
            }
        }
    }

在这个方法中完成了初始化DiskLruCache操作,这样我们对Android-Universal-Image-Loader的缓存结构都是分析Lru*算法源码。

下载参数配置

在参数配置中UniversalImageloader提供了ImageLoaderConfiguration和DisplayImageOptions来实现图片下载和下载过程中的显示所需要的参数,这两个文件都是采用建造者设计模式通过Builder来赋值参数值。

简单举个配置的例子。
ImageLoaderConfiguration配置:

ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context).threadPoolSize(3)
                .tasksProcessingOrder(QueueProcessingType.LIFO).threadPriority(Thread.NORM_PRIORITY)
                .memoryCache(new LruMemoryCache(getHeapsize()))
                .memoryCacheSize((256 * 1024 * 1024)).build();

DisplayImageOptions配置:

options = new DisplayImageOptions.Builder()
                .showImageOnLoading(R.drawable.ic_stub)
                .showImageForEmptyUri(R.drawable.ic_empty)
                .showImageOnFail(R.drawable.ic_error)
                .cacheInMemory(true)
                .cacheOnDisk(true)
                .considerExifParams(true)
                .displayer(new RoundedBitmapDisplayer(20))
                .build();

因为这两个类中的参数我们可以非常直观的看着名字就能知道它的用途所以我们重点看下默认情况的初始化:

private void initEmptyFieldsWithDefaultValues() {
            if (taskExecutor == null) {
                taskExecutor = DefaultConfigurationFactory
                        .createExecutor(threadPoolSize, threadPriority, tasksProcessingType);
            } else {
                customExecutor = true;
            }
            if (taskExecutorForCachedImages == null) {
                taskExecutorForCachedImages = DefaultConfigurationFactory
                        .createExecutor(threadPoolSize, threadPriority, tasksProcessingType);
            } else {
                customExecutorForCachedImages = true;
            }
            if (diskCache == null) {
                if (diskCacheFileNameGenerator == null) {
                    diskCacheFileNameGenerator = DefaultConfigurationFactory.createFileNameGenerator();
                }
                diskCache = DefaultConfigurationFactory
                        .createDiskCache(context, diskCacheFileNameGenerator, diskCacheSize, diskCacheFileCount);
            }
            if (memoryCache == null) {
                memoryCache = DefaultConfigurationFactory.createMemoryCache(context, memoryCacheSize);
            }
            if (denyCacheImageMultipleSizesInMemory) {
                memoryCache = new FuzzyKeyMemoryCache(memoryCache, MemoryCacheUtils.createFuzzyKeyComparator());
            }
            if (downloader == null) {
                downloader = DefaultConfigurationFactory.createImageDownloader(context);
            }
            if (decoder == null) {
                decoder = DefaultConfigurationFactory.createImageDecoder(writeLogs);
            }
            if (defaultDisplayImageOptions == null) {
                defaultDisplayImageOptions = DisplayImageOptions.createSimple();
            }
        }
    }

其他的参数初始化还是比较简单只需要new即可,主要看下createExecutor这个方法:

/** Creates default implementation of task executor */
    public static Executor createExecutor(int threadPoolSize, int threadPriority,
            QueueProcessingType tasksProcessingType) {
        boolean lifo = tasksProcessingType == QueueProcessingType.LIFO;
        BlockingQueue<Runnable> taskQueue =
                lifo ? new LIFOLinkedBlockingDeque<Runnable>() : new LinkedBlockingQueue<Runnable>();
        return new ThreadPoolExecutor(threadPoolSize, threadPoolSize, 0L, TimeUnit.MILLISECONDS, taskQueue,
                createThreadFactory(threadPriority, "uil-pool-"));
    }

通过ThreadPoolExecutor来实现线程池,其中createThreadFactory这个方法主要是一个线程工厂类主要是初始化线程用的,规范后更方便调试:

private static class DefaultThreadFactory implements ThreadFactory {

        private static final AtomicInteger poolNumber = new AtomicInteger(1);

        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;
        private final int threadPriority;

        DefaultThreadFactory(int threadPriority, String threadNamePrefix) {
            this.threadPriority = threadPriority;
            group = Thread.currentThread().getThreadGroup();
            namePrefix = threadNamePrefix + poolNumber.getAndIncrement() + "-thread-";
        }

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
            if (t.isDaemon()) t.setDaemon(false);
            t.setPriority(threadPriority);
            return t;
        }
    }

图片下载功能

定义好下载图片之前的所有参数配置后,我们是时候开始开启线程进行图片下载操作了。调用ImageLoader.getInstance()来获取到ImageLoader对象,ImageLoader是一个单列类。

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

然后将ImageLoaderConfiguration配置参数写入到ImageLoader中去以供下载过程中使用。

public synchronized void init(ImageLoaderConfiguration configuration) {
        if (configuration == null) {
            throw new IllegalArgumentException(ERROR_INIT_CONFIG_WITH_NULL);
        }
        if (this.configuration == null) {
            L.d(LOG_INIT_CONFIG);
            engine = new ImageLoaderEngine(configuration);
            this.configuration = configuration;
        } else {
            L.w(WARNING_RE_INIT_CONFIG);
        }
    }

到了这步之后才把初始化工作全部完成了,这时候就需要把图片的url传进来去下载图片即可了,displayImage这个类提供多种的方法进行图片下载。在进行下载我们还需要了解ImageAware这个类把ImageView封装在里面,并暴露一些ImageView需要的方法给开发者调用:

public interface ImageAware {
    /**
     * Returns width of image aware view.
     */
    int getWidth();

    /**
     * Returns height of image aware view.
     */
    int getHeight();

    ViewScaleType getScaleType();

    View getWrappedView();

    /**
     * Returns a flag whether image aware view is collected by GC or whatsoever. If so then ImageLoader stop processing
     */
    boolean isCollected();

    /**
     * Returns ID of image aware view.
     */
    int getId();

    /**
     * Sets image drawable into this image aware view
     */
    boolean setImageDrawable(Drawable drawable);

    /**
     * Sets image bitmap into this image aware view
     */
    boolean setImageBitmap(Bitmap bitmap);
}

首先我们需要判断是否图片地址为空,并进行非空处理:

if (TextUtils.isEmpty(uri)) {
            engine.cancelDisplayTaskFor(imageAware);
            listener.onLoadingStarted(uri, imageAware.getWrappedView());
            if (options.shouldShowImageForEmptyUri()) {
                imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
            } else {
                imageAware.setImageDrawable(null);
            }
            listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
            return;
        }

如果空的话现在我们之前配置好空图片res/drawable或者null值给ImageView。
读取内存中的Bitmap对象值,如果内存中有值就直接将Bitmap写入到ImageView中去。

Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
        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));
                if (options.isSyncLoading()) {
                    displayTask.run();
                } else {
                    engine.submit(displayTask);
                }
            } else {
                options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
                listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
            }
        } 

当然内存中如果没有的话 开启线程进行图片读取操作:

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);
            }

其中的engine主要进行图片下载线程分发, 把对应的LoadAndDisplayImageTask线程按是从网络或者磁盘分发给不同的Executor去执行。

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);
                }
            }
        });
    }

其中如果图片需要从网络下载也是将图片先存入到磁盘然都从磁盘中读取已经下载好的图片,通过调用DiskLruCache的save直接将从网络获取到的InputStream直接写入到磁盘中去。

private boolean downloadImage() throws IOException {
        InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader());
        if (is == null) {
            L.e(ERROR_NO_IMAGE_STREAM, memoryCacheKey);
            return false;
        } else {
            try {
                return configuration.diskCache.save(uri, is, this);
            } finally {
                IoUtils.closeSilently(is);
            }
        }
    }

所以我们现在只需要统一看下磁盘读取即可了。
先从磁盘中读取到Bitmap:

File imageFile = configuration.diskCache.get(uri);
            if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
                bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
            }

从上可以看出来主要的代码还是decodeImage这个方法中的代码:

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这个方法中的实现,所以我们再次进入这个方法:

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

        InputStream imageStream = getImageStream(decodingInfo);
        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;
    }

可以看到这突然一堆不认识的方法
getImageStream这个方法最终是调取decodingInfo.getDownloader().getStream这个方法中去读取InputStream,然后:

protected ImageFileInfo defineImageSizeAndRotation(InputStream imageStream, ImageDecodingInfo decodingInfo)
            throws IOException {
        Options options = new Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(imageStream, null, options);

        ExifInfo exif;
        String imageUri = decodingInfo.getImageUri();
        if (decodingInfo.shouldConsiderExifParams() && canDefineExifParams(imageUri, options.outMimeType)) {
            exif = defineExifOrientation(imageUri);
        } else {
            exif = new ExifInfo();
        }
        return new ImageFileInfo(new ImageSize(options.outWidth, options.outHeight, exif.rotation), exif);
    }

将options.inJustDecodeBounds = true这样bitmap就不占据内存,然后我们返回一个ImageFileInfo对象然后通过prepareDecodingOptions进行对图片压缩操作主要是获取inSampleSize从而压缩图片,因为这些的套路比较统一你是否跟我一样发现了ExifInfo,所以重点看这些。

if (decodingInfo.shouldConsiderExifParams() && canDefineExifParams(imageUri, options.outMimeType)) {
            exif = defineExifOrientation(imageUri);
        } else {
            exif = new ExifInfo();
        }

在ExifInfo中我们可以看到两个参数:

public final int rotation;
public final boolean flipHorizontal;

代表的是图片被旋转的角度以及旋转方向,如果我们在DisplayImageOptions中有进行配置的话那么我们可以这么来读取旋转角度:

protected ExifInfo defineExifOrientation(String imageUri) {
        int rotation = 0;
        boolean flip = false;
        try {
            ExifInterface exif = new ExifInterface(Scheme.FILE.crop(imageUri));
            int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
            switch (exifOrientation) {
                case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
                    flip = true;
                case ExifInterface.ORIENTATION_NORMAL:
                    rotation = 0;
                    break;
                case ExifInterface.ORIENTATION_TRANSVERSE:
                    flip = true;
                case ExifInterface.ORIENTATION_ROTATE_90:
                    rotation = 90;
                    break;
                case ExifInterface.ORIENTATION_FLIP_VERTICAL:
                    flip = true;
                case ExifInterface.ORIENTATION_ROTATE_180:
                    rotation = 180;
                    break;
                case ExifInterface.ORIENTATION_TRANSPOSE:
                    flip = true;
                case ExifInterface.ORIENTATION_ROTATE_270:
                    rotation = 270;
                    break;
            }
        } catch (IOException e) {
            L.w("Can't read EXIF tags from file [%s]", imageUri);
        }
        return new ExifInfo(rotation, flip);
    }

最后我们在considerExactScaleAndOrientatiton这个方法中就会根据ExifInfo中存的信息进行把图片旋转到正常的方向和角度。
这样我们图片就下载完成了,然后把下载得到的Bitmap存入到内存中:

configuration.memoryCache.put(memoryCacheKey, bmp);

最后

Android-Universal-Image-Loader中提供了一个类PauseOnScrollListener当快速滑动的时候暂停线程从而提高性能。
暂停:

private boolean waitIfPaused() {
        AtomicBoolean pause = engine.getPause();
        if (pause.get()) {
            synchronized (engine.getPauseLock()) {
                if (pause.get()) {
                    L.d(LOG_WAITING_FOR_RESUME, memoryCacheKey);
                    try {
                        engine.getPauseLock().wait();
                    } catch (InterruptedException e) {
                        L.e(LOG_TASK_INTERRUPTED, memoryCacheKey);
                        return true;
                    }
                    L.d(LOG_RESUME_AFTER_PAUSE, memoryCacheKey);
                }
            }
        }
        return isTaskNotActual();
    }

恢复:

void resume() {
        paused.set(false);
        synchronized (pauseLock) {
            pauseLock.notifyAll();
        }
    }

当然我们还要处理一下将下载好的图片显示到ImageView上:

static void runTask(Runnable r, boolean sync, Handler handler, ImageLoaderEngine engine) {
        if (sync) {
            r.run();
        } else if (handler == null) {
            engine.fireCallback(r);
        } else {
            handler.post(r);
        }
    }

就是通过Handler.post方法将bitmap从异步线程移到UI线程并显示在ImageView上。
这样Android-Universal-Image-loader的一个图片下载流程就完成了,阅读源码能读到很到未知的知识。好像画下调用流程图,可惜自己工具画的好丑,有时间再补上。写的不好不要见怪!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值