編輯:關於Android編程
Universal-Image-Loader是一個強大而又靈活的用於加載、緩存、顯示圖片的Android庫。它提供了大量的配置選項,使用起來非常方便。
首次配置
在第一次使用ImageLoader時,必須初始化一個全局配置,一般會選擇在Application中配置。
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
//為ImageLoader初始化一個全局配置
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this)
...
.build();
ImageLoader.getInstance().init(config);//初始化
...
}
}
可選的所有配置如下。
// 不要把這些拷貝到你的項目中! 這裡僅僅是例舉出所有可用的選項,根據自身情況進行配置。。
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 UnlimitedDiskCache(cacheDir)) // default //磁盤緩存
.diskCacheSize(50 * 1024 * 1024) //磁盤緩存大小,只在使用默認緩存有效
.diskCacheFileCount(100) //磁盤緩存文件數,只在使用默認緩存有效
.diskCacheFileNameGenerator(new HashCodeFileNameGenerator()) // default //key生成器
.imageDownloader(new BaseImageDownloader(context)) // default //圖片下載器
.imageDecoder(new BaseImageDecoder()) // default //圖片解碼器
.defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default,這裡配置DisplayImageOptions
.writeDebugLogs() //打印調試日志
.build();
配置顯示圖片選項
我們可以給每一次顯示圖片配置一些選項,比如是否可以緩存,采樣大小等等。
// 不要把這些拷貝到你的項目中! 這裡僅僅是例舉出所有可用的選項,根據自身情況進行配置。。
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 僅在沒有配置loading占位圖時生效
.delayBeforeLoading(1000) //延時加載
.cacheInMemory(false) // default
.cacheOnDisk(false) // default
.preProcessor(...) //bitmap預處理
.postProcessor(...) //bitmap後處理
.extraForDownloader(...) //額外的下載器
.considerExifParams(false) // default //考慮旋轉參數
.imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2) // default 默認采樣方式
.bitmapConfig(Bitmap.Config.ARGB_8888) // default
.decodingOptions(...) //配置解碼的BitmapFactory.Options
.displayer(new SimpleBitmapDisplayer()) // default 配置顯示器
.handler(new Handler()) // default //配置Handler
.build();
加載圖片
ImageLoader.getInstance().displayImage(...)//顯示圖片
ImageLoader.getInstance().loadImage(...)//加載圖片
相信這個圖片加載框架是大家最熟悉而又最有疑問的。疑問如下:
這個框架會不會對本地圖片進行磁盤緩存? 內部是怎麼支持Drawable等其他類型的? 怎麼實現多尺寸和單尺寸緩存? 怎麼實現僅在wifi環境下加載圖片? 這個框架可以在ListView的復用中自動取消任務嗎? 怎麼針對ListView進行優化?我們知道在使用ImageLoader之前,必須進行配置,那麼我們就從ImageLoaderConfiguration
這個類入手,該類屬性如下:
public final class ImageLoaderConfiguration {
final Resources resources;//用來加載drawable圖片
//內存緩存最大寬高,默認為屏幕尺寸
final int maxImageWidthForMemoryCache;
final int maxImageHeightForMemoryCache;
//磁盤緩存最大寬高,默認為0,不做限制
final int maxImageWidthForDiskCache;
final int maxImageHeightForDiskCache;
//Bitmap處理器,用來處理原始bitmap,返回一個新bitmap
final BitmapProcessor processorForDiskCache;
final Executor taskExecutor;//線程池,默認3個線程
final Executor taskExecutorForCachedImages;//緩存圖片線程池,默認3個線程
//是否使用了自定義的線程池
final boolean customExecutor;
final boolean customExecutorForCachedImages;
//線程池數量、優先級、排隊類型(FIFO,LIFO)
final int threadPoolSize;
final int threadPriority;
final QueueProcessingType tasksProcessingType;
final MemoryCache memoryCache;//接口,內存緩存
final DiskCache diskCache;//接口,磁盤緩存
final ImageDownloader downloader;//圖片下載器,根據url下載成流
final ImageDecoder decoder;//圖片解碼器,用於將流解碼成bitmap
final DisplayImageOptions defaultDisplayImageOptions;//顯示配置
final ImageDownloader networkDeniedDownloader;//禁止網絡的下載器(只從本地圖片加載圖片,可以用來做只在wifi下加載圖片這個功能)
final ImageDownloader slowNetworkDownloader;//慢網絡的加載器
注釋寫的很詳細,這裡就不一一介紹了,我們知道構建者模式,需要使用build()
來初始化,那麼build()
又做了什麼?
public ImageLoaderConfiguration build() {
initEmptyFieldsWithDefaultValues();//初始化部分空值
return new ImageLoaderConfiguration(this);//賦值
}
可以看出,build()
會對一些空值進行初始化,然後在通過ImageLoaderConfiguration的構造方法來賦值參數。ImageLoaderConfiguration
的構造方法只是簡單的一些賦值操作,我們就不進去看了。現在來看看initEmptyFieldsWithDefaultValues
方法。
private void initEmptyFieldsWithDefaultValues() {
if (taskExecutor == null) {//初始化下載線程池
taskExecutor = DefaultConfigurationFactory
.createExecutor(threadPoolSize, threadPriority, tasksProcessingType);
} else {
customExecutor = true;
}
if (taskExecutorForCachedImages == null) {//初始化緩存線程池
taskExecutorForCachedImages = DefaultConfigurationFactory
.createExecutor(threadPoolSize, threadPriority, tasksProcessingType);
} else {
customExecutorForCachedImages = true;
}
if (diskCache == null) {//創建磁盤緩存
if (diskCacheFileNameGenerator == null) {
diskCacheFileNameGenerator = DefaultConfigurationFactory.createFileNameGenerator();
}
diskCache = DefaultConfigurationFactory
.createDiskCache(context, diskCacheFileNameGenerator, diskCacheSize, diskCacheFileCount);
}
if (memoryCache == null) {//創建內存緩存
memoryCache = DefaultConfigurationFactory.createMemoryCache(context, memoryCacheSize);
}
if (denyCacheImageMultipleSizesInMemory) {//創建單尺寸內存緩存(同一張圖片只緩存一種尺寸到內存中)
memoryCache = new FuzzyKeyMemoryCache(memoryCache, MemoryCacheUtils.createFuzzyKeyComparator());
}
if (downloader == null) {//創建下載器
downloader = DefaultConfigurationFactory.createImageDownloader(context);
}
if (decoder == null) {//創建解碼器
decoder = DefaultConfigurationFactory.createImageDecoder(writeLogs);
}
if (defaultDisplayImageOptions == null) {//創建默認的顯示配置
defaultDisplayImageOptions = DisplayImageOptions.createSimple();
}
}
}
根據隊列排隊策略,采用了不同的阻塞隊列來初始化線程池。此外,可以看出核心線程數和最大線程數是一樣的,在ImageLoader中默認開啟3個線程。
/** Creates default implementation of task executor */
public static Executor createExecutor(int threadPoolSize, int threadPriority,
QueueProcessingType tasksProcessingType) {
//隊列類型
boolean lifo = tasksProcessingType == QueueProcessingType.LIFO;
//隊列
BlockingQueue taskQueue =
lifo ? new LIFOLinkedBlockingDeque() : new LinkedBlockingQueue();
//線程池
return new ThreadPoolExecutor(threadPoolSize, threadPoolSize, 0L, TimeUnit.MILLISECONDS, taskQueue,
createThreadFactory(threadPriority, "uil-pool-"));
}
先來看下磁盤緩存,createReserveDiskCacheDir
可以看出根據是否設置了磁盤緩存大小用了不同的DiskCache。當設置了緩存大小時采用LruDiskCache,LruDiskCache會單獨新建一個名為uil-images的目錄用來存放,UnlimitedDiskCache用於不限制緩存大小的情況,直接緩存在根目錄下(當根目錄不可用時,才會選擇獨立目錄)。
public static DiskCache createDiskCache(Context context, FileNameGenerator diskCacheFileNameGenerator,
long diskCacheSize, int diskCacheFileCount) {
File reserveCacheDir = createReserveDiskCacheDir(context);//創建獨立緩存目錄
if (diskCacheSize > 0 || diskCacheFileCount > 0) {
//使用獨立的緩存目錄
File individualCacheDir = StorageUtils.getIndividualCacheDirectory(context);
try {
//如果定義了磁盤緩存大小,則返回一個LruDiskCache
return new LruDiskCache(individualCacheDir, reserveCacheDir, diskCacheFileNameGenerator, diskCacheSize,
diskCacheFileCount);
} catch (IOException e) {
L.e(e);
// continue and create unlimited cache
}
}
//獲取緩存根目錄
File cacheDir = StorageUtils.getCacheDirectory(context);
//如果沒有定義磁盤緩存大小,則返回一個UnlimitedDiskCache。將根目錄和獨立目錄都傳入
return new UnlimitedDiskCache(cacheDir, reserveCacheDir, diskCacheFileNameGenerator);
}
LruDiskCache
內部使用了DiskLruCache,DiskLruCache是JakeWharton開源的一個緩存庫,關於DiskLruCache的使用請自行查閱資料,這裡只需知道LruDiskCache
中使用了DiskLruCache來進行磁盤緩存。UnlimitedDiskCache
這個緩存類不用考慮磁盤緩存大小,這裡也不做介紹了。此外,ImageLoader中還提供了一個LimitedAgeDiskCache
可以指定緩存時間。
關於內存緩存比較簡單,如果可以多尺寸緩存使用了LruMemoryCache,否則使用FuzzyKeyMemoryCache。內存緩存都是使用LruCache實現的。這裡不做深究。
我們知道下載器是用來根據url來下載為InputStream。那麼具體是怎麼實現的呢?
public static ImageDownloader createImageDownloader(Context context) {
return new BaseImageDownloader(context);
}
內部返回了BaseImageDownloader
,BaseImageDownloader
的核心源碼如下:
@Override
public InputStream getStream(String imageUri, Object extra) throws IOException {
switch (Scheme.ofUri(imageUri)) {
case HTTP:
case HTTPS:
return getStreamFromNetwork(imageUri, extra);
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);
}
}
可以看出,根據不同類型使用了不同方法,看到這相信你已經明白該庫是怎麼支持Drawable等其他類型的了,如果你需要支持自定義的類型,只需要重寫getStreamFromOtherSource
即可。我們來看看其中兩種類型。
getStreamFromDrawable
將Drawable轉化為流
protected InputStream getStreamFromDrawable(String imageUri, Object extra) {
String drawableIdString = Scheme.DRAWABLE.crop(imageUri);//提取drawable://後的內容
int drawableId = Integer.parseInt(drawableIdString);//提取id
return context.getResources().openRawResource(drawableId);//轉為InputStream
}
getStreamFromNetwork
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
HttpURLConnection conn = createConnection(imageUri, extra);
//..
//省略了部分源碼
InputStream imageStream=conn.getInputStream();//獲取流
//..
//省略了部分源碼
return new ContentLengthInputStream(new BufferedInputStream(imageStream, BUFFER_SIZE), conn.getContentLength());//將InputStream包裝為ContentLengthInputStream後返回,可以獲取長度。
}
源碼的思路非常清晰,如果想要擴展的話也是比較簡單的。
DefaultConfigurationFactory.createImageDecoder(writeLogs)
內部同樣返回了一個BaseImageDecoder
,解碼器用來將InputStream解碼成Bitmap,我們來看看內部的核心源碼。
@Override
public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
Bitmap decodedBitmap;
ImageFileInfo imageInfo;//保存了圖片的大小和旋轉信息
InputStream imageStream = getImageStream(decodingInfo);//獲取輸入流
//..
//省略了部分源碼
imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);//從輸入流中獲取大小信息和旋轉信息保存起來,采用了inJustDecodeBounds
imageStream = resetStream(imageStream, decodingInfo);//由於流不能二次讀取,所有這裡進行重置
//根據獲取到的大小,生成一個BitmapFactory.Options
Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
//根據BitmapFactory.Options來解碼bitmap
decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
//..
//省略了部分源碼
if (decodedBitmap == null) {
L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
} else {
//如果bitmap不為空,現在對bitmap進行旋轉和翻轉操作(如果需要考慮旋轉因素)
decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
imageInfo.exif.flipHorizontal);
}
return decodedBitmap;
}
整個解碼流程是這樣的,首先從ImageDecodingInfo中獲取輸入流(ImageDecodingInfo內部保存了下載器,通過下載器下載成流),然後采用inJustDecodeBounds來讀取寬高和Exif信息。不同於BitmapFactory.decodeFile,InputStream不能二次讀取,必須重置,讀取到寬高信息後,通過prepareDecodingOptions來計算采樣率,然後解碼返回bitmap,最後對bitmap處理Exif旋轉信息。
ImageDecodingInfo
的源碼如下:
public class ImageDecodingInfo {
private final String imageKey;
private final String imageUri;
private final String originalImageUri;
private final ImageSize targetSize;
private final ImageScaleType imageScaleType;//圖片縮放類型,NONE(不縮放),NONE_SAFE(除非超出硬件加速的顯示范圍,否則不縮放),IN_SAMPLE_POWER_OF_2(2次冪縮放),IN_SAMPLE_INT(整數縮放),EXACTLY(縮放到至少寬高有一個等於目標值,原始圖片小於目標大小則不縮放),EXACTLY_STRETCHED(原始圖片小於目標大小仍然縮放)
private final ViewScaleType viewScaleType;//ImageView的縮放類型(被整理成兩類,FIT_INSIDE和CROP)
private final ImageDownloader downloader;//圖片下載器
private final Object extraForDownloader;//輔助下載器
private final boolean considerExifParams;//考慮旋轉參數
private final Options decodingOptions;//解碼的BitmapFactory.Options
public ImageDecodingInfo(String imageKey, String imageUri, String originalImageUri, ImageSize targetSize, ViewScaleType viewScaleType,
ImageDownloader downloader, DisplayImageOptions displayOptions) {
this.imageKey = imageKey;
this.imageUri = imageUri;
this.originalImageUri = originalImageUri;
this.targetSize = targetSize;
this.imageScaleType = displayOptions.getImageScaleType();
this.viewScaleType = viewScaleType;
this.downloader = downloader;
this.extraForDownloader = displayOptions.getExtraForDownloader();
considerExifParams = displayOptions.isConsiderExifParams();
decodingOptions = new Options();
copyOptions(displayOptions.getDecodingOptions(), decodingOptions);
}
ImageFileInfo
和ExifInfo
的源碼如下,可以看出使用了ImageSize來保存寬高,ExifInfo
中保存了旋轉角度以及是否水平翻轉等等。
讀取旋轉信息用了Android中的ExifInterface
api,由於只能從文件獲取Exif信息,所以在defineImageSizeAndRotation
中做了相關判斷。
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);//讀取旋轉信息。默認為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中。可以看出,使用了Matrix
進行旋轉縮放。
protected Bitmap considerExactScaleAndOrientatiton(Bitmap subsampledBitmap, ImageDecodingInfo decodingInfo,
int rotation, boolean flipHorizontal) {
Matrix m = new Matrix();
//獲取采樣錯放類型
ImageScaleType scaleType = decodingInfo.getImageScaleType();
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);
}
}
}
// Flip bitmap if need
if (flipHorizontal) {//水平翻轉
m.postScale(-1, 1);
}
//旋轉
if (rotation != 0) {
m.postRotate(rotation);
}
//創建了一個新bitmap返回
Bitmap finalBitmap = Bitmap.createBitmap(subsampledBitmap, 0, 0, subsampledBitmap.getWidth(), subsampledBitmap
.getHeight(), m, true);
if (finalBitmap != subsampledBitmap) {
subsampledBitmap.recycle();
}
return finalBitmap;
}
看到這裡,我們明白了,uri通過下載器下載成InputStream,然後解碼器讀取圖片的寬高和旋轉信息,采樣InputStream解碼成bitmap,最後處理了旋轉信息並返回。
在初始化配置中使用了createSimple
來創建了一個默認顯示選項。
if (defaultDisplayImageOptions == null) {//創建默認的顯示配置
defaultDisplayImageOptions = DisplayImageOptions.createSimple();
}
關於DisplayImageOptions,下一小節會詳細介紹,createSimple
只是直接調用了build
用了默認值而已。
DisplayImageOptions
同樣也使用了構建者模式,按照老規矩,先來看看該類的屬性。
public final class DisplayImageOptions {
//=============各種占位圖 START===============
private final int imageResOnLoading;
private final int imageResForEmptyUri;
private final int imageResOnFail;
private final Drawable imageOnLoading;
private final Drawable imageForEmptyUri;
private final Drawable imageOnFail;
//=============各種占位圖 END===============
private final boolean resetViewBeforeLoading;//加載前重置
private final boolean cacheInMemory;//內存緩存?
private final boolean cacheOnDisk;//磁盤緩存?
private final ImageScaleType imageScaleType;//采樣縮放類型
private final Options decodingOptions;//解碼時的BitmapFactory.Options
private final int delayBeforeLoading;//延時加載
private final boolean considerExifParams;//考慮旋轉參數
private final Object extraForDownloader;//輔助的下載器
//bitmap處理器接口,用來處理原始bitmap,返回一個新bitmap
private final BitmapProcessor preProcessor;//預處理(磁盤中加載出來,放入內存之前)
private final BitmapProcessor postProcessor;//後處理(顯示之前)
private final BitmapDisplayer displayer;//圖片顯示器
private final Handler handler;//用於切換線程
private final boolean isSyncLoading;//是否異步加載
我們知道構建者模式一般通過build
來初始化,那我們來看看一些默認值。
可以看出,默認沒有采用任何緩存策略。縮放類型采用了二次冪采樣。
默認的BitmapDisplayer如下:
/** Creates default implementation of {@link BitmapDisplayer} - {@link SimpleBitmapDisplayer} */
public static BitmapDisplayer createBitmapDisplayer() {
return new SimpleBitmapDisplayer();
}
可以看出內部采用了SimpleBitmapDisplayer
public final class SimpleBitmapDisplayer implements BitmapDisplayer {
@Override
public void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom) {
imageAware.setImageBitmap(bitmap);
}
}
ImageAware保存View的寬高、View的哈希值標識以及View本身等信息,主要用來將圖像設置到控件中。
LoadedFrom是一個枚舉類,用來標識從內存、磁盤、網絡中加載。
此外,還有FadeInBitmapDisplayer、RoundedBitmapDisplayer、CircleBitmapDisplayer等等。
CircleBitmapDisplayer的源碼如下,可以看出唯一不同的是加載了CircleDrawable(自定義的Drawable類,使用BitmapShader來切圓),只要你喜歡,你可以自定義出各種各樣形狀的顯示器。
@Override
public void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom) {
if (!(imageAware instanceof ImageViewAware)) {
throw new IllegalArgumentException("ImageAware should wrap ImageView. ImageViewAware is expected.");
}
imageAware.setImageDrawable(new CircleDrawable(bitmap, strokeColor, strokeWidth));
}
用ImageAware包裝的好處在於內部使用了弱引用,可以避免內存洩漏。
終於講到正題了——加載/顯示圖片,我們來看看ImageLoader是怎麼將下載器、解碼器、顯示器等結合起來了的吧。在分析之前,來認識一下ImageLoader這個類中的屬性。
出乎意料的簡潔,getInstance
采用了單例模式。ImageLoadingListener加載監聽大家應該很清楚,這裡不做贅述。ImageLoaderConfiguration也已經介紹過了。但是ImageLoaderEngine這個是什麼鬼呢?
大家還記得 ImageLoader.getInstance().init(config);//初始化
這一句嗎?沒錯,將ImageLoaderConfiguration傳入了進去。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;">
*/
public synchronized void init(ImageLoaderConfiguration configuration) {
if (configuration == null) {
throw new IllegalArgumentException(ERROR_INIT_CONFIG_WITH_NULL);
}
if (this.configuration == null) {
L.d(LOG_INIT_CONFIG);
engine = new ImageLoaderEngine(configuration);//用ImageLoaderEngine包裝了起來
this.configuration = configuration;//同時也賦值給configuration一份
} else {
L.w(WARNING_RE_INIT_CONFIG);
}
}
可以看出ImageLoaderEngine用來包裝了ImageLoaderConfiguration。那麼ImageLoaderEngine到底是來干嘛的?既然取名為ImageLoader引擎,可以想象到其核心地位。ImageLoaderEngine主要負責執行加載和顯示圖片等任務的引擎(LoadAndDisplayImageTask,ProcessAndDisplayImageTask)。
該類屬性如下。
class ImageLoaderEngine {
final ImageLoaderConfiguration configuration;//配置
private Executor taskExecutor;//任務執行者(下載圖片的線程池)
private Executor taskExecutorForCachedImages;//處理緩存的線程池
private Executor taskDistributor;//任務分配者(由它來控制把任務往哪個線程池提交)
private final Map cacheKeysForImageAwares = Collections
.synchronizedMap(new HashMap());//key為View的哈希值,value為請求的網址(後面會追加寬高)
private final Map uriLocks = new WeakHashMap();//uri鎖map
private final AtomicBoolean paused = new AtomicBoolean(false);
private final AtomicBoolean networkDenied = new AtomicBoolean(false);
private final AtomicBoolean slowNetwork = new AtomicBoolean(false);
private final Object pauseLock = new Object();//暫停鎖
ImageLoaderEngine(ImageLoaderConfiguration configuration) {
this.configuration = configuration;
taskExecutor = configuration.taskExecutor;
taskExecutorForCachedImages = configuration.taskExecutorForCachedImages;
taskDistributor = DefaultConfigurationFactory.createTaskDistributor();
}
//..
//省略部分源碼
}
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();
if (isImageCachedOnDisk) {
//如果磁盤存在就提交到緩存線程池
taskExecutorForCachedImages.execute(task);
} else {
//提交到下載線程池
taskExecutor.execute(task);
}
}
});
}
/** Submits task to execution pool */
//ProcessAndDisplayImageTask提交到緩存線程池
void submit(ProcessAndDisplayImageTask task) {
initExecutorsIfNeed();
taskExecutorForCachedImages.execute(task);
}
現在再來看看平時用的最多的displayImage吧。
public void displayImage(String uri, ImageView imageView) {
//用ImageViewAware包裝ImageView
displayImage(uri, new ImageViewAware(imageView), null, null, null);
}
可以看出,用ImageViewAware包裝了ImageView,displayImage最終調用的重載方法如下
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
checkConfiguration();//檢查ImageLoaderConfiguration有沒有初始化。
if (imageAware == null) {//ImageAware不可為空
throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
}
if (listener == null) {//加載監聽
listener = defaultListener;
}
if (options == null) {//顯示選項
options = configuration.defaultDisplayImageOptions;
}
//=================如果是個空url直接設置占位圖 START====
if (TextUtils.isEmpty(uri)) {
engine.cancelDisplayTaskFor(imageAware);//引擎取消顯示任務(從map中移除)
listener.onLoadingStarted(uri, imageAware.getWrappedView());//加載開始監聽
if (options.shouldShowImageForEmptyUri()) {//顯示占位圖
imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
} else {
imageAware.setImageDrawable(null);
}
listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);//加載完成監聽
return;//返回
}
//=================如果是個空url直接設置占位圖 END====
if (targetSize == null) {//如果沒有定義顯示目標大小,就根據ImageView自動獲取
targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware,configuration.getMaxImageSize());
}
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);//生成內存緩存的key(`[imageUri]_[width]x[height]`的形式)
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);//引擎准備顯示任務(放入map中)
listener.onLoadingStarted(uri, imageAware.getWrappedView());//加載開始監聽
//=================從內存中取 START====
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);//從內存中取
if (bmp != null && !bmp.isRecycled()) {//如果內存中取到
L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
if (options.shouldPostProcess()) {//是否需要後處理?
//engine.getLockForUri(uri),獲取當前url的鎖
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
//ProcessAndDisplayImageTask是一個Runable對象,處理再顯示
ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {//如果是同步加載,則直接執行Runable中的run()方法
displayTask.run();
} else {
engine.submit(displayTask);//異步加載,直接使用引擎提交到線程池中
}
} else {
//如果不需要後處理bitmap,直接獲取BitmapDisplayer進行顯示
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);//加載完成監聽
}
} else {
//=================從內存中取 END====
//=================從磁盤/網絡中取 START====
//內存中沒有
if (options.shouldShowImageOnLoading()) {//設置占位圖
imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
} else if (options.isResetViewBeforeLoading()) {
imageAware.setImageDrawable(null);
}
//engine.getLockForUri(uri),獲取當前url的鎖
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
//LoadAndDisplayImageTask也是一個Runable對象,加載然後顯示
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {//如果是同步,直接執行run
displayTask.run();
} else {
engine.submit(displayTask);//否則通過引擎提交到線程池中
}
}
//=================從磁盤/網絡中取 END====
}
源碼有點長,我們慢慢來。引擎取消任務和准備任務的源碼如下。
void cancelDisplayTaskFor(ImageAware imageAware) {
cacheKeysForImageAwares.remove(imageAware.getId());//從map中移除
}
void prepareDisplayTaskFor(ImageAware imageAware, String memoryCacheKey) {
//key為View的hashcode,value為請求url(加上寬高)
//保存到map中
cacheKeysForImageAwares.put(imageAware.getId(), memoryCacheKey);
}
ImageLoadingInfo用於保存圖片加載時所需要的信息
final class ImageLoadingInfo {
final String uri;//原始的url
final String memoryCacheKey; //加上寬高的url
final ImageAware imageAware;
final ImageSize targetSize;
final DisplayImageOptions options;
final ImageLoadingListener listener;
final ImageLoadingProgressListener progressListener;
final ReentrantLock loadFromUriLock; //uri鎖
//構造方法中會傳入url鎖
public ImageLoadingInfo(String uri, ImageAware imageAware, ImageSize targetSize, String memoryCacheKey,
DisplayImageOptions options, ImageLoadingListener listener,
ImageLoadingProgressListener progressListener, ReentrantLock loadFromUriLock) {
this.uri = uri;
this.imageAware = imageAware;
this.targetSize = targetSize;
this.options = options;
this.listener = listener;
this.progressListener = progressListener;
this.loadFromUriLock = loadFromUriLock;
this.memoryCacheKey = memoryCacheKey;
}
}
如果內存緩存中存在bitmap,此時應該使用ProcessAndDisplayImageTask,ProcessAndDisplayImageTask是一個Runable對象,從名字可以看出,這個任務主要處理bitmap然後進行顯示。run
方法如下:
@Override
public void run() {
//獲取後處理器
BitmapProcessor processor = imageLoadingInfo.options.getPostProcessor();
//處理bitmap
Bitmap processedBitmap = processor.process(bitmap);
//將新bitmap給DisplayBitmapTask,DisplayBitmapTask是一個用來顯示的Runable
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(processedBitmap, imageLoadingInfo, engine,
LoadedFrom.MEMORY_CACHE);
//然後調用LoadAndDisplayImageTask來執行任務
LoadAndDisplayImageTask.runTask(displayBitmapTask, imageLoadingInfo.options.isSyncLoading(), handler, engine);
}
如果是異步,我們就需要通過引擎把ProcessAndDisplayImageTask提交到線程池中。
void submit(ProcessAndDisplayImageTask task) {
initExecutorsIfNeed();
taskExecutorForCachedImages.execute(task);//提交到執行緩存的線程池中
}
如果內存中沒有讀到bitmap,此時應該使用LoadAndDisplayImageTask來加載bitmap,LoadAndDisplayImageTask也是一個Runable對象,run
方法如下:
@Override
public void run() {
if (waitIfPaused()) return;//如果暫停了就掛起等待
if (delayIfNeed()) return; //如果延時就休眠等待
ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;//獲取url鎖
L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);
if (loadFromUriLock.isLocked()) {
L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);
}
loadFromUriLock.lock(); //獲取鎖
Bitmap bmp;
try {
checkTaskNotActual();//判讀View是否被GC回收或者被復用,如果是就拋出異常
//再次從內存取(為什麼再次取呢?因為有可能之前有個取的時候,已經有個任務提交到後台,現在正好加載完。)
bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp == null || bmp.isRecycled()) {
bmp = tryLoadBitmap();//如果內存中真的沒有,就去磁盤/網絡中取
if (bmp == null) return; // listener callback already was fired
checkTaskNotActual();//判讀View是否被GC回收或者被復用,如果是就拋出異常
checkTaskInterrupted();//判讀線程是否被中斷,如果是就拋出異常
if (options.shouldPreProcess()) {//是否預處理?
L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
bmp = options.getPreProcessor().process(bmp);//預處理
if (bmp == null) {
L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey);
}
}
//預處理完畢後,如果允許內存緩存,就放入內存中
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);
}
//是否需要後處理?(之前直接從內存中取也詢問了是否後處理,忘記的回頭看一下源碼)
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();//判讀View是否被GC回收或者被復用,如果是就拋出異常
checkTaskInterrupted();//判讀線程是否被中斷,如果是就拋出異常
} catch (TaskCancelledException e) {
fireCancelEvent();//這裡捕獲異常,然後回調取消監聽
return;
} finally {
loadFromUriLock.unlock();//釋放鎖
}
//顯示Bitmap的任務
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
//執行runtask。
runTask(displayBitmapTask, syncLoading, handler, engine);
}
我們先不看tryLoadBitmap
,只需知道tryLoadBitmap
是從磁盤或者網絡中讀取圖片即可。現在來看看DisplayBitmapTask中的run方法如下:
@Override
public void run() {
if (imageAware.isCollected()) {//是否被回收?
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else if (isViewWasReused()) {//是否被重用?
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else {
//最後才是調用displayer來顯示
displayer.display(bitmap, imageAware, loadedFrom);//顯示bitmap
engine.cancelDisplayTaskFor(imageAware);//取消任務
listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);//監聽
}
}
LoadAndDisplayImageTask中的runTask
源碼如下:
static void runTask(Runnable r, boolean sync, Handler handler, ImageLoaderEngine engine) {
if (sync) {
r.run();//同步就直接執行run
} else if (handler == null) {
engine.fireCallback(r);//如果Handler為空,就提交到另起線程執行
} else {
handler.post(r);//使用handler切換到主線程
}
}
看完上面,應該已經知道了怎麼切換線程去顯示圖片的吧。
現在再來看看tryLoadBitmap
相關源碼,ImageLoader是怎麼從磁盤或者網絡中加載圖片的呢?
private Bitmap tryLoadBitmap() throws TaskCancelledException {
Bitmap bitmap = null;
try {
//首先從磁盤中讀取
File imageFile = configuration.diskCache.get(uri);
if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
//如果磁盤中有
L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
loadedFrom = LoadedFrom.DISC_CACHE;
checkTaskNotActual();//View是否被回收,是否被重用,是就拋出異常?
//解碼成bitmap
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;//url
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
//可以磁盤緩存時就使用tryCacheImageOnDisk()下載到磁盤
imageFile = configuration.diskCache.get(uri);//然後再從磁盤讀
if (imageFile != null) {
//只要保存成功,url將被替換成file://類型
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
}
}
checkTaskNotActual();//View是否被回收,是否被重用?
bitmap = decodeImage(imageUriForDecoding);//根據url解碼(如果是從磁盤中讀的,全部為file://開頭)
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
fireFailEvent(FailType.DECODING_ERROR, null);//回調失敗事件
}
}
} 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;
}
tryCacheImageOnDisk
從磁盤中加載圖片,其實內部的核心源碼就是downloadImage()
,如果指定了磁盤最大緩存尺寸,還會進行重新調整下Bitmap大小。
private boolean tryCacheImageOnDisk() throws TaskCancelledException {
L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey);
boolean loaded;
try {
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;
}
downloadImage()
的相關源碼如下
private boolean downloadImage() throws IOException {
//下載成InputStream
//getDownloader()會根據設置獲取三種類型的下載器(基本的、禁止網絡的、慢網絡的)
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);
}
}
}
在成功下載到磁盤之後,下一步就該進行解碼了。就是執行decodeImage
這個方法:
private Bitmap decodeImage(String imageUri) throws IOException {
ViewScaleType viewScaleType = imageAware.getScaleType();//獲取View的縮放類型
//將uri,緩存key,下載器全部封裝成ImageDecodingInfo。
ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
getDownloader(), options);
return decoder.decode(decodingInfo);//調用解碼器進行解碼
}
介紹完displayImage後,再來看一下它的兄弟方法loadImage。可以看出內部也是調用了displayImage,只不過用了NonViewAware來包裝。
public void loadImage(String uri, ImageSize targetImageSize, DisplayImageOptions options,
ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
checkConfiguration();
if (targetImageSize == null) {
targetImageSize = configuration.getMaxImageSize();
}
if (options == null) {
options = configuration.defaultDisplayImageOptions;
}
//使用NonViewAware來包裝
NonViewAware imageAware = new NonViewAware(uri, targetImageSize, ViewScaleType.CROP);
//最終也是調用了displayImage
displayImage(uri, imageAware, options, listener, progressListener);
}
那麼NonViewAware跟ImageViewAware有什麼區別呢?
@Override
public boolean setImageDrawable(Drawable drawable) { // Do nothing
return true;
}
@Override
public boolean setImageBitmap(Bitmap bitmap) { // Do nothing
return true;
}
可以看出,setImageDrawable和setImageBitmap不做任何事,其他方面和displayImage沒有半毛錢區別。
整個加載和顯示的流程如下圖所示:
首先通過下載器下載圖片,然後緩存到磁盤一份(可選),接著通過解碼器將流解碼成bitmap,放入內存之前先對bitmap進行預處理(可選),然後放入內存(可選),在顯示之前對bitmap進行處理(可選),最後調用顯示器來進行顯示圖片。
這個框架會不會對本地圖片進行磁盤緩存?
從源碼可以看出,只要你允許磁盤緩存,任何流到會寫入到磁盤內,包括本地圖片及Drawable圖片。
ImageLoader是怎麼實現多尺寸緩存的?那麼怎麼禁止多尺寸緩存?
多尺寸緩存的核心在於緩存key的格式為[imageUri]_[width]x[height]
,這樣每種尺寸一個key,然後放入內存中。那麼ImageLoader怎麼禁止多尺寸緩存呢?
很簡單,只需配置denyCacheImageMultipleSizesInMemory
即可,那麼在存放bitmap時會截取url進行遍歷比較,如果存在,就移除舊圖片。
怎麼實現僅在wifi環境下加載圖片?
很簡單,下面一句代碼就行。這樣在getDownloader()
就會返回禁止加載網絡圖片的下載器。
ImageLoader.getInstance().denyNetworkDownloads(true);
NetworkDeniedImageDownloader的相關源碼如下。
這個框架可以在ListView的復用中自動取消任務嗎?
從源碼角度來看是可以的。許多地方都加入了checkTaskNotActual()
來檢查View是否被回收或者復用。
具體判斷的源碼讀者自行閱讀即可。
怎麼針對ListView進行優化?
針對ListView添加監聽即可。
listView.setOnScrollListener(new PauseOnScrollListener(...));
PauseOnScrollListener的核心源碼如下。可以看出滾動時會停止加載圖片。
怎麼針對生命周期優化?
在生命周期的相關代碼中加入如下代碼即可。
ImageLoader.getInstance().resume();
ImageLoader.getInstance().pause();
該開源庫地址:https://github.com/nostra13/Android-Universal-Image-Loader
本期解讀到此結束,如有錯誤之處,歡迎指出。
1.TextView Textview在之前的學習中用到過好多次,就不再貼代碼了,在第四天學到的新知識是對齊方式,在activity_main中創建TextVi
相關源碼framework/base/core/java/andorid/os/Handler.javaframework/base/core/java/andorid/
Android下拉刷新庫,利用viewdraghelper實現。集成了下拉刷新,底部加載更多,以及剛進入加載數據的loadview。包括了listview與g
一 完善部分的QQ音樂效果圖二 需要完善點1 歌曲的切換和暫停播放2 歌曲當前播放時間和歌曲總時間的更新3 進度條的處理4 歌手頭像處理5 頭像動畫效果6 歌詞的進度顯示