Android圖片加載框架--Universal-Image-Loader源碼解析

前言

提到圖片加載框架,最經典的應該就是Universal-Image-Loader了。此篇文章不會涉及如何使用,而是通過查看源碼的方式對框架的原理進行理解。

源碼解析

當我們要顯示一張圖片時,實際上只需要一行代碼就可以做到:

                ImageLoader.getInstance().displayImage(address,imgv);

getInstance()很顯然是一個單例模式,那么我們跟進去displayImage方法:

    public void displayImage(String uri, ImageView imageView) {
        displayImage(uri, new ImageViewAware(imageView), null, null, null);
    }

在這里創建了一個ImageViewAware對象。

    public ImageViewAware(ImageView imageView) {
        super(imageView);
    }
       //他的父類是ViewAware,貼上父類的構造函數:
    public ViewAware(View view) {
        this(view, true);
    }

    public ViewAware(View view, boolean checkActualViewSize) {
        if (view == null) throw new IllegalArgumentException("view must not be null");

        this.viewRef = new WeakReference<View>(view);
        this.checkActualViewSize = checkActualViewSize;
    }

這里涉及到了一個概念:強引用、弱引用、虛引用的區別。

  • 強引用。我們平時使用的引用默認都是強引用。如果一個對象可以被某個強引用找到(即存在一個強引用指向該對象),那么這個對象一定不會被Java虛擬機所回收。如果當內存不夠時,強引用指向的對象又無法被回收,那么,就會拋出OutOfMemory異常。
  • 弱引用(SoftReference)。如果一個對象不存在指向它的強引用,并且存在指向它的弱引用。那么,當內存不夠時,Java虛擬機會將該對象回收。而如果內存充足,那么就不會回收該對象。
  • 虛引用(WeakReference)。如果一個對象不存在指向它的強引用和弱引用,并且存在指向它的虛引用。當虛擬機進行垃圾回收時,無論此時內存是否充足,這個對象都會被回收。
    看到這里,我想大家就會明白了,使用虛引用能夠有效的避免內存泄露。

我們繼續查看displayImage方法,代碼比較長,把源碼的解釋寫到了注釋里:

    public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
            ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
        displayImage(uri, imageAware, options, null, listener, progressListener);
    }

    public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
            ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
       
        checkConfiguration();    //用來檢查是否對ImageLoader進行了配置,只需要在Application中初始化一次
        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) {
           //在獲取圖片之前,首先查看ImageView的大小
            targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
        }
        //使用url和大小作為主鍵
        String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
        //使用HashMap將ImageView的id和主鍵對應存放起來,等效于為ImageView設置tag,從而解決ListView圖片亂序等問題。
        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);
            }
        }
    }

重要步驟的方法的解釋已經寫到了注釋里,就不再贅述了。首先來看一下defineTargetSizeForView方法。

    public static ImageSize defineTargetSizeForView(ImageAware imageAware, ImageSize maxImageSize) {
        int width = imageAware.getWidth();
        if (width <= 0) width = maxImageSize.getWidth();

        int height = imageAware.getHeight();
        if (height <= 0) height = maxImageSize.getHeight();

        return new ImageSize(width, height);
    }

    public int getWidth() {    ///ImageViewAware類
        int width = super.getWidth();
        if (width <= 0) {
            ImageView imageView = (ImageView) viewRef.get();
            if (imageView != null) {
                width = getImageViewFieldValue(imageView, "mMaxWidth"); // Check maxWidth parameter
            }
        }
        return width;
    }

    public int getWidth() {      //ViewAware類
        View view = viewRef.get();
        if (view != null) {
            final ViewGroup.LayoutParams params = view.getLayoutParams();
            int width = 0;
            if (checkActualViewSize && params != null && params.width != ViewGroup.LayoutParams.WRAP_CONTENT) {
                width = view.getWidth(); // Get actual image width
            }
            if (width <= 0 && params != null) width = params.width; // Get layout width parameter
            return width;
        }
        return 0;
    }

寬度和高度的邏輯是一樣的,在這里以寬度為例。在ViewAware類的getWidth()方法中,可以看到,如果ImageView的寬度不是wrap_content,那么就返回ImageView的寬度;如果ImageView的寬度是wrap_content,那么獲取寬度的邏輯應該轉移到ImageViewAware類的getWidth方法,如果ImageView設置了maxWidth這個屬性,那么就返回maxWidth,否則獲取寬度的邏輯應該轉移到defineTargetSizeForView類中,maxImageSize.getWidth()方法返回的數值,實際上是我們在初始化ImageLoader設置的參數。

接下來我們來看一下最主要的流程的代碼,也就是displayImage最底部的代碼。很容易看出來,LoadAndDisplayImageTask應該是一個Thread,跟進去看一下run方法。

    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();
        }
        //將圖像顯示在ImageView中
        DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
        runTask(displayBitmapTask, syncLoading, handler, engine);
    }

首先我們來看一下tryLoadBitmap方法。

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

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

    private boolean resizeAndSaveImage(int maxWidth, int maxHeight) throws IOException {
        // Decode image file, compress and re-save it
        boolean saved = false;
        File targetFile = configuration.diskCache.get(uri);
        if (targetFile != null && targetFile.exists()) {
            ImageSize targetImageSize = new ImageSize(maxWidth, maxHeight);
            DisplayImageOptions specialOptions = new DisplayImageOptions.Builder().cloneFrom(options)
                    .imageScaleType(ImageScaleType.IN_SAMPLE_INT).build();
            ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey,
                    Scheme.FILE.wrap(targetFile.getAbsolutePath()), uri, targetImageSize, ViewScaleType.FIT_INSIDE,
                    getDownloader(), specialOptions);
            Bitmap bmp = decoder.decode(decodingInfo);
            if (bmp != null && configuration.processorForDiskCache != null) {
                L.d(LOG_PROCESS_IMAGE_BEFORE_CACHE_ON_DISK, memoryCacheKey);
                bmp = configuration.processorForDiskCache.process(bmp);
                if (bmp == null) {
                    L.e(ERROR_PROCESSOR_FOR_DISK_CACHE_NULL, memoryCacheKey);
                }
            }
            if (bmp != null) {
                //將圖像保存到硬盤中
                saved = configuration.diskCache.save(uri, bmp);
                bmp.recycle();
            }
        }
        return saved;
    }

通過上面的分析,我們知道了如何獲取到Bitmap。然后來看一下將Bitmap顯示出來的部分。

final class DisplayBitmapTask implements Runnable {
     .......
    @Override
    public void run() {
        if (imageAware.isCollected()) {
            L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
            listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
        } else if (isViewWasReused()) {
            L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
            listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
        } else {
            L.d(LOG_DISPLAY_IMAGE_IN_IMAGEAWARE, loadedFrom, memoryCacheKey);
           //將圖像顯示在ImageView中
            displayer.display(bitmap, imageAware, loadedFrom);
             //從HashMap中,將ImageView的id和url組成的鍵值對刪去
            engine.cancelDisplayTaskFor(imageAware);
            listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
        }
    }

    /** Checks whether memory cache key (image URI) for current ImageAware is actual */
    private boolean isViewWasReused() {
        String currentCacheKey = engine.getLoadingUriForView(imageAware);
        return !memoryCacheKey.equals(currentCacheKey);
    }
}

到這里,整個流程就分析完了~希望讓你有所收獲~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容