美文网首页
Universal-Image-Loader源码解析之图片获取流

Universal-Image-Loader源码解析之图片获取流

作者: LeonXtp | 来源:发表于2017-09-13 07:06 被阅读20次

    此处抛开线程调度、图片处理等,仅拿出图片加载流程概要。

    图片获取流程概要

    ImageLoader

    单例模式的请求入口。
    每一个请求都将进入到ImageLoaderdisplayImage()方法中:
    主要工作:

    • 负责请求的一些初始化
    • 首先尝试从内存中获取,如果成功,则直接开启后续的图片处理、展示等任务。如果失败,则将请求信息包装到ImageLoadingInfo中,包括图片URL、ImageView、图片的大小、内存缓存key、监听器等等。然后再创建一个图片加载的任务LoadAndDisplayImageTask,此任务将之后被执行(视同步还是异步请求而定),任务完成后,将会把图片缓存到内存中。
        public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
                ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
            checkConfiguration();
            if (imageAware == null) {
                throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
            }
            if (listener == null) {
                listener = defaultListener;
            }
            if (options == null) {
                options = configuration.defaultDisplayImageOptions;
            }
    
            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;
            }
    
            if (targetSize == null) {
                targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
            }
            String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
            engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
    
            listener.onLoadingStarted(uri, imageAware.getWrappedView());
    
            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);
                }
            } 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);
                }
            }
        }
    

    LoadAndDisplayImageTask

    一个实现了Runnable接口的图片加载任务,负责图片的获取、缓存到不同位置(磁盘、内存等)、解码、处理、进度监听等。
    当此任务被执行时,将首先执行其 run()方法:

        @Override
        public void run() {
            if (waitIfPaused()) return;
            if (delayIfNeed()) return;
    
            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();
            Bitmap bmp;
            try {
                checkTaskNotActual();
    
                bmp = configuration.memoryCache.get(memoryCacheKey);
                if (bmp == null || bmp.isRecycled()) {
                    bmp = tryLoadBitmap();
                    if (bmp == null) return; // listener callback already was fired
    
                    checkTaskNotActual();
                    checkTaskInterrupted();
    
                    if (options.shouldPreProcess()) {
                        L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
                        bmp = options.getPreProcessor().process(bmp);
                        if (bmp == null) {
                            L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey);
                        }
                    }
    
                    if (bmp != null && options.isCacheInMemory()) {
                        L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey);
                        configuration.memoryCache.put(memoryCacheKey, bmp);
                    }
                } else {
                    loadedFrom = LoadedFrom.MEMORY_CACHE;
                    L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey);
                }
    
                if (bmp != null && options.shouldPostProcess()) {
                    L.d(LOG_POSTPROCESS_IMAGE, memoryCacheKey);
                    bmp = options.getPostProcessor().process(bmp);
                    if (bmp == null) {
                        L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey);
                    }
                }
                checkTaskNotActual();
                checkTaskInterrupted();
            } catch (TaskCancelledException e) {
                fireCancelEvent();
                return;
            } finally {
                loadFromUriLock.unlock();
            }
    
            DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
            runTask(displayBitmapTask, syncLoading, handler, engine);
        }
    
    • 首先再次判断此图片是否已经存在于内存中(因有可能其他任务也负责加载此图片)
    • 如果没有或者已经被GC回收,则tryLoadBitmap()尝试从磁盘或者网络中获取,如果加载失败,直接return
    • 加载成功,则对图片做后续处理。

    下面是从磁盘或者网络加载图片的过程:

    • tryLoadBitmap()会线判断图片是否已经缓存在磁盘上,如果有,则调用decode()方法进行解码,得到Bitmap实例。
    • 如果解码失败,就会调用tryCacheImageOnDisk ()去网络上获取图片了,加载完成后会先缓存图片到磁盘上。
    • 完成后将再次调用decode()方法进行解码,得到Bitmap实例。
    • 如果Bitmap还是null,这时将通知调用者,图片加载失败了。
        private Bitmap tryLoadBitmap() throws TaskCancelledException {
            Bitmap bitmap = null;
            try {
                File imageFile = configuration.diskCache.get(uri);
                if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
                    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);
                    }
                }
            } catch (IllegalStateException e) {
                fireFailEvent(FailType.NETWORK_DENIED, null);
            } catch (TaskCancelledException e) {
                throw e;
            } catch (IOException e) {
                L.e(e);
                fireFailEvent(FailType.IO_ERROR, e);
            } catch (OutOfMemoryError e) {
                L.e(e);
                fireFailEvent(FailType.OUT_OF_MEMORY, e);
            } catch (Throwable e) {
                L.e(e);
                fireFailEvent(FailType.UNKNOWN, e);
            }
            return bitmap;
        }
    

    下面是tryCacheImageOnDisk ():

        private boolean tryCacheImageOnDisk() throws TaskCancelledException {
            L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey);
    
            boolean loaded;
            try {
                loaded = downloadImage();
                if (loaded) {
                    int width = configuration.maxImageWidthForDiskCache;
                    int height = configuration.maxImageHeightForDiskCache;
                    if (width > 0 || height > 0) {
                        L.d(LOG_RESIZE_CACHED_IMAGE_FILE, memoryCacheKey);
                        resizeAndSaveImage(width, height); // TODO : process boolean result
                    }
                }
            } catch (IOException e) {
                L.e(e);
                loaded = false;
            }
            return loaded;
        }
    

    在这里,downloadImage()方法才是真正的开始进入图片下载过程,继续跟进:

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

    这里获取getDownloader ()到了下载器ImageDownloader接口的具体实现,这个UIL它提供了默认的实现BaseImageDownloader,我们也可以自己实现一个,在UIL的初始配置时传入。看看默认下载器的getStream()

    BaseImageDownloader

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

    在这里就可以看到,这里根据图片的URL去到了不同的来源去获取图片
    跟进Scheme看看,它是一个枚举类型,

    public enum Scheme {
            HTTP("http"), HTTPS("https"), FILE("file"), CONTENT("content"), ASSETS("assets"), DRAWABLE("drawable"), UNKNOWN("");
            ...
            public static Scheme ofUri(String uri) {
                if (uri != null) {
                    for (Scheme s : values()) {
                        if (s.belongsTo(uri)) {
                            return s;
                        }
                    }
                }
                return UNKNOWN;
            }
    
            private boolean belongsTo(String uri) {
                return uri.toLowerCase(Locale.US).startsWith(uriPrefix);
            }
    }
    

    根据uri的开头,分别从网络、SD卡、app内部的文件系统去加载了图片。
    先看看UIL是如何从网络上加载图片的:

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

    看了才知道,它的图片加载核心代码也很简单,就是使用了HttpURLConnection去获取了图片字节流。
    而最后返回的ContentLengthInputStream其实就是一个使用装饰者模式包装了的InputStream,它仅仅是重写了InputStreamavailable ()方法

        @Override
        public int available() {
            return length;
        }
    

    这个方法干嘛的呢?其实就是提供本输入流的字节量大小。这个最后用在了图片下载的进度监听上。

    跑远了,获取到图片的输入流之后,继续回到downloadImage()方法,可以看到它的返回是:

    return configuration.diskCache.save(uri, is, this);
    

    这就是在从网络加载图片后,将图片缓存到磁盘了。这里传入了输入流和this,这里的diskCache是UIL初始配置的时候初始化的,它也有个默认的磁盘缓存:

    BaseDiskCache

        @Override
        public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
            File imageFile = getFile(imageUri);
            File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
            boolean loaded = false;
            try {
                OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
                try {
                    loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize);
                } finally {
                    IoUtils.closeSilently(os);
                }
            } finally {
                if (loaded && !tmpFile.renameTo(imageFile)) {
                    loaded = false;
                }
                if (!loaded) {
                    tmpFile.delete();
                }
            }
            return loaded;
        }
    

    可以看到这里将输入流转为输出流,保存文件到了本地磁盘上。
    这里传入的参数里, 有IoUtils.CopyListener,这个就是图片下载进度的监听器了,看到这里,就该想到,真正实现进度监听功能的代码,就要到了。
    跟进IoUtils.copyStream,看到:

        public static boolean copyStream(InputStream is, OutputStream os, CopyListener listener, int bufferSize)
                throws IOException {
            int current = 0;
            int total = is.available();
            if (total <= 0) {
                total = DEFAULT_IMAGE_TOTAL_SIZE;
            }
    
            final byte[] bytes = new byte[bufferSize];
            int count;
            if (shouldStopLoading(listener, current, total)) return false;
            while ((count = is.read(bytes, 0, bufferSize)) != -1) {
                os.write(bytes, 0, count);
                current += count;
                if (shouldStopLoading(listener, current, total)) return false;
            }
            os.flush();
            return true;
        }
    

    这里is.available()就是获取到了之前getStreamFromNetwork()所创建的InputStream的装饰者中的文件字节流大小。我们的目标是找到进度回调的最终调用链源点,于是紧盯着listener,于是跟进了方法shouldStopLoading:

        private static boolean shouldStopLoading(CopyListener listener, int current, int total) {
            if (listener != null) {
                boolean shouldContinue = listener.onBytesCopied(current, total);
                if (!shouldContinue) {
                    if (100 * current / total < CONTINUE_LOADING_PERCENTAGE) {
                        return true; // if loaded more than 75% then continue loading anyway
                    }
                }
            }
            return false;
        }
    

    就是这里了!进度监听就是这么实现的。
    下面的while()循环里就是将图片数据流写入到输出流了。图片最终保存到了磁盘上。

    相关文章

      网友评论

          本文标题:Universal-Image-Loader源码解析之图片获取流

          本文链接:https://www.haomeiwen.com/subject/wjzhsxtx.html