Universal-Image-Loader源碼解析之圖片獲取流程

此處拋開(kāi)線(xiàn)程調(diào)度、圖片處理等,僅拿出圖片加載流程概要锦援。

圖片獲取流程概要

ImageLoader

單例模式的請(qǐng)求入口。
每一個(gè)請(qǐng)求都將進(jìn)入到ImageLoaderdisplayImage()方法中:
主要工作:

  • 負(fù)責(zé)請(qǐng)求的一些初始化
  • 首先嘗試從內(nèi)存中獲取剥悟,如果成功灵寺,則直接開(kāi)啟后續(xù)的圖片處理、展示等任務(wù)区岗。如果失敗略板,則將請(qǐng)求信息包裝到ImageLoadingInfo中,包括圖片URL慈缔、ImageView叮称、圖片的大小、內(nèi)存緩存key胀糜、監(jiān)聽(tīng)器等等颅拦。然后再創(chuàng)建一個(gè)圖片加載的任務(wù)LoadAndDisplayImageTask蒂誉,此任務(wù)將之后被執(zhí)行(視同步還是異步請(qǐng)求而定)教藻,任務(wù)完成后,將會(huì)把圖片緩存到內(nèi)存中右锨。
    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

一個(gè)實(shí)現(xiàn)了Runnable接口的圖片加載任務(wù)括堤,負(fù)責(zé)圖片的獲取、緩存到不同位置(磁盤(pán)绍移、內(nèi)存等)悄窃、解碼、處理蹂窖、進(jìn)度監(jiān)聽(tīng)等轧抗。
當(dāng)此任務(wù)被執(zhí)行時(shí),將首先執(zhí)行其 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);
    }
  • 首先再次判斷此圖片是否已經(jīng)存在于內(nèi)存中(因有可能其他任務(wù)也負(fù)責(zé)加載此圖片)
  • 如果沒(méi)有或者已經(jīng)被GC回收瞬测,則tryLoadBitmap()嘗試從磁盤(pán)或者網(wǎng)絡(luò)中獲取横媚,如果加載失敗,直接return
  • 加載成功月趟,則對(duì)圖片做后續(xù)處理灯蝴。

下面是從磁盤(pán)或者網(wǎng)絡(luò)加載圖片的過(guò)程:

  • tryLoadBitmap()會(huì)線(xiàn)判斷圖片是否已經(jīng)緩存在磁盤(pán)上,如果有孝宗,則調(diào)用decode()方法進(jìn)行解碼穷躁,得到Bitmap實(shí)例。
  • 如果解碼失敗因妇,就會(huì)調(diào)用tryCacheImageOnDisk ()去網(wǎng)絡(luò)上獲取圖片了问潭,加載完成后會(huì)先緩存圖片到磁盤(pán)上猿诸。
  • 完成后將再次調(diào)用decode()方法進(jìn)行解碼,得到Bitmap實(shí)例睦授。
  • 如果Bitmap還是null两芳,這時(shí)將通知調(diào)用者,圖片加載失敗了去枷。
    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()方法才是真正的開(kāi)始進(jìn)入圖片下載過(guò)程,繼續(xù)跟進(jìn):

    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接口的具體實(shí)現(xiàn)删顶,這個(gè)UIL它提供了默認(rèn)的實(shí)現(xiàn)BaseImageDownloader竖螃,我們也可以自己實(shí)現(xiàn)一個(gè),在UIL的初始配置時(shí)傳入逗余。看看默認(rèn)下載器的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);
        }
    }

在這里就可以看到,這里根據(jù)圖片的URL去到了不同的來(lái)源去獲取圖片
跟進(jìn)Scheme看看,它是一個(gè)枚舉類(lèi)型酬核,

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

根據(jù)uri的開(kāi)頭嫡意,分別從網(wǎng)絡(luò)蔬螟、SD卡汽畴、app內(nèi)部的文件系統(tǒng)去加載了圖片菠齿。
先看看UIL是如何從網(wǎng)絡(luò)上加載圖片的:

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

看了才知道,它的圖片加載核心代碼也很簡(jiǎn)單,就是使用了HttpURLConnection去獲取了圖片字節(jié)流是尔。
而最后返回的ContentLengthInputStream其實(shí)就是一個(gè)使用裝飾者模式包裝了的InputStream殉了,它僅僅是重寫(xiě)了InputStreamavailable ()方法

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

這個(gè)方法干嘛的呢隔箍?其實(shí)就是提供本輸入流的字節(jié)量大小奶稠。這個(gè)最后用在了圖片下載的進(jìn)度監(jiān)聽(tīng)上竹握。

跑遠(yuǎn)了,獲取到圖片的輸入流之后战得,繼續(xù)回到downloadImage()方法常侦,可以看到它的返回是:

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

這就是在從網(wǎng)絡(luò)加載圖片后坡倔,將圖片緩存到磁盤(pán)了瘩缆。這里傳入了輸入流和this佃蚜,這里的diskCache是UIL初始配置的時(shí)候初始化的庸娱,它也有個(gè)默認(rèn)的磁盤(pán)緩存:

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

可以看到這里將輸入流轉(zhuǎn)為輸出流,保存文件到了本地磁盤(pán)上谐算。
這里傳入的參數(shù)里涌韩, 有IoUtils.CopyListener,這個(gè)就是圖片下載進(jìn)度的監(jiān)聽(tīng)器了氯夷,看到這里臣樱,就該想到,真正實(shí)現(xiàn)進(jìn)度監(jiān)聽(tīng)功能的代碼腮考,就要到了雇毫。
跟進(jìn)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()所創(chuàng)建的InputStream的裝飾者中的文件字節(jié)流大小踩蔚。我們的目標(biāo)是找到進(jìn)度回調(diào)的最終調(diào)用鏈源點(diǎn)棚放,于是緊盯著listener,于是跟進(jìn)了方法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;
    }

就是這里了馅闽!進(jìn)度監(jiān)聽(tīng)就是這么實(shí)現(xiàn)的飘蚯。
下面的while()循環(huán)里就是將圖片數(shù)據(jù)流寫(xiě)入到輸出流了。圖片最終保存到了磁盤(pán)上福也。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末局骤,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子暴凑,更是在濱河造成了極大的恐慌峦甩,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件现喳,死亡現(xiàn)場(chǎng)離奇詭異凯傲,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)嗦篱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)冰单,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人灸促,你說(shuō)我怎么就攤上這事诫欠∈ㄍ龋” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵呕诉,是天一觀(guān)的道長(zhǎng)缘厢。 經(jīng)常有香客問(wèn)我,道長(zhǎng)甩挫,這世上最難降的妖魔是什么贴硫? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮伊者,結(jié)果婚禮上英遭,老公的妹妹穿的比我還像新娘。我一直安慰自己亦渗,他們只是感情好挖诸,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著法精,像睡著了一般多律。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搂蜓,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天狼荞,我揣著相機(jī)與錄音,去河邊找鬼帮碰。 笑死相味,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的殉挽。 我是一名探鬼主播丰涉,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼斯碌!你這毒婦竟也來(lái)了一死?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤输拇,失蹤者是張志新(化名)和其女友劉穎摘符,沒(méi)想到半個(gè)月后贤斜,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體策吠,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年瘩绒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了猴抹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡锁荔,死狀恐怖蟀给,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤跋理,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布择克,位于F島的核電站,受9級(jí)特大地震影響前普,放射性物質(zhì)發(fā)生泄漏肚邢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一拭卿、第九天 我趴在偏房一處隱蔽的房頂上張望骡湖。 院中可真熱鬧,春花似錦峻厚、人聲如沸响蕴。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)浦夷。三九已至,卻和暖如春辜王,著一層夾襖步出監(jiān)牢的瞬間军拟,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工誓禁, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留懈息,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓摹恰,卻偏偏與公主長(zhǎng)得像辫继,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子俗慈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容