github地址:   https://github.com/nostra13/Android-Universal-Image-Loader


发现一个开源项目Android-Universal-Image-Loader十分火热。代码并不十分复杂,却写的不错,决定记录和分享一下。

  Android-Universal-Image-Loader是一个针对图片加载、缓存的开源项目。github: https://github.com/nostra13/Android-Universal-Image-Loader

  @author nostra13频繁的更新记录来看,Android-Universal-Image-Loader的后期维护比较好。从Applications using Universal Image Loader来看,还是相当强大的,如:EyeEm Camera, UPnP/DLNA Browser, Facebook Photo/Album,淘宝天猫,京东商城等。到底有何种魅力让大家纷至沓来?

   

01. 项目包结构


android imageview 资源释放 android universal image loader_Image

图 1

 

  去掉无实际意义的包名:com.nostra13.universalimageloader,一切变显得比较清楚了。

  (1)cache主要是磁盘缓存及内存缓存预定的接口和常规实现类,包含的算法较多(并不复杂),如FIFO算法、LRU算法等。

  (2)core明显是整个Image Loader的核心包,图片下载、适配显示,并向上层应用提供各种接口,默认模板,还包括很多关键枚举类、工具类。

  (3)utils比较简单些,常规工具类,如ImageSizeUtils、StorageUtils等。

 


 

02. 文件存储策略

  针对手机,流量、电量及体验的要求,不断向网络发起重复的请求,都是不恰当的。常规存储方式包括:文件存储、数据库存储、SharedPreference、云存储(网络存储)及保存至内存等几种手段。显然,需要在本地进行持久化,就图片而已,以文件的方式保存无疑是最好的选择。至此,完成了云端-->本地的持久化,但是如果简单的每次都去”本地存储“请求,请求不到再到云端请求,无疑无法解决频繁请求图片所需要的速度和体验。请记住,用户对图片的渴望程序是相当高,试想一下,ListView上下滚动时,每次直接到”本地存储“请求时,整个界面多处在Loading,显然体验效果比较一般。因此,图片一般都会存储至内存中。从内存中读取图片的速度比本地加载的快得多,体验也会更好。这就是平时通常说的三步:内存-->本地文件-->云端。

  在Android-Universal-Image-Loader,单纯从包结构上就可以看出,cache.disc是负责硬盘存储,而cache.memory是负责内存存储的(ps.具体存储实现方式还是在core包内)。而网络请求应该在core.download中,core包在Part3.2会深入分析。

  事实上,我们似乎忽略了一些东西,如果我发起请求的是本地图片呢?如果是R.drawable.*的图片呢?优点1:Android-Universal-Image-Loader设计得比较细致,在ImageDownloader接口枚举类Scheme,明确指出所支持的请求类型:

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

  021. 存储空间、时间、数量策略

    在cache.disc下,有着比较完整的存储策略,根据预先指定的空间大小,使用频率(生命周期),文件个数的约束条件,都有着对应的实现策略。最基础的接口DiscCacheAware和抽象类BaseDiscCache。简单类图如下:


android imageview 资源释放 android universal image loader_Android-Universal-Im_02

图2

  从类图中,明显可以看出,整个disc存储的实现方式有四种:文件总数,文件总大小(或目录),生命周期,无限制(即空间、时间维度的限制)。分析重点,是接口/抽象类。

public interface DiscCacheAware {    /**     * This method must not to save file on file system in fact. It is called after image was cached in cache directory     * and it was decoded to bitmap in memory. Such order is required to prevent possible deletion of file after it was     * cached on disc and before it was tried to decode to bitmap.     */    void put(String key, File file);    /**     * Returns {@linkplain File file object} appropriate incoming key.<br />     * <b>NOTE:</b> Must <b>not to return</b> a null. Method must return specific {@linkplain File file object} for     * incoming key whether file exists or not.     */    File get(String key);    /** Clears cache directory */    void clear();}

  具体看英文注释,比较好理解,此处就不翻译了。

  

  022. 图片文件的命名

  如果你是心思细腻的用户,在图片浏览器中是否会经常看到一些小图片甚至于广告图片。在一定程序上,本人挺鄙视这些图片来源的应用。为什么把一些临时下载的图片,并无实际意义的图片,占据如此重要的位置。对此,我只想说:我会直接删掉这个目录,如果知道是哪个应用的话,估计我也会卸载。我不禁猜想:如果所有应用缓存的图片,都是可以被媒体库扫描出来的话,这是什么境况呢?慢,肯定的。

  因此,图片保存时,以什么样的文件名进行存储是相当重要的。这个必须要根据需求进行定制生成的策略。举三个例子:

  (1)如果要做一个“云相册”,保存图片后,当然希望能够使用“美图秀秀”、“相机360”等进行编辑。

  (2)如果要做一个“淘宝客户端”,所有浏览过的商品图片都存储,并可被其它图片浏览器感知,这似乎不恰当吧。

  (3)如果要做一个“私密相册”,保存本地的图片,自然不希望别人去感知。

  结论:用户有意识去保存的图片,应该可被感知;用户无意识缓存的图片,不应该被“外界”感知,但也有例外如第3种。当然,第三种权限的控制也是可行的。

  Android-Universal-Image-Loader在保存图片时,需要一个FileNameGenerator(译:文件名生产者)。抽象类BaseDiscCache中存在两个构造器:

public BaseDiscCache(File cacheDir) {        this(cacheDir, DefaultConfigurationFactory.createFileNameGenerator());    }    public BaseDiscCache(File cacheDir, FileNameGenerator fileNameGenerator) {        if (cacheDir == null) {            throw new IllegalArgumentException(String.format(ERROR_ARG_NULL, "cacheDir"));        }        if (fileNameGenerator == null) {            throw new IllegalArgumentException(String.format(ERROR_ARG_NULL, "fileNameGenerator"));        }        this.cacheDir = cacheDir;        this.fileNameGenerator = fileNameGenerator;    }

  DefaultConfigurationFactory提供默认文件名生产者为HashCodeFileNameGenerator,默认实现比较简单:返回字符串的hashCode。

  核心类图:


android imageview 资源释放 android universal image loader_Universal_03

图3

  Md5FileNameGenerator相关原理可查询MD5加密算法,JDK自带类java.security.MessageDigest。

 


 

03. 内存存储

  这部分涉及的知识面比较多,算法、链表、堆栈、队列、泛型等。Android-Universal-Image-Loader在“开闭原则”上做得很好,接口简洁、实用,基于接口,都可以自行扩展功能。

  直接看下接口MemoryCacheAware的定义:

public interface MemoryCacheAware<K, V> {    /**     * Puts value into cache by key     *     * @return <b>true</b> - if value was put into cache successfully, <b>false</b> - if value was <b>not</b> put into     *         cache     */    boolean put(K key, V value);    /** Returns value by key. If there is no value for key then null will be returned. */    V get(K key);    /** Removes item by key */    void remove(K key);    /** Returns all keys of cache */    Collection<K> keys();    /** Remove all items from cache */    void clear();}

  如果不熟悉泛型的话,建议复习一下,或者直接把MemoryCacheAware<K, V>当MemoryCacheAware<String, Bitmap>看,会方便些。

  核心类图:


android imageview 资源释放 android universal image loader_Universal_04

图4

  大体结构上以文件存储比较相近,关键性的区别在于BaseMemoryCache子类使用的都是WeakReference(弱引用),FuzzyKeyMemoryCache、LimitedAgeMemoryCache、LruMemoryCache使用的是强引用。本节对内存存储方式,比较关键的概念:强引用、软引用、弱引用及虚引用。

  回顾:

  (1)StrongReference(强引用)

    强引用就是平时经常使用的,如常规new Object()。如果一个对象具有强引用,那垃圾回收器绝不会回收。内存不足,甚至出现OOM时,也不会随意回收强引用的对象。

  (2)SoftReference(软引用)

    在内存空间足够,垃圾回收器不会回收它;如果内存空间不足,垃圾回收器就会回收软引用的对象。

  (3)WeakReference(弱引用)

    弱引用相对软引用,具有更短暂的生命周期。常规的GC,只要被扫描到,都会直接被回收。

  (4)PhantomReference(虚引用)

    虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。没用过。。。

  选其一FIFOLimitedMemoryCache进行分析,构造时,必须要指定大小(单位:字节)。当设置的大小超过16MB(Android默认分配的大小好像也是这个)时,会有警告。

************************************************************************************

1. Include library

Manual:

or

Maven dependency:



<dependency>
    <groupId>com.nostra13.universalimageloader</groupId>
    <artifactId>universal-image-loader</artifactId>
    <version>1.9.3</version>
</dependency>



or

Gradle dependency:



compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.3'



2. Android Manifest



<manifest>
    <!-- Include following permission if you load images from Internet -->
    <uses-permission android:name="android.permission.INTERNET" />
    <!-- Include following permission if you want to cache images on SD card -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest>



3. Application or Activity class (before the first usage of ImageLoader)



public class MyActivity extends Activity {
    @Override
    public void onCreate() {
        super.onCreate();

        // Create global configuration and initialize ImageLoader with this config
        ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this)
            ...
            .build();
        ImageLoader.getInstance().init(config);
        ...
    }
}



Configuration and Display Options

  • ImageLoader Configuration (

ImageLoaderConfiguration

  • ) is global
  • Display Options (

DisplayImageOptions

  • ) are local for every display task (

ImageLoader.displayImage(...)

  • ).

Configuration

All options in Configuration builder are optional. Use only those you really want to customize.
See default values for config options in Java docs for every option.



// DON'T COPY THIS CODE TO YOUR PROJECT! This is just example of ALL options using.
// See the sample project how to use ImageLoader correctly.
File cacheDir = StorageUtils.getCacheDirectory(context);
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
        .memoryCacheExtraOptions(480, 800) // default = device screen dimensions
        .diskCacheExtraOptions(480, 800, null)
        .taskExecutor(...)
        .taskExecutorForCachedImages(...)
        .threadPoolSize(3) // default
        .threadPriority(Thread.NORM_PRIORITY - 2) // default
        .tasksProcessingOrder(QueueProcessingType.FIFO) // default
        .denyCacheImageMultipleSizesInMemory()
        .memoryCache(new LruMemoryCache(2 * 1024 * 1024))
        .memoryCacheSize(2 * 1024 * 1024)
        .memoryCacheSizePercentage(13) // default
        .diskCache(new UnlimitedDiscCache(cacheDir)) // default
        .diskCacheSize(50 * 1024 * 1024)
        .diskCacheFileCount(100)
        .diskCacheFileNameGenerator(new HashCodeFileNameGenerator()) // default
        .imageDownloader(new BaseImageDownloader(context)) // default
        .imageDecoder(new BaseImageDecoder()) // default
        .defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default
        .writeDebugLogs()
        .build();



Display Options

ImageLoader.displayImage(...)Note: If Display Options wasn't passed to ImageLoader.displayImage(...)method then default Display Options from configuration (ImageLoaderConfiguration.defaultDisplayImageOptions(...)) will be used.


// DON'T COPY THIS CODE TO YOUR PROJECT! This is just example of ALL options using.
// See the sample project how to use ImageLoader correctly.
DisplayImageOptions options = new DisplayImageOptions.Builder()
        .showImageOnLoading(R.drawable.ic_stub) // resource or drawable
        .showImageForEmptyUri(R.drawable.ic_empty) // resource or drawable
        .showImageOnFail(R.drawable.ic_error) // resource or drawable
        .resetViewBeforeLoading(false)  // default
        .delayBeforeLoading(1000)
        .cacheInMemory(false) // default
        .cacheOnDisk(false) // default
        .preProcessor(...)
        .postProcessor(...)
        .extraForDownloader(...)
        .considerExifParams(false) // default
        .imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2) // default
        .bitmapConfig(Bitmap.Config.ARGB_8888) // default
        .decodingOptions(...)
        .displayer(new SimpleBitmapDisplayer()) // default
        .handler(new Handler()) // default
        .build();



Usage

Acceptable URIs examples



String imageUri = "http://site.com/image.png"; // from Web
String imageUri = "file:///mnt/sdcard/image.png"; // from SD card
String imageUri = "content://media/external/audio/albumart/1"; // from content provider
String imageUri = "assets://image.png"; // from assets
String imageUri = "drawable://" + R.drawable.img; // from drawables (non-9patch images)


NOTE: Use drawable:// only if you really need it! Always consider the native way to load drawables -ImageView.setImageResource(...) instead of using of ImageLoader.

Simple



// Load image, decode it to Bitmap and display Bitmap in ImageView (or any other view 
//  which implements ImageAware interface)
imageLoader.displayImage(imageUri, imageView);



// Load image, decode it to Bitmap and return Bitmap to callback
imageLoader.loadImage(imageUri, new SimpleImageLoadingListener() {
    @Override
    public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
        // Do whatever you want with Bitmap
    }
});



// Load image, decode it to Bitmap and return Bitmap synchronously
Bitmap bmp = imageLoader.loadImageSync(imageUri);



Complete



// Load image, decode it to Bitmap and display Bitmap in ImageView (or any other view 
//  which implements ImageAware interface)
imageLoader.displayImage(imageUri, imageView, options, new ImageLoadingListener() {
    @Override
    public void onLoadingStarted(String imageUri, View view) {
        ...
    }
    @Override
    public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
        ...
    }
    @Override
    public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
        ...
    }
    @Override
    public void onLoadingCancelled(String imageUri, View view) {
        ...
    }
}, new ImageLoadingProgressListener() {
    @Override
    public void onProgressUpdate(String imageUri, View view, int current, int total) {
        ...
    }
});



// Load image, decode it to Bitmap and return Bitmap to callback
ImageSize targetSize = new ImageSize(80, 50); // result Bitmap will be fit to this size
imageLoader.loadImage(imageUri, targetSize, options, new SimpleImageLoadingListener() {
    @Override
    public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
        // Do whatever you want with Bitmap
    }
});



// Load image, decode it to Bitmap and return Bitmap synchronously
ImageSize targetSize = new ImageSize(80, 50); // result Bitmap will be fit to this size
Bitmap bmp = imageLoader.loadImageSync(imageUri, targetSize, options);