从UIL库谈Android图片加载中需要注意的事情

Android Universal Image Loader 算是Android中最流行的图片加载库了,作者Sergey的确牛逼,能将整个Android图片加载的点点滴滴考虑的如此全面。网上研究这个开源库的人很多比如这个:http://codekk.com/open-source-project-analysis, 我就不做全面介绍了,感兴趣的朋友可以进入这个网站查看关于UIL库的全面介绍,这个网站上还有很多其他著名Android开源项目的分析,有兴趣的朋友可以慢慢研究哦。我今天就从UIL库的代码出发结合实际经验,发掘出一些关于Android图片加载需要注意的事情。

转载请注明出处:http://blog.csdn.net/qinjunni2014/article/details/45298989

1、如何解决同时请求重复url时的资源浪费

很多朋友肯定遇到过一些问题,比如在同时加载相同的图片时,效率会比你想象的低,尤其是在这个url还是一个错误的url的时候,很有可能你的整个app图片加载层就死掉了,这个因为你的加载引擎同时发起了多个任务,这些任务都指向了一个错误的url,在获取错误的url图片时,任务就会效率非常低。本人觉得这是图片加载最基本的问题,对于重复的url,我们应该直接忽略掉后面的请求。我们来看看UIL库是如何处理的。

class ImageLoaderEngine {
    //对某个url执行load任务时,先要获得url对应的ReentrantLock锁
    private final Map<String, ReentrantLock> uriLocks = new WeakHashMap<String, ReentrantLock>();

}

ImageloaderEngine是整个UIL处理任务的引擎,不熟悉的朋友可以上面提到的链接。可以看到engine中有一个map,key是image的url,value是一个ReentrantLock类型的锁,接下来的事情就很明了了,大家肯定会想到,如果要处理某个url,先要获得这个锁对不对?

//LoadAndDisplayImageTask.java
@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 {
         //...获取bitmap         
    } finally {
        loadFromUriLock.unlock();
    }
}

LoadAndDisplayImageTask是UIL中执行获取bitmap的任务类型,继承自Runnable对象,可以看到在执行真正的获取bitmap操作前,我们首先试图锁住loadFromUriLock锁,这个锁就是从engine中获取的。对象同的url只有一个对应的lock,如果已经有任务在执行,lock()操作就会失败,线程就会处于休眠状态。

相关讨论:http://angeldevil.me/2015/03/19/a-bug-in-universal-image-loader/

2、如何解决类似ListView情况下view可以被重用的情况

这个可能是图片加载中最常被考虑的问题了,因为ListView广泛被使用,类似的还有GridView,RecyclerView。因为view可以被重用,而图片加载往往又是异步的,因此如果不加特殊考虑,会出现图片加载不对或者图片闪烁 的情况,通常比较简单地做法就是对ImageView执行settag,把url设为imageview的tag,这样url能保持最新,在图片加载任务返回时,先检查tag是否匹配,如果相等就把bitmap画到Imageview中去。原理比较简单,不过我们还是先看看UIL是怎么做的,先上代码:

private boolean isViewReused() {
    String currentCacheKey = engine.getLoadingUriForView(imageAware);
    // Check whether memory cache key (image URI) for current ImageAware is actual.
    // If ImageAware is reused for another task then current task should be cancelled.
    boolean imageAwareWasReused = !memoryCacheKey.equals(currentCacheKey);
    if (imageAwareWasReused) {
        L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
        return true;
    }
    return false;
}

这个函数也位于LoadAndDisplayImageTask中,用于检测view时候已经被重用,这个函数首先会去从engine中去获取ImageAware的对应的uri(ImageAware可以暂且理解为imageview的包装,加上一些属性操作,读者如果不懂可以先阅读之前那个连接,或者直接读源码),其实这个和settag类似,但是这中方法更安全,如何安全?

//ImageLoaderEngine.java
private final Map<Integer, String> cacheKeysForImageAwares = Collections
            .synchronizedMap(new HashMap<Integer, String>());

以上是engine中对cacheKeysForImageAwares的定义,利用synchronizedMap,保证对cacheKey的并发安全。
有了这个isViewReused函数,我们就可以根据view时候已经被重用做出措施了。

其实手上有源码的童鞋,可以注意到LoadAndDisplayImageTask的run方法中,经常会去调用checkTaskNotActual函数,

private void checkTaskNotActual() throws TaskCancelledException {
    checkViewCollected();//检测view时候已经被回收
    checkViewReused();//检测view时候已经被重用
}

看名字就能理解了,这个函数会去检测view的有效性,如果不可用,就会抛出TaskCancelledException。run函数会捕捉这个异常做取消操作。

3、如何处理ListView滚动加载

第三个问题还是跟ListView相关,讨论在ListView进行滚动的时候,我们需要做一些处理,使得图片加载暂停,不然如果用户不停的做fling操作的话,我们需要不停的去加载一些可能最终不会呈现给用户的图片,这样会使得效率变得更低,同时也会更耗电。当然UIL也考虑到了这些。

public class PauseOnScrollListener implements OnScrollListener {

    private ImageLoader imageLoader;

    private final boolean pauseOnScroll;
    private final boolean pauseOnFling;
    private final OnScrollListener externalListener;

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        switch (scrollState) {
            case OnScrollListener.SCROLL_STATE_IDLE:
                imageLoader.resume();
                break;
            case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
                if (pauseOnScroll) {
                    imageLoader.pause();
                }
                break;
            case OnScrollListener.SCROLL_STATE_FLING:
                if (pauseOnFling) {
                    imageLoader.pause();
                }
                break;
        }
        if (externalListener != null) {
            externalListener.onScrollStateChanged(view, scrollState);
        }
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        if (externalListener != null) {
            externalListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
        }
    }
}

UIL有这个PauseOnScrollListener,这个类继承自AbsListView.OnScrollListener,顾名思义,就是在scroll的时候暂停图片的加载。因此我们只需要在ListView或者GridView上set这个类的对象,在OnScrollListener的onScrollStateChanged函数中,我们可以根据ListView的状态去决定是否要暂停ImageLoader。另外这个类的构造函数可以再传一个OnScrollListener对象,这样就可以做额外的scroll监听操作。

4、如何处理同一张图片不同尺寸的内存缓存

有经验的朋友一定遇到过这个问题,因为我们的内存缓存肯定是downScale过的bitmap,而对于同一个image对不同大小的imageView我们会downscale成不同的尺寸,是否需要在memorycache中存储多张不同尺寸的缓存呢?对于一张图片,如果只存储大尺寸的bitmap缓存,那会很占内存,memorycache总量一定的情况下,可缓存的图片较少,而只存储小尺寸的图片的话,对于大尺寸的imageview,进行放大处理后会显示模糊。那么UIL是怎么做的呢?

其实UIL默认是会对一张图片存储不同的尺寸的缓存的,因为首先它的memorycache的key中不仅包含uri而且还包括目标尺寸大小,比如 “http://example.com/a.jpg_500x400“, 那么对于不同的目标大小,会有不同的cachekey,如果不能hitcache,就会从硬盘或者网络解码出这个尺寸的bitmap,这样的话,在memorycache中对同一张图片就有可能存在多种不同尺寸的缓存。当然这对缓存的空间会照成比较大得浪费,不过UIL可以关掉这种机制,在ImageLoaderConfiguration(用来配置UIL的全局配置)中,有一个函数叫做denyCacheImageMultipleSizesInMemory, 从名字就可以看出,如果设置了这个配置,memorycache中就不会存储一张图的多尺寸缓存。

public class FuzzyKeyMemoryCache implements MemoryCache {

    private final MemoryCache cache;
    private final Comparator<String> keyComparator;

    public FuzzyKeyMemoryCache(MemoryCache cache, Comparator<String> keyComparator) {
        this.cache = cache;
        this.keyComparator = keyComparator;
    }

    @Override
    public boolean put(String key, Bitmap value) {
        // Search equal key and remove this entry
        synchronized (cache) {
            String keyToRemove = null;
            for (String cacheKey : cache.keys()) {
                if (keyComparator.compare(key, cacheKey) == 0) {
                    keyToRemove = cacheKey;
                    break;
                }
            }
            if (keyToRemove != null) {
                cache.remove(keyToRemove);
            }
        }
        return cache.put(key, value);
    }

    @Override
    public Bitmap get(String key) {
        return cache.get(key);
    }
}

MemoryCache是一个interface它规定了get,put,remove等接口函数,而FuzzyKeyMemoryCache是一个装饰类,它内部的keyComparator是一个模糊匹配比较,只需要图片的url相同就认为相等。值得注意的是,只有在put操作时做了模糊匹配,在get时并没有,这意味着,对于同一张图片,如果目标大小和缓存中得大小不一致,UIL依然会去从网络或者disk解码图片。在加入新的尺寸时,老的尺寸会被替换掉。个人觉得还是如果只是个别图片有不同尺寸的显示尺寸的话,可以开启这个配置。

5、如何解决url重定向问题

相信大部分人在自己写图片加载时,并没有考虑到这个小问题。如果图片的url并没有直接给出,而是给出了一个可重定向的url,那么http请求会返回3xx的状态码,表示url重定向,并在头部中包含Location,它的值就是重定向的目标地址。来看UIL时如何处理的:

int redirectCount = 0;
while (conn.getResponseCode() / 100 == 3 && redirectCount < MAX_REDIRECT_COUNT) {
    conn = createConnection(conn.getHeaderField("Location"), extra);
    redirectCount++;
}

它用一个循环来处理,如果返回3xx,就拿到新的地址,重新发起连接,不过最大不能超过MAX_REDIRECT_COUNT次。

6、如何处理硬件加速bitmap尺寸限制

可能对Tablet上Android开发有经验的朋友都会遇到过,图片加载成功了但是却没法显示出来的问题,而且最诡异的是没有error,令你没法调试,但是仔细查看log时,会发现有这么一条warning: Bitmap too large to be uploaded into a texture,主要原因是在做硬件加速渲染时,openGL渲染的texture对bitmap的大小是有一定限制的,很多是2048x2048,还有一些是4096x4096,取决于你的手机硬件水平还有openGL的版本。在阅读UIL源码时,发现这个问题作者也是有解决的,请参考一下代码:

public final class ImageSizeUtils {

    private static final int DEFAULT_MAX_BITMAP_DIMENSION = 2048;

    private static ImageSize maxBitmapSize;

    static {
        int[] maxTextureSize = new int[1];
        GLES10.glGetIntegerv(GL10.GL_MAX_TEXTURE_SIZE, maxTextureSize, 0);
        int maxBitmapDimension = Math.max(maxTextureSize[0], DEFAULT_MAX_BITMAP_DIMENSION);
        maxBitmapSize = new ImageSize(maxBitmapDimension, maxBitmapDimension);
    }

    /** * Computes minimal sample size for downscaling image so result image size won't exceed max acceptable OpenGL * texture size.<br /> * We can't create Bitmap in memory with size exceed max texture size (usually this is 2048x2048) so this method * calculate minimal sample size which should be applied to image to fit into these limits. * * @param srcSize Original image size * @return Minimal sample size */
    public static int computeMinImageSampleSize(ImageSize srcSize) {
        final int srcWidth = srcSize.getWidth();
        final int srcHeight = srcSize.getHeight();
        final int targetWidth = maxBitmapSize.getWidth();
        final int targetHeight = maxBitmapSize.getHeight();

        final int widthScale = (int) Math.ceil((float) srcWidth / targetWidth);
        final int heightScale = (int) Math.ceil((float) srcHeight / targetHeight);

        return Math.max(widthScale, heightScale); // max
    }
}

位于static代码块中代码就是为了获取当前设备支持的最大texture size。不过我在实际开发中注意到这段代码只有在主线程中去调用才能获取到正确地值,否则为0,不知道是否正确,请各位读者谨慎使用,不过Sergey也做了保险起见,规定最小至少能支持2048。在获取了这个值之后,我们就能通过这个maxBitmapSize来计算我们在解码bitmap时需要采取的sampleSize,保证我们解码出来的bitmap不会超过这个限制。不过默认情况下,UIL并没有开启这种计算sampleSize的模式。UIL中一个枚举类型ImageScaleType,不同的scaleType规定了计算sampleSize的不同方式。

public enum ImageScaleType {
    /** Image won't be scaled */
    NONE,
    /** * Image will be scaled down only if image size is greater than maximum acceptable texture size}. * Usually it's 2048x2048. */
    NONE_SAFE,
    /** * Image will be reduces 2-fold until next reduce step make image smaller target size.<br /> */
    IN_SAMPLE_POWER_OF_2,
    /** * Image will be subsampled in an integer number of times (1, 2, 3, ...). Use it if memory economy is quite */
    IN_SAMPLE_INT,
    /** * Image will scaled-down exactly to target size (scaled width or height or both will be equal to target size; */
    EXACTLY,
    /** * Image will scaled exactly to target size (scaled width or height or both will be equal to target size; depends on * {@linkplain android.widget.ImageView.ScaleType ImageView's scale type}). Use it if memory economy is critically * important.<br /> * <b>Note:</b> If original image size is smaller than target size then original image <b>will be stretched */
    EXACTLY_STRETCHED
}

UIL默认会采用IN_SAMPLE_POWER_OF_2,这种计算sampleSize的方式,不过读者可以通过更改DisplayImageOptions来手动改成NON_SAFE,这样UIL解码出来的bitmap大小就不会超过texture的大小上限。

7、在慢网络中下载图片

这个问题说来惭愧,本人并没有在实际开发中碰到过,说的是在网络比较慢时下载图片会出现6060这个issue: https://code.google.com/p/android/issues/detail?id=6066,总的来说基本原因是因为输入流的skip操作不完全所致,issue中也给出了解决方法,Sergey就是采取了这种做法,用户可以调用ImageLoader的handleSlowNetwork方法开启慢网络模式,在这种模式下,下载的操作由SlowNetworkImageDownloader完成

private static class SlowNetworkImageDownloader implements ImageDownloader {

    private final ImageDownloader wrappedDownloader;

    public SlowNetworkImageDownloader(ImageDownloader wrappedDownloader) {
        this.wrappedDownloader = wrappedDownloader;
    }

    @Override
    public InputStream getStream(String imageUri, Object extra) throws IOException {
        InputStream imageStream = wrappedDownloader.getStream(imageUri, extra);
        switch (Scheme.ofUri(imageUri)) {
            case HTTP:
            case HTTPS:
                return new FlushedInputStream(imageStream);
            default:
                return imageStream;
        }
    }
}

可以看到这个类也是个装饰类,它在普通的ImageDownloader外面包装了一层,它返回的输入流也是在ImageDownloader的输入流的基础上包装。

public class FlushedInputStream extends FilterInputStream {

    public FlushedInputStream(InputStream inputStream) {
        super(inputStream);
    }

    @Override
    public long skip(long n) throws IOException {
        long totalBytesSkipped = 0L;
        while (totalBytesSkipped < n) {
            long bytesSkipped = in.skip(n - totalBytesSkipped);
            if (bytesSkipped == 0L) {
                int by_te = read();
                if (by_te < 0) {
                    break; // we reached EOF
                } else {
                    bytesSkipped = 1; // we read one byte
                }
            }
            totalBytesSkipped += bytesSkipped;
        }
        return totalBytesSkipped;
    }
}

这个类重写了skip操作,不断尝试skip直到遇到EOF或者skip完指定字符数。有大神对这个issue十分了解的,别忘了评论哦。

好了,先写到这里,基本上UIL为我们考虑到了图片所有应该考虑的事情,这里没有提到的比如Memorycache, DiskCache,BitmapDisplayer等,UIL都进行了完美的实现,有兴趣的童鞋可以直接阅读源码,难度不大,我在这里只是列举了一些典型的问题,让大家在写自己的图片加载库时有一个很好的参考。基本上UIL已经是一个很成熟的库了,再次感谢作者Sergey!!再会。

你可能感兴趣的:(android)