面试之——Android-Universal-Image-Loader源码(一)

为了开年面试的准备,开始看一些之前用过的开源库,今天就主要围绕Android-Universal-Image-Loadergithub的源码带着大家浏览一遍。为了更好地理解Android-Universal-Image-Loader代码部分,这里给大家画张类的结构图:

Android-Universal-Image-Loader类图结构.png

大家看到上面图,其实也不知道咋回事。下面就顺着代码去看看是怎么个流程:

ImageLoader.getInstance().displayImage(IMAGE_URLS[position], holder.image, options, animateFirstListener);

上面的代码是大家经常用的使用方法,那咱们去瞧瞧displayImage方法是如何实现的,几个重载的displayImage最终都会调用该displayImage方法

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
        ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
    //省略空判断的代码 

    String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
    engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
    listener.onLoadingStarted(uri, imageAware.getWrappedView());
    Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
        //内存中bitmap存在
    if (bmp != null && !bmp.isRecycled()) {
        L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
        //如果是需要进行process操作,默认不会process操作的
        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));
            //默认不是cync操作
            if (options.isSyncLoading()) {
                displayTask.run();
            } else {
                engine.submit(displayTask);
            }
        } else {
            //直接从内存中获取bitmap进行显示了
            options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
            listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
        }
    } else {
      //内存中不存在bitmap的操作
        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));
        //内存中没有bitmap走该task
        LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
                defineHandler(options));
        if (options.isSyncLoading()) {
             //同步的话,就是立即执行
            displayTask.run();
        } else {
            //默认走这里的,这里不会同步执行task,而是放在线程池中等待执行的
            engine.submit(displayTask);
        }
    }
}

那咱们知道了内存中没有bitmap的时候,会创建了一个LoadAndDisplayImageTask。那咱们去瞧瞧:

image.png

从图上可以看出,它是一个子线程的操作。那咱们就直接去看run方法吧:

@Override
public void run() {
    //省略部分代码
    Bitmap bmp;
    try {
        checkTaskNotActual();
        //此处又从memory中读取了
        bmp = configuration.memoryCache.get(memoryCacheKey);
        //内存中没有数据
        if (bmp == null || bmp.isRecycled()) {
            //从disk活网络加载bitmap
            bmp = tryLoadBitmap();
       //省略代码   
            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);
        }
        //省略代码
    } catch (TaskCancelledException e) {
        fireCancelEvent();
        return;
    } finally {
        loadFromUriLock.unlock();
    }
    //要显示的task
    DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
    runTask(displayBitmapTask, syncLoading, handler, engine);
}

上面代码也说明了,如果memory中获取不到bitmap会走tryLoadBitmap方法,那咱们也去看看:

private Bitmap tryLoadBitmap() throws TaskCancelledException {
    Bitmap bitmap = null;
    try {
      //从disk中获取bitmap,注意了这里只是获取是否存在该bitmap的文件
        File imageFile = configuration.diskCache.get(uri);
      //如果disk中有我们想要的bitmap文件
        if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
            L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
            loadedFrom = LoadedFrom.DISC_CACHE;
            checkTaskNotActual();
                  //进行了decodeImage操作
            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;
                        //默认的Scheme是网络类型的
            String imageUriForDecoding = uri;
            if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
                imageFile = configuration.diskCache.get(uri);
                                //如果图片的文件存在,此时会将uri格式改成FILE类型
                if (imageFile != null) {
                    imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
                }
            }
            checkTaskNotActual();
            bitmap = decodeImage(imageUriForDecoding);
        //省略代码
        }
    } 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;
}

可以看出上面最后都进调用了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);
}

这里就顺藤摸瓜呗,看看decoderdecode方法了,这里的decoder对象其实是一个BaseImageDecoder对象,那咱们进去看看:

@Override
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);
            //输入流转成bitmap
        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;
}

上面做了两件事:获取输入流输入流转成bitmap,咱们要着重看下这里是怎么获取输入流的:

protected InputStream getImageStream(ImageDecodingInfo decodingInfo) throws IOException {
    return decodingInfo.getDownloader().getStream(decodingInfo.getImageUri(), decodingInfo.getExtraForDownloader());
}

decodingInfo.getDownloader()实际上默认是一个BaseImageDownloader对象,那咱们瞧瞧getStream方法:

@Override
public InputStream getStream(String imageUri, Object extra) throws IOException {
    switch (Scheme.ofUri(imageUri)) {
                //走网络的
        case HTTP:
        case HTTPS:
            return getStreamFromNetwork(imageUri, extra);
                //disk的cache就是走这里获取输入流
        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);
    }
}

那咱们也进去看看这两种inputStream都是怎么获取的吧:

protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
      //获取connection对象
    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;
    }
    //省略代码
    return new ContentLengthInputStream(new BufferedInputStream(imageStream, BUFFER_SIZE), conn.getContentLength());
}
protected HttpURLConnection createConnection(String url, Object extra) throws IOException {
//这里就是咋们熟悉不过的HttpURLConnection请求代码了
    String encodedUrl = Uri.encode(url, ALLOWED_URI_CHARS);
    HttpURLConnection conn = (HttpURLConnection) new URL(encodedUrl).openConnection();
    conn.setConnectTimeout(connectTimeout);
    conn.setReadTimeout(readTimeout);
    return conn;
}

也顺便看下file类型的输入流获取吧:

protected InputStream getStreamFromFile(String imageUri, Object extra) throws IOException {
    String filePath = Scheme.FILE.crop(imageUri);
    if (isVideoFileUri(imageUri)) {
        return getVideoThumbnailStream(filePath);
    } else {
//图片格式走这里,可以看到输入流对象直接是通过filePah直接new出来的
        BufferedInputStream imageStream = new BufferedInputStream(new FileInputStream(filePath), BUFFER_SIZE);
        return new ContentLengthInputStream(imageStream, (int) new File(filePath).length());
    }
}

其实咋们漏了一个点,就是什么时候存储bitmap到disk中的呢?咋们还得回去看下tryLoadBitmap方法中有这么一段:

if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
    imageFile = configuration.diskCache.get(uri);
    if (imageFile != null) {
        imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
    }
}

其实这里的tryCacheImageOnDisk方法就是进行保存到disk中的:

private boolean tryCacheImageOnDisk() throws TaskCancelledException {
    L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey);
    boolean loaded;
    try {
              //存储到disk之前先判断有没有下载到图片
        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 {
        //注意了这里的getDownloader不是上面说的BaseImageDownloader了,这里是要分情况的
    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 ImageDownloader getDownloader() {
    ImageDownloader d;
        //根据网络情况去获取不同的ImageDownloader
    if (engine.isNetworkDenied()) {
        d = networkDeniedDownloader;
    } else if (engine.isSlowNetwork()) {
        d = slowNetworkDownloader;
    } else {
                //这里是BaseImageDownloader
        d = downloader;
    }
    return d;
}

咋们知道这么回事就行了,有兴趣的去看看他们是如何获取inputstream。接着看tryCacheImageOnDisk方法中用调用了resizeAndSaveImage方法:

private boolean resizeAndSaveImage(int maxWidth, int maxHeight) throws IOException {
    // Decode image file, compress and re-save it
    boolean saved = false;
//获取目标file文件
    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);
//decode操作,将Scheme为FILE类型的inputstream转化成bitmap对象
        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) {
//这里才是真正保存bitmap到disk中的操作
            saved = configuration.diskCache.save(uri, bmp);
            bmp.recycle();
        }
    }
    return saved;
}

上面代码在保存到disk中,先是判断有没有下载到,如果有下载先进行保存inputstream中的内容,在进行保存的时候,通过copy的方式来保存到OutputStream中,这里在copy的时候,如果当前保存少于CONTINUE_LOADING_PERCENTAGE=75的时候,视为失败。这里追踪代码到LruDiskCachesave方法,其中save方法传入的是InputStream对象。最后通过targetFile文件进行decode操作获取到bitmap对象。最后又进行保存bitmap对象到LruDiskCache中。

这里我提个疑问哈,为什么在保存到disk中,为什么先去save传了一个inputstream,紧接着又去save传了一个bitmap

至此,代码分析得差不多了,如果有什么疑问,我们可以共同讨论,或者加群(184793647)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354

推荐阅读更多精彩内容