Android-Universal-Image-Loader阅读笔记(二)

Android-Universal-Image-Loader阅读笔记(一)
上一篇主要介绍了一些参数和基础的用法

简述

上一篇有提到在displayImage的时候,首先会在调用线程中检查是否击中内存缓存,如果击中内存缓存,在没有指定postProcessor的情况下就直接在当前线程回调结果。这个过程如果是在主线程中进行,则整个任务都会完结在主线程中。
如果没有击中内存缓存,接着任务就到了engine.submit(displayTask)中,接下来看一下ImageLoaderEngine

ImageLoaderEngine

    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();//如果ImageLoader之前进行了stop,那么这里要尝试使用可用的线程池
                if (isImageCachedOnDisk) {//当前命中硬盘缓存,通过专门处理缓存的线程池执行任务
                    taskExecutorForCachedImages.execute(task);
                } else {//当前没有命中硬盘缓存,通过专门处理从流(网络等来源)中获取图片的线程池执行任务
                    taskExecutor.execute(task);
                }
            }
        });
    }

    /**
     * 暂停后续的图片加载任务,当前已经进行中的任务无法暂停
     * 只能通过resume恢复暂停后添加的新任务(重复的任务会不断的覆盖)
     */
    void pause() {
        paused.set(true);//实际上就是标记一下当前ImageLoaderEngine暂停
    }

    /**
     * 继续图片的加载任务,并且会尝试唤醒之前暂停的任务
     */
    void resume() {
        paused.set(false);//标记当前ImageLoaderEngine可以运行
        synchronized (pauseLock) {
            pauseLock.notifyAll();//尝试唤醒之前因为pause而处于wait状态的加载线程,从而开始之前的任务
        }
    }

简要说就是当加载一个图片的时候,首先在UI线程中尝试击中内存缓存,如果没有击中,会立刻在子线程中尝试击中硬盘缓存,如果击中,通过缓存专用的线程池调度到另一个线程中执行task,否则通过流专用的线程池调度到另一个线程中执行task,总之就是到不同线程中执行同一个任务。
接着看任务LoadAndDisplayImageTask本身

LoadAndDisplayImageTask

实际上就是一个Runnable,用于在线程池中执行

    @Override
    public void run() {
        //ImageLoaderEngine是否被暂停,如果暂停,则当前任务直接结束
        //不过还是可以从内存缓存中获取图片,这个在列表页滑动加载还是很有意义的
        //因为线程池默认是fixed类型的线程池,那么pause的时候最多会导致核心线程数的等待
        //后续任务进入都会进行线程池的队列中等待执行
        if (waitIfPaused()) return;
        //如果配置了当前任务需要延时读取,则当前线程会沉睡指定毫秒后唤醒,如果任务有效则继续执行
        if (delayIfNeed()) return;
        //这个是当前链接对应的线程锁
        //一个连接对应有一个ReentrantLock
        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 {
            //对于同一个链接的任务,有的任务在等待后重新执行,此时可能过了一段时间
            //需要检查任务的有效性,如果无效直接进入catch
            checkTaskNotActual();
            //再次尝试从内存缓存中获取
            //主要场景就是当多个相同链接的请求发生的时候,只有一个任务正常执行,其余任务阻塞
            //那么当第一个任务完成之后,下一个任务唤醒之后,此时可能因为第一个任务的成功而导致内存缓存中有值
            //此时从内存缓存中获取即可,不必要再次进行多余操作
            bmp = configuration.memoryCache.get(memoryCacheKey);
            if (bmp == null || bmp.isRecycled()) {//内存缓存中还是没有数据
                //从硬盘或者网络上尝试获取Bitmap
                bmp = tryLoadBitmap();
                if (bmp == null) return; // listener callback already was fired
                //bitmap在缓存到内存缓存中之前可能要进行preProcessor操作
                //在这之前先检查任务的有效性
                checkTaskNotActual();
                checkTaskInterrupted();
                //后续操作和硬盘缓存已经没有关系

                //在进行内存缓存前可以对Bitmap进行操作
                //这个对应于从内存缓存中获取bitmap之后的postProcessor
                if (options.shouldPreProcess()) {
                    L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
                    bmp = options.getPreProcessor().process(bmp);
                    if (bmp == null) {
                        L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey);
                    }
                }
                //当前允许将bitmap存入内存缓存中
                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);
            }
            //这里的bitmap可以看做从内存缓存中获取的,
            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();//这个异常仅对应与任务取消异常,会回调onLoadingCancelled
            return;
        } finally {//注意ReentrantLock的基础,必须在try/catch/finally模块中,否则可能出现锁无法释放的情况
            loadFromUriLock.unlock();
        }
        //进行展示任务
        DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
        runTask(displayBitmapTask, syncLoading, handler, engine);
    }

    /** 
     * 如果有必要,wait当前线程
     * */
    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()的锁,并且当前线程等待执行
                        //ImageLoaderEngine在resume中通过engine.getPauseLock().notifyAll即可唤醒当前所有在等待中的线程
                        engine.getPauseLock().wait();
                    } catch (InterruptedException e) {
                        L.e(LOG_TASK_INTERRUPTED, memoryCacheKey);
                        return true;
                    }
                    L.d(LOG_RESUME_AFTER_PAUSE, memoryCacheKey);
                }
            }
        }
        //当任务被唤醒时,因为可能隔了一段时间,重新检查一下当前任务的有效性
        return isTaskNotActual();
    }

    /** 
     * 如果需要,尝试延时当前线程的执行
     * */
    private boolean delayIfNeed() {
        //如果在DisplayOption中配置了延时读取的毫秒
        if (options.shouldDelayBeforeLoading()) {
            L.d(LOG_DELAY_BEFORE_LOADING, options.getDelayBeforeLoading(), memoryCacheKey);
            try {//线程沉睡指定毫秒
                Thread.sleep(options.getDelayBeforeLoading());
            } catch (InterruptedException e) {
                L.e(LOG_TASK_INTERRUPTED, memoryCacheKey);
                return true;
            }
            //唤醒后,因为过了一段时间,需要重新检查当前任务的有效性
            return isTaskNotActual();
        }
        return false;
    }

    /**
     * 尝试从硬盘和网络上获取Bitmap
     * @return 获取的bitmap
     * @throws TaskCancelledException 当前任务已经无效异常
     */
    private Bitmap tryLoadBitmap() throws TaskCancelledException {
        Bitmap bitmap = null;
        try {
            //从硬盘缓存中根据链接获取指定文件
            File imageFile = configuration.diskCache.get(uri);
            //该文件是可读性质的,同时要求长度>0
            if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {//击中硬盘缓存
                L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
                loadedFrom = LoadedFrom.DISC_CACHE;
                //即将进行图片的压缩等处理,先检查任务的有效性
                checkTaskNotActual();
                //根据uri解析bitmap,这个Scheme中定义了ImageLoader可以识别的前缀,具体看Scheme类
                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;
                //DisplayOptions中设置允许缓存在硬盘中的话,尝试从网络上加载图片并且缓存在硬盘中
                if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
                    //尝试从硬盘中获取刚刚通过网络等方式获取的图片
                    //这里除非在ImageLoaderConfiguration中指定硬盘缓存中的最大宽高
                    //否则一般来说就是原图
                    imageFile = configuration.diskCache.get(uri);
                    if (imageFile != null) {
                        //获取成功后需要添加file的Scheme
                        imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
                    }
                }
                //准备进行拉伸压缩等操作,先检查任务的有效性
                checkTaskNotActual();
                //如果允许硬盘缓存的话,再次解析文件,压缩等操作(之前进行过压缩,所以这里基本上就是过一遍判断)
                //否则就是从网络上获取流,然后压缩等操作,不会进行硬盘缓存
                bitmap = decodeImage(imageUriForDecoding);

                if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
                    fireFailEvent(FailType.DECODING_ERROR, null);
                }
            }
            //普通的异常意味着获取图片失败,会设置fail时候设置的图片,并且回调onLoadingFailed
            //有一个异常比较特殊,任务取消异常,这个会在上一级catch中捕捉
        } 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;
    }

    /**
     * 根据uri解析图片(uri可能是http、drawable、content等,具体看Scheme类)
     * @param imageUri 当前要处理的图片URI
     * @return 压缩拉伸等处理后的Bitmap
     */
    private Bitmap decodeImage(String imageUri) throws IOException {
        //获取压缩类型CROP或FIT_INSIDE
        ViewScaleType viewScaleType = imageAware.getScaleType();
        //创建解析Bitmap所需要的参数
        ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
                getDownloader(), options);
        //默认decoder在ImageLoaderConfiguration中创建BaseImageDecoder
        return decoder.decode(decodingInfo);
    }

    /**
     * 尝试将图片缓存到硬盘中
     * */
    private boolean tryCacheImageOnDisk() throws TaskCancelledException {
        L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey);

        boolean loaded;
        try {
            //从网络上加载图片,并且缓存进硬盘当中
            loaded = downloadImage();
            if (loaded) {//缓存成功
                //如果配置当中设置了硬盘缓存的最大宽高,则需要改变bitmap的宽高并且覆盖缓存
                int width = configuration.maxImageWidthForDiskCache;
                int height = configuration.maxImageHeightForDiskCache;
                if (width > 0 || height > 0) {//注意这里的resize会影响到内存缓存的结果,因为内存缓存是在最后面操作
                    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;
    }

    /**
     * 从网络(或者文件之类)上获取图片的输入流,并且保存进硬盘当中
     * @return true表示硬盘缓存成功
     */
    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 {
        boolean saved = false;

        File targetFile = configuration.diskCache.get(uri);
        //从网络或者其它来源加载图片输入流成功之后,会先将流存入硬盘缓存中
        //这里会尝试再次取出
        if (targetFile != null && targetFile.exists()) {
            ImageSize targetImageSize = new ImageSize(maxWidth, maxHeight);
            //指定IN_SAMPLE_INT的时候,只会进行压缩处理,不会拉伸
            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) {//将处理后的bitmap重新写入硬盘缓存中进行覆盖
                saved = configuration.diskCache.save(uri, bmp);
                bmp.recycle();
            }
        }
        return saved;
    }

看到这里,ImageLoader的整个加载流程就已经清楚了。
1.先判断当前uri是否为空,如果为空,取消同一个ImageAware的请求,并且设置链接为空的时候的默认图。否则继续步骤2
2.确定想要的图片宽高,标记当前ImageAware的请求(可以使得旧的请求失效),从内存缓存中获取bitmap,获取成功,执行展示任务展示图片即可。否则继续步骤3
3.首先展示设置的加载中默认图或者重置当前载体中的图片,然后进入子线程中尝试从硬盘缓存中获取缓存,然后进入另一个子线程中处理。
4.判断当前ImageLoader是否暂停,如果暂停,wait当前线程。否则继续步骤5
5.判断当前加载任务是否需要延时,如果需要,让当前线程sleep指定时间,然后继续步骤6
6.获取当前链接对应的锁,否则等待。
7.从内存缓存中获取bitmap,命中缓存则开始展示任务,否则继续步骤8
8.从硬盘缓存中获取图片,如果没有命中缓存,从网络等来源获取图片的输入流,然后将原图先缓存到硬盘缓存中,如果设置了硬盘缓存的最大宽高,会重新读取一次硬盘缓存,然后压缩拉伸图片,最后再存入硬盘缓存中。
9.进行压缩拉伸等处理,然后如果允许缓存到内存缓存中,将当前压缩处理后的bitmap缓存到内存缓存中
10.最后执行展示图片任务即可

ImageDownloader

用于通过URI获取图片的输入流

    /**
     * 从指定的URI中获取图片的输入流
     *
     * @param imageUri 图片的URI
     * @param extra 在DisplayImageOptions中通过extraForDownloader指定的参数
     * @return 图片的输入流
     */
    InputStream getStream(String imageUri, Object extra) throws IOException;

    public enum Scheme {
        HTTP("http"), HTTPS("https"), FILE("file"), CONTENT("content"), ASSETS("assets"), DRAWABLE("drawable"), UNKNOWN("");

根据Scheme来看,ImageLoader支持从网络上面来的图片、硬盘上的图片、ContentProvider、assets目录下的图片和资源文件。
看一下ImageLoader默认的Downloader实现
BaseImageDownloader

    @Override
    public InputStream getStream(String imageUri, Object extra) throws IOException {
        //根据不同的Scheme获取对应的流,一个个看
        switch (Scheme.ofUri(imageUri)) {
            case HTTP:
            case HTTPS:
                //网络
                return getStreamFromNetwork(imageUri, extra);
            case FILE:
                //文件,主要用于硬盘缓存,当然也可以自己写文件路径
                return getStreamFromFile(imageUri, extra);
            case CONTENT:
                //内容提供者,当然就包括一些文件,里面主要是视频的缩略图、联系人的头像等,当然主要是照相和选择图片的uri
                return getStreamFromContent(imageUri, extra);
            case ASSETS:
                //assets中的资源
                return getStreamFromAssets(imageUri, extra);
            case DRAWABLE:
                //res中的图片资源
                return getStreamFromDrawable(imageUri, extra);
            case UNKNOWN:
            default:
                //异常处理,直接抛出异常
                return getStreamFromOtherSource(imageUri, extra);
        }
    }

    /**
     * 从网络上获取对应输入流
     */
    protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
        HttpURLConnection conn = createConnection(imageUri, extra);

        int redirectCount = 0;
        //状态码如果是300-399的状态,认为当前链接存在重定向的行为,此时重定向之后会将新的地址放入报头的Location中
        //之所以最多5次,是因为HttpURLConnection内部最多允许5次重定向http://blog.csdn.net/flowingflying/article/details/18731667
        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)
            //如果链接成功但是出现了异常,Connection仍然会返回一个错误流,此时为了继续使用Connection,最好将流全部读取并且关闭
            IoUtils.readAndCloseStream(conn.getErrorStream());
            throw e;
        }
        //判断当前返回标志是否为200
        if (!shouldBeProcessed(conn)) {//当前没有返回200,认为返回失败
            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返回200状态码才认为本次网络连接成功
     */
    protected boolean shouldBeProcessed(HttpURLConnection conn) throws IOException {
        return conn.getResponseCode() == 200;
    }

    /**
     * 根据指定的http协议的url创建指定的HttpURLConnetion
     */
    protected HttpURLConnection createConnection(String url, Object extra) throws IOException {
        //这里对链接进行了encode操作,当中指定了一些需要避免encode的字符
        String encodedUrl = Uri.encode(url, ALLOWED_URI_CHARS);
        HttpURLConnection conn = (HttpURLConnection) new URL(encodedUrl).openConnection();
        //默认连接超时5s,读取图片输入流超时20s
        conn.setConnectTimeout(connectTimeout);
        conn.setReadTimeout(readTimeout);
        return conn;
    }

    /**
     * 返回来源于文件的图片的输入流
     */
    protected InputStream getStreamFromFile(String imageUri, Object extra) throws IOException {
        //获取文件的绝对路径(不包含Scheme)
        String filePath = Scheme.FILE.crop(imageUri);
        if (isVideoFileUri(imageUri)) {
            //如果是video类型文件,尝试通过video文件创建一个缩略图
            return getVideoThumbnailStream(filePath);
        } else {//普通的文件
            BufferedInputStream imageStream = new BufferedInputStream(new FileInputStream(filePath), BUFFER_SIZE);
            return new ContentLengthInputStream(imageStream, (int) new File(filePath).length());
        }
    }

    /**
     * 根据video文件路径生成缩略图,并且获得缩略图的输入流
     */
    @TargetApi(Build.VERSION_CODES.FROYO)
    private InputStream getVideoThumbnailStream(String filePath) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
            //创建缩略图
            Bitmap bitmap = ThumbnailUtils
                    .createVideoThumbnail(filePath, MediaStore.Images.Thumbnails.FULL_SCREEN_KIND);
            if (bitmap != null) {
                //将bitmap转为输入流返回
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                bitmap.compress(CompressFormat.PNG, 0, bos);
                return new ByteArrayInputStream(bos.toByteArray());
            }
        }
        return null;
    }

    /**
     * 通过内容提供者的uri形式获取图片的输入流
     */
    protected InputStream getStreamFromContent(String imageUri, Object extra) throws FileNotFoundException {
        ContentResolver res = context.getContentResolver();

        Uri uri = Uri.parse(imageUri);
        if (isVideoContentUri(uri)) {//当前uri为video类型
            //video类型只获取对应缩略图
            Long origId = Long.valueOf(uri.getLastPathSegment());
            Bitmap bitmap = MediaStore.Video.Thumbnails
                    .getThumbnail(res, origId, MediaStore.Images.Thumbnails.MINI_KIND, null);
            if (bitmap != null) {
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                bitmap.compress(CompressFormat.PNG, 0, bos);
                return new ByteArrayInputStream(bos.toByteArray());
            }
        } else if (imageUri.startsWith(CONTENT_CONTACTS_URI_PREFIX)) {
            //如果uri是联系人类型,返回联系人的头像
            return getContactPhotoStream(uri);
        }
        //对于一般的图片来说,直接通过ContentResolver和Uri打开输入流即可
        //比方说系统的选择图片之类的
        return res.openInputStream(uri);
    }

    /**
     * 通过制定URI获取联系人头像的输入流
     */
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    protected InputStream getContactPhotoStream(Uri uri) {
        ContentResolver res = context.getContentResolver();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            return ContactsContract.Contacts.openContactPhotoInputStream(res, uri, true);
        } else {
            return ContactsContract.Contacts.openContactPhotoInputStream(res, uri);
        }
    }

    /**
     * 从assets中读取文件的输入流
     */
    protected InputStream getStreamFromAssets(String imageUri, Object extra) throws IOException {
        String filePath = Scheme.ASSETS.crop(imageUri);
        //通过这种形式可以直接是相对路径
        return context.getAssets().open(filePath);
    }

    /**
     * 通过drawable://123123这种类型的uri获取资源文件的输入流
     */
    protected InputStream getStreamFromDrawable(String imageUri, Object extra) {
        //这里要求的是drawable://123123,其中12313应该是资源id,通过R.drawable.icon获取
        String drawableIdString = Scheme.DRAWABLE.crop(imageUri);
        int drawableId = Integer.parseInt(drawableIdString);
        return context.getResources().openRawResource(drawableId);
    }

这个只是ImageLoader的默认实现,一般来说可以自己实现从网络上获取图片,结合自己项目中使用的网络库。
最后看一下图片的压缩拉伸处理

ImageDecoder

当从网络上获取图片之后,因为原图有的时候可能远远大于载体,处于对内存的考虑也是应该进行压缩等操作的,而ImageDecoder就是做这件事情的。

    public interface ImageDecoder {

    /**
     * 根据指定的目标大小和参数处理图片
     * @param imageDecodingInfo 处理的时候可能需要使用的参数
     * @return 处理后的bitmap
     */
    Bitmap decode(ImageDecodingInfo imageDecodingInfo) throws IOException;
}

可以看到,职责很单一,就是通过给定的参数对bitmap进行处理,接着看默认的实现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 {
            //根据Bitmap和图像的方向获得图片大小等信息
            imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
            //BitmapFactory是不支持多次使用同一个流的,所以要么重置,要么重新来获取图片的输入流
            //默认的网络使用的是BufferedInputStream,是可以重置流的,不需要重新发起网络请求
            imageStream = resetStream(imageStream, decodingInfo);
            //根据当前获得的bitmap的宽高和载体的宽高,计算并设置Options的压缩比例,主要是inSampleSize
            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;
    }

    protected ImageFileInfo defineImageSizeAndRotation(InputStream imageStream, ImageDecodingInfo decodingInfo)
            throws IOException {
        Options options = new Options();
        //当前不会将bitmap的数据加载入内存,只会获得相关参数,避免造成内存过大的占用
        options.inJustDecodeBounds = true;
        //解析当前流,并且将数据记录在Options中,主要是为了记录原图的宽高
        BitmapFactory.decodeStream(imageStream, null, options);

        ExifInfo exif;
        String imageUri = decodingInfo.getImageUri();
        //默认一般的图片方向都是不考虑的,但是比方说有的自定义使用照相API的时候拍的图片可能要考虑
        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);
    }

    /**
     * 首先是本地的图片,而且jpeg格式的图片才需要考虑图片的方向问题
     * 这个场景比方说手机照相
     */
    private boolean canDefineExifParams(String imageUri, String mimeType) {
        return "image/jpeg".equalsIgnoreCase(mimeType) && (Scheme.ofUri(imageUri) == Scheme.FILE);
    }

    /**
     * 获取图片流,这种形式从文件或者网络获取都有可能
     */
    protected InputStream getImageStream(ImageDecodingInfo decodingInfo) throws IOException {
        return decodingInfo.getDownloader().getStream(decodingInfo.getImageUri(), decodingInfo.getExtraForDownloader());
    }

    /**
     * 判断当前图片的方向决定旋转的角度之类
     */
    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);
    }

    /**
     * 准备实际上用于解析Bitmap的参数
     * @param imageSize 需要解析的Bitmap的原大小
     * @param decodingInfo 解析参数
     */
    protected Options prepareDecodingOptions(ImageSize imageSize, ImageDecodingInfo decodingInfo) {
        ImageScaleType scaleType = decodingInfo.getImageScaleType();
        int scale;
        //根据配置中的压缩模式来计算压缩比例
        if (scaleType == ImageScaleType.NONE) {//不进行压缩
            scale = 1;
        } else if (scaleType == ImageScaleType.NONE_SAFE) {
            //这个和NONE没有太大区别,只有图片非常大的时候才会有压缩的作用
            scale = ImageSizeUtils.computeMinImageSampleSize(imageSize);
        } else {
            //获取当前载体的宽高
            ImageSize targetSize = decodingInfo.getTargetSize();
            //当前是否按照循环2除的方式获取压缩比例
            boolean powerOf2 = scaleType == ImageScaleType.IN_SAMPLE_POWER_OF_2;
            //压缩逻辑,具体看方法内部
            scale = ImageSizeUtils.computeImageSampleSize(imageSize, targetSize, decodingInfo.getViewScaleType(), powerOf2);
        }
        if (scale > 1 && loggingEnabled) {
            L.d(LOG_SUBSAMPLE_IMAGE, imageSize, imageSize.scaleDown(scale), scale, decodingInfo.getImageKey());
        }

        Options decodingOptions = decodingInfo.getDecodingOptions();
        //设置压缩大小
        decodingOptions.inSampleSize = scale;
        return decodingOptions;
    }

    /**
     * 重置输入流,用于再次获取Bitmap
     * 一般来说流用过之后要再次使用必须重置或者重新获取
     */
    protected InputStream resetStream(InputStream imageStream, ImageDecodingInfo decodingInfo) throws IOException {
        if (imageStream.markSupported()) {//当前流支持定位功能
            try {
                //重置流
                imageStream.reset();
                return imageStream;
            } catch (IOException ignored) {
            }
        }
        //如果当前流不支持定位功能,先关闭旧的流,再重新获取输入流
        IoUtils.closeSilently(imageStream);
        return getImageStream(decodingInfo);
    }

    protected Bitmap considerExactScaleAndOrientatiton(Bitmap subsampledBitmap, ImageDecodingInfo decodingInfo,
            int rotation, boolean flipHorizontal) {
        Matrix m = new Matrix();
        ImageScaleType scaleType = decodingInfo.getImageScaleType();
        //可以在ImageLoaderConfiguration或DisplayOptions中配置ImageScaleType
        //如果为EXACTLY系列,要考虑拉伸或者压缩到target大小
        if (scaleType == ImageScaleType.EXACTLY || scaleType == ImageScaleType.EXACTLY_STRETCHED) {
            ImageSize srcSize = new ImageSize(subsampledBitmap.getWidth(), subsampledBitmap.getHeight(), rotation);
            float scale = ImageSizeUtils.computeImageScale(srcSize, decodingInfo.getTargetSize(), decodingInfo
                    .getViewScaleType(), scaleType == ImageScaleType.EXACTLY_STRETCHED);
            if (Float.compare(scale, 1f) != 0) {
                m.setScale(scale, scale);

                if (loggingEnabled) {
                    L.d(LOG_SCALE_IMAGE, srcSize, srcSize.scale(scale), scale, decodingInfo.getImageKey());
                }
            }
        }

        if (flipHorizontal) {//实际上就是每一个x坐标取-,看上去就是横向的镜像对称
            m.postScale(-1, 1);

            if (loggingEnabled) L.d(LOG_FLIP_IMAGE, decodingInfo.getImageKey());
        }

        if (rotation != 0) {//是否要旋转bitmap
            m.postRotate(rotation);

            if (loggingEnabled) L.d(LOG_ROTATE_IMAGE, rotation, decodingInfo.getImageKey());
        }
        //处理拉伸/压缩,旋转和颠倒,这个过程相对会耗费一些内存
        Bitmap finalBitmap = Bitmap.createBitmap(subsampledBitmap, 0, 0, subsampledBitmap.getWidth(), subsampledBitmap
                .getHeight(), m, true);
        if (finalBitmap != subsampledBitmap) {
            subsampledBitmap.recycle();//手动回收旧的bitmap,已经没用了
        }
        return finalBitmap;
    }

大致可以将以上流程分为5步
1.获取图片的输入流
2.获得当前图片的宽高(不应该将bitmap载入内存)和应该旋转的角度
3.重置输入流
4.通过载体的大小计算压缩比例
5.根据之前计算的选择角度和压缩比例等获得最终的bitmap
接下来关心一下ImageLoader的压缩处理算法:
上面有提到,压缩处理与ImageScaleType相关,其中NONE是完全不压缩,NONE_SAFE是保证图片不大于默认的最大大小,基本上就是2048*2048.
重点看一下IN_SAMPLE_POWER_OF_2、IN_SAMPLE_INT、EXACTLY和EXACTLY_STRETCHED,这些比较常用一点。

    public static int computeImageSampleSize(ImageSize srcSize, ImageSize targetSize, ViewScaleType viewScaleType,
            boolean powerOf2Scale) {
        //获取Bitmap的原始宽高
        final int srcWidth = srcSize.getWidth();
        final int srcHeight = srcSize.getHeight();
        //获取载体的宽高
        final int targetWidth = targetSize.getWidth();
        final int targetHeight = targetSize.getHeight();

        int scale = 1;
        //inSampleSize是用于压缩的,一般来说2的话就意味着宽和高都变成原来的一般,从像素和内存的角度就是变成了1/4
        switch (viewScaleType) {
            case FIT_INSIDE://对应于FIT_XY系列
                if (powerOf2Scale) {//ImageScaleType.IN_SAMPLE_POWER_OF_2
                    final int halfWidth = srcWidth / 2;
                    final int halfHeight = srcHeight / 2;
                    //循环2除,直到bitmap的宽高都小于等于载体的宽高
                    //FIT系列在设置的时候会自动拉伸,所以说FIT_XY会有拉伸
                    //注意到这里如果视图没有绘制完成或者wrap_content,并且没有指定maxHeight/Width的时候调用
                    //此时target默认为屏幕,图片基本可以认为不会进行压缩
                    while ((halfWidth / scale) > targetWidth || (halfHeight / scale) > targetHeight) {
                        scale *= 2;
                    }
                } else {//IN_SAMPLE_INT、EXACTLY、EXACTLY_STRETCHED
                    //这种方式不一定会得到2的倍数,但是BitmapFactory的inSampleSize只支持2的倍数
                    //其中BitmapFactory会自动向下取,比方说这里取7,但是实际上用的是4,会比预期要大
                    scale = Math.max(srcWidth / targetWidth, srcHeight / targetHeight);
                }
                break;
            case CROP://对应于CROP系列
                if (powerOf2Scale) {
                    final int halfWidth = srcWidth / 2;
                    final int halfHeight = srcHeight / 2;
                    //只要有宽/高压缩到target的宽/高即可,就是说可能存在另一边大于target的情况
                    while ((halfWidth / scale) > targetWidth && (halfHeight / scale) > targetHeight) {
                        scale *= 2;
                    }
                } else {
                    //这种方式不一定会得到2的倍数,但是BitmapFactory的inSampleSize只支持2的倍数
                    //其中BitmapFactory会自动向下取,比方说这里取7,但是实际上用的是4
                    //这里预期是宽/高有一边到达target的宽/高即可,这样会导致可能两边都比target大
                    scale = Math.min(srcWidth / targetWidth, srcHeight / targetHeight);
                }
                break;
        }

        if (scale < 1) {//注意这里是没有拉伸操作的,当然inSampleSize也不支持<1,默认也会使用1
            scale = 1;
        }
        //不允许大于2048x2048
        scale = considerMaxTextureSize(srcWidth, srcHeight, scale, powerOf2Scale);
        //如果不采用2除的方式,inSampleSize默认会取1,2,4,8...,从而向下取整
        return scale;
    }

    /**
     * 这个方法只有EXACTLY和EXACTLY_STRETCHED会执行
     **/
    public static float computeImageScale(ImageSize srcSize, ImageSize targetSize, ViewScaleType viewScaleType,
            boolean stretch) {
        //压缩后的bitmap的宽高
        final int srcWidth = srcSize.getWidth();
        final int srcHeight = srcSize.getHeight();
        //载体的宽高
        final int targetWidth = targetSize.getWidth();
        final int targetHeight = targetSize.getHeight();
        //
        final float widthScale = (float) srcWidth / targetWidth;
        final float heightScale = (float) srcHeight / targetHeight;

        final int destWidth;
        final int destHeight;
        //将宽/高其中的靠近载体的一边变成target的大小,另一边按照比例拉伸或者压缩
        if ((viewScaleType == ViewScaleType.FIT_INSIDE && widthScale >= heightScale) ||
                (viewScaleType == ViewScaleType.CROP && widthScale < heightScale)) {
            destWidth = targetWidth;
            destHeight = (int) (srcHeight / widthScale);
        } else {
            destWidth = (int) (srcWidth / heightScale);
            destHeight = targetHeight;
        }

        float scale = 1;
        //EXACTLY的时候只会进行压缩
        //FIT系列会按照最大边进行压缩,那么压缩完之后bitmap的宽高必然小于载体
        //CROP系列会按照最小边进行压缩,那么压缩完是保证至少有一边满足载体要求
        //EXACTLY_STRETCHED会进行压缩或者拉伸,压缩的表现同EXACTLY
        //FIT系列会进行拉伸,知道有一边满足载体要求
        //CROP系列在压缩的时候,基本上会保证bitmap大于等于载体,拉伸基本上没啥可能
        //一般来说提供相同比例的图片,那么就是宽高都是完全匹配的,这两种模式没有任何区别
        if ((!stretch && destWidth < srcWidth && destHeight < srcHeight) || (stretch && destWidth != srcWidth && destHeight != srcHeight)) {
            scale = (float) destWidth / srcWidth;
        }

        return scale;
    }

稍微总结一下:
首先是ImageScaleType对压缩的影响
FIX系列:保证宽高都必定小于等于载体的宽高。
CROP系列:保证宽/高有一边小于等于载体即可。
IN_SAMPLE_POWER_OF_2:遵循BitmapFactory的inSampleSize规则,每一次压缩必定是2的倍数。最后压出来的图片在展示的时候是依赖ImageView本身的ScaleType,比方说FITXY的时候,实际上bitmap是比ImageView小的,但是ImageView在展示的时候会拉伸bitmap,从而铺满ImageView。
IN_SAMPLE_INT:直接计算压缩比例,会导致计算的数字不满足2的倍数,那么BitmapFactory在处理的时候会向下取2的倍数,也就是说算出来是7,实际上压缩的时候用的是4。这样会导致压缩之后的bitmap偏大。最后展示的时候同样会依赖于ImageView的ScaleType。比起IN_SAMPLE_POWER_OF_2来说,个人认为相对会占用多一点的内存。
EXACTLY:压缩的算法使用的是IN_SAMPLE_INT,在IN_SAMPLE_INT的处理基础上,如果当前bitmap比载体还要大一些,会进行二次压缩。
FIT系列要求宽高都小于载体,CROP保证至少有一边等于载体。
如果当前bitmap比载体要小,不会拉伸bitmap。
后续的展示依赖于ImageView的ScaleType。
EXACTLY_STRETCHED:在IN_SAMPLE_INT的处理基础上,会压缩或者拉伸bitmap。
FIT系列要求宽高都小于载体,CROP保证至少有一边等于载体。
上述的所有处理都会保持bitmap原有的宽高比例。

总结

ImageLoader基本上是满足大部分的使用,个人觉得有两个比较明显的问题,一个是不支持gif。其次就是从硬盘和网络上获取图片的时候使用的是cache线程池,这样在图片很多的时候会导致CPU非常忙碌。

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

推荐阅读更多精彩内容