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. 项目包结构
图 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。简单类图如下:
图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。
核心类图:
图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>看,会方便些。
核心类图:
图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:
- Download JAR
- Put the JAR in the libs subfolder of your Android project
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 for application. -
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
Display Options can be applied to every display task (ImageLoader.displayImage(...)
call).
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);