Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android-Universal-Imageloader源碼完全解析

Android-Universal-Imageloader源碼完全解析

編輯:關於Android編程

現在網上對此Imageloader圖片加載的開源框架的解析有好多文章,有好多只是簡單分析它的實現,此篇文章是通過自己對其源碼的分析,對它的實現方式進行分析,針對它用到的重點知識點進行重點介紹,以及自己對於此框架的理解。下面的分析從以下兩個方面進行分析。

Imageloader的初始化

Imageloader加載圖片的實現方式分析

1.Imageloader的初始化

Imageloader是在Application的onCreate()方法中進行初始化的,在官方的demo 中的初始化方式如下:

 

ImageLoaderConfiguration.Builder config = new ImageLoaderConfiguration.Builder(context);
config.threadPriority(Thread.NORM_PRIORITY - 2);
config.denyCacheImageMultipleSizesInMemory();
config.diskCacheFileNameGenerator(new Md5FileNameGenerator());
config.diskCacheSize(50 * 1024 * 1024); // 50 MiB
config.tasksProcessingOrder(QueueProcessingType.LIFO);
config.writeDebugLogs(); // Remove for release app

// Initialize ImageLoader with configuration.
ImageLoader.getInstance().init(config.build());
從上面我們能看出,官方的demo對指定的配置選項進行了配置,當然,沒有設置的選項,系統也會采用默認的方式進行實現。可以通過config.build()方法中實現的,會調用initEmptyFieldsWithDefaultValues()方法會將那些必要的並且沒有設置的配置參數進行初始化,比如downloader(執行下載的對象),decoder(執行解碼的對象)等等,如果想了解,可以自己查看更多默認參數,下面分析在加載的圖片的流程中,用到這些對象會對其進行詳細的分析。

 

2.Imageloader加載圖片的實現方式分析

本文主要對displayImage()方法進行分析,其實其他方式原理都一樣,就不過多描述了。實現看displayImage()參數列表如下:

 

public void displayImage(String uri, ImageView imageView, DisplayImageOptions options,
			ImageLoadingListener listener, ImageLoadingProgressListener progressListener)
其實其中,需要了解的也就是參數三的DisplayImageOptions了,這是對顯示的圖片的配置信息,我們來看官方demo的實現方式如下:

 

 

options = new DisplayImageOptions.Builder()
		.showImageOnLoading(R.drawable.ic_stub)
		.showImageForEmptyUri(R.drawable.ic_empty)
		.showImageOnFail(R.drawable.ic_error)
		.cacheInMemory(true)
		.cacheOnDisk(true)
		.considerExifParams(true)
		.displayer(new CircleBitmapDisplayer(Color.WHITE, 5))
		.build();
其中重點需要說一下的就是設置displayer(),這裡實現的是顯示圓形圖片,系統還提供了其他幾種Displayer,都是繼承Bitmap Displayer進行擴展實現的。內部的實現是通過Drawable實現,而不是對ImageView進行操作了,這樣性能會更好。實現原理就是將Bitmap畫到Drawable,然後直接將Drawable設置給ImageVIew即可。我記得好像之前看到鴻陽有一篇文件就是寫的這方面,想要了解的可以看一下。這裡代碼的具體實現:
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));
}
內部有一個CircleDrawable的靜態內部淚,它繼承自Drawable,具體實現,以及原理,大家可以自己查看,以及查閱相關資料,這個不是這裡的重點。

 

 

繼續上面displayImage()的分析,其他最後它真正調用的方法是:

 

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
			ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {

相信,大家看完這個參數列表,都會好奇,為什麼原來的ImageVIew在這裡變成了ImageAware,這個究竟是什麼,其實它是這個框架一個值得稱贊的地方,它其實是將ImageView包裹起來,並且是用弱引用包裹起來的。為什麼這樣呢,這樣的好處又是什麼呢。首先要先明白弱引用在這裡的作用:為了防止由於ImageView被當前Imageloader引用著,造成其他引用它的地方內存也無法釋放掉,造成內存洩漏(例如一個場景:ImageView所在的父view已經沒用,可以被銷毀掉了,但是由於,Imageloader的線程還在下載等待將圖片設置給ImageVIew,所以此ImageVIew就被Imageloader引用著,釋放不掉,同時造成引用它的父view也釋放不掉。),關於弱引用的原理和作用,這裡就不做介紹,大家可以查閱相關資料。

 

下面分析其內部代碼的實現(只截取重要部分):

 

if (targetSize == null) {
	//根據 ImageView的寬高 和  設置的最大圖片寬高 計算出 目標圖片的尺寸(如果圖片寬(高)等於0,那麼會取configuration中的設置的最大圖片的尺寸(默認去屏幕尺寸))
	targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
}
//根據 uri 和  圖片尺寸計算出 緩存的key
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);//存儲起來(Map存儲,key:imageView的hashcode;value:計算出來的 cachekey)

listener.onLoadingStarted(uri, imageAware.getWrappedView());//回調函數的回調

//第一層緩存(內存緩存)
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);//普通的基於lru算法的緩存(使用LinkedHashMap實現)
if (bmp != null && !bmp.isRecycled()) {//緩存中有,就直接取出來,不去通過網絡獲取了
	L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);

	if (options.shouldPostProcess()) {//是否設置了BitmapProcessor(Bitmap處理器,對 Bitmap獲取到的Bitmap做相應的處理)
		ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
				options, listener, progressListener, engine.getLockForUri(uri));
		ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
				defineHandler(options));//這是一個Runnable任務,run主體方法中,會調用Options中設置的BitmapProcessor處理圖片,然後加入到engine的線程池中執行對應顯示圖片的任務
		if (options.isSyncLoading()) {//同步執行,直接調用
			displayTask.run();
		} else {//異步執行,添加到線程池中,等待異步執行
			engine.submit(displayTask);
		}
	} else {
		options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);//根據設置Displayer直接為ImageView設置Drawable(根據要求,通過Bitmap創建對應的Drawable)
		listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);//回調方法的調用
	}
} else {// 內存緩存沒有,走 第二層磁盤緩存
	if (options.shouldShowImageOnLoading()) {//設置正在加載的圖片
		imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
	} else if (options.isResetViewBeforeLoading()) {//如果設置加載之前,重置圖片,那麼就將ImageView的 圖像設置為null
		imageAware.setImageDrawable(null);
	}

	ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
			options, listener, progressListener, engine.getLockForUri(uri));//最後一個參數:創建url對應的線程鎖
	LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
			defineHandler(options));//創建下載任務
	if (options.isSyncLoading()) {//同步
		displayTask.run();//直接調用 Runable的 run()方法
	} else {//異步執行(默認)(運行在子線程中,將其添加到線程池中)
		engine.submit(displayTask);
	}
}
代碼都已經加上注釋,其實它也是采用的兩級級緩存,通過之前的配置選項也可以看出來,內存,本地磁盤這兩級緩存來優化體驗。每個代碼的內部具體實現,自己查閱內部代碼實現吧,我就不詳細分析其內部實現,下面重點分析LoadAndDisplayImageTask這個任務中的具體實現,其實它內部就是先從磁盤緩存中查找是否含有對應的緩存,有的話直接取出來,進行處理解碼設置給ImageView,否者調用對應的downloader去下載對應的資源。主要看它run()方法的實現:

 

 

loadFromUriLock.lock();//線程安全
Bitmap bmp;
try {
	checkTaskNotActual();//判斷當前ImageView是否回首掉了,是否正在被其他線程使用

	bmp = configuration.memoryCache.get(memoryCacheKey);//從緩存中獲取數據
	if (bmp == null || bmp.isRecycled()) {//沒有從緩存中獲取數據,或者bitmap已經回收掉了
		bmp = tryLoadBitmap();//先從緩存中獲取數據,如果沒有,再從網絡下載,然後存儲在到本地(如果設置緩存在本地)
		if (bmp == null) return; // listener callback already was fired

		checkTaskNotActual();
		checkTaskInterrupted();

		if (options.shouldPreProcess()) {
			L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
			bmp = options.getPreProcessor().process(bmp);//提前處理器,對Bitmap進行相應的處理(這個需要自己配置,並且需要自己實現對應的PreProcessor)
			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);//如果配置,執行對Bitmap的處理操作
		if (bmp == null) {
			L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey);
		}
	}
	checkTaskNotActual();
	checkTaskInterrupted();
} catch (TaskCancelledException e) {
	fireCancelEvent();
	return;
} finally {
	loadFromUriLock.unlock();
}

DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);//為Imageview設置圖片的task,內部是調用displayer實現的
runTask(displayBitmapTask, syncLoading, handler, engine);//如果是異步操作,並且handler不為null,用handler執行此task,也就是設置圖片的操作要運行在主線程中
下面主要對tryLoadBitmap()方法內部進行分析,其內部會先從內存中獲取圖片,如果有處理之後返回,如果沒有,使用downloader去下載圖片。

 

 

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();
	bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));//decodeImage()操作:讀取數據流,然後調整圖片的應該縮小的大小(使用BitmapFactory.Options實現,
	// 裡面使用BufferInputStream來存儲數據流,重復使用)
}
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
	L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey);
	loadedFrom = LoadedFrom.NETWORK;

	String imageUriForDecoding = uri;
	//第一個判斷:是否緩存到本地,只有第一個判斷為true,才會執行第二個判斷方法
	if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {//tryCacheImageOnDisk()下載圖片,並緩存本地
		imageFile = configuration.diskCache.get(uri);
		if (imageFile != null) {
			imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
		}
	}

	checkTaskNotActual();
	//如果沒有設置緩存本地,那麼執行下面的方法的時候,imageUriForDecoding 還是 http,還是會調用 Downloader的 getStream()方法,
	//此方法會判斷uri是什麼開頭的,可以處理http,file,content,assets等
	bitmap = decodeImage(imageUriForDecoding);

	if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
		fireFailEvent(FailType.DECODING_ERROR, null);
	}
}
首先對decodeImage()方法進行分析,此方法就是調用decoder的decode()方法進行實現,所以直接看官方demo使用的BaseImageDecoder的decode()方法的實現:

 

 

InputStream imageStream = getImageStream(decodingInfo);//如果沒有下載,下載圖片,否則獲取到緩存到本地的file的流
if (imageStream == null) {
	L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey());
	return null;
}
try {
	imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);//創建一個file info(計算出圖片流的圖片的實際尺寸)
	//這裡很重要;因為上面已經decodeStream()已經將上面的流給讀取了,當前流指定的位置已經改變了,所以下面的流要重置一下(兩種方式,如果支持mark(),那麼就reset()之前的位置)
	//如果不支持,那麼就重新下載這個流
	imageStream = resetStream(imageStream, decodingInfo);//重置圖片流,如果圖片流可以重置到之前讀取的位置,那麼就reset(),如果不能,就重新下載這個流
	//創建縮放的比例,然後賦值創建Options返回
	Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);//參數1:圖片流的大小,
	decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);//根據流創建對應的Bitmap(如果這裡的圖片源太大,此方法會報異常,親測結果)
} 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;
有兩個地方需要注意一下,第一個:getImageStream()獲取的輸入流是緩沖輸入流,支持markSupport()和reset()和mark()方法,如果一個流都到末尾之後,可以調用reset()將讀取位置重置到頭部,這個流就可以繼續讀取數據。在BitmapFactory.decodeStream()第一次獲取圖片的尺寸的時候,已經將流讀到末尾了;第二次BitmapFactory.decodeStream()的之前必須調用這個輸入流的reset()方法重置這個流,才能繼續使用這個流。第二個:BitmapFactory.decodeStream()方法報異常,由於圖片太大,沒找到解決方法(希望哪位大神了解的,告知小弟)。

 

下面分析,磁盤緩存沒有對應的緩存的時候,執行的方法:tryCacheImageOnDisk().

 

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);// 調整圖片大小,並將調整後的圖片的Bitmap緩存在本地緩存中// TODO : process boolean result
	}
}
首先看downloadImage()方法的實現:
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);//釋放數據流
	}
}
緊接著看getDownloader.getStream()的實現,官方demo采用的是BaseImageDownloader 的 getStream()的實現:

 

 

switch (Scheme.ofUri(imageUri)) {
	case HTTP:
	case HTTPS://網絡獲取
		return getStreamFromNetwork(imageUri, extra);
	case FILE://本地文件中獲取
		return getStreamFromFile(imageUri, extra);
	case CONTENT://ContentProvider
		return getStreamFromContent(imageUri, extra);
	case ASSETS://assets文件夾中獲取
		return getStreamFromAssets(imageUri, extra);
	case DRAWABLE://從資源文件中獲取
		return getStreamFromDrawable(imageUri, extra);
	case UNKNOWN:
	default:
		return getStreamFromOtherSource(imageUri, extra);
}
主要看從網絡獲取圖片實現:

 

 

		HttpURLConnection conn = createConnection(imageUri, extra);

		int redirectCount = 0;
		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)
			IoUtils.readAndCloseStream(conn.getErrorStream());
			throw e;
		}
		if (!shouldBeProcessed(conn)) {//狀態碼!=200
			IoUtils.closeSilently(imageStream);
			throw new IOException("Image request failed with response code " + conn.getResponseCode());
		}
		//注意它將外層包裹了 BufferedInputStream(使用緩沖輸入流,它可以markSupport()返回true,支持mark()和reset()操作,可以重復讀取流數據)
		//解決問題:在BitmapFactory.decode()執行之後,讀取的位置到達了流的結尾,如果是這個對象,就可以調用reset()方法之後繼續使用這個流
		return new ContentLengthInputStream(new BufferedInputStream(imageStream, BUFFER_SIZE), conn.getContentLength());
下面看resizeAndSaveImage()方法的實現,內部調整存儲在本地的圖片流的大小,生成對應的Bitmap存儲到磁盤緩存中。

 

 

		File targetFile = configuration.diskCache.get(uri);
		if (targetFile != null && targetFile.exists()) {
			ImageSize targetImageSize = new ImageSize(maxWidth, maxHeight);
			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) {
				saved = configuration.diskCache.save(uri, bmp);//將處理完的Bitmap存儲到本地緩存
				bmp.recycle();
			}
		}


全部分析到此結束,如果哪裡有錯誤,歡迎糾正。其實關於progressListener的調用沒有分析到,其實在調用diskCache的save()方法將從網絡獲取的輸入流寫入到本地的輸出流中,這個過程是下載圖片真正運行的過程,並且save()方法會將LoadAndDisplayImage這個類的this對象傳遞進去,這個類實現了IoUtils.CopyListener接口,在save()將輸入流寫入到輸出流的過程會調用onByteCopies()回調方法,此方法會調用progressListener。
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved