從源碼角度分析imageLoader框架

本文來自http://blog.csdn.net/andywuchuanlong,轉載請說明出處

對於圖片的加載和處理基本上是Android應用軟件項目中的常客,很多初學者在遇到圖片加載這個問題是,總是喜歡自己寫一個http請求,然後使用將流轉換成bitmap,從而顯示在項目的view中。其實對於圖片的處理自己寫固然是好,但是要想軟件穩定的運行,裏面還是需要很多細節東西需要處理的。在github上有很多的開源項目,處理圖片的也不少,下面介紹一下imageLoader這個開源框架。

    接觸Imageloader這個框架已經很久了,在項目總也使用過,但是僅僅是使用,作爲一個開發人員而言,雖然不提倡重複的造輪子,但是對於利用現有的輪子我們應該還是要知其所以然,才能將這個輪子的製造工藝轉成我麼自己的技術。廢話不多說,我們一起從源碼的角度來認識imageLoader。
    在使用imageLoader加載圖片之前,我們必須要先初始化一個loader:
<span style="white-space:pre">	</span>public static ImageLoader getInstance() {
		if (instance == null) {
			synchronized (ImageLoader.class) {
				if (instance == null) {
					instance = new ImageLoader();
				}
			}
		}
		return instance;
	}
ImageLoader是一個單例,也就是說在一個項目中只會有一個imageLaoder存在。實例化ImageLoader之後緊接着使用imageLoader.init(ImageLoaderConfiguration)給圖片加載器設置一些配置項並且初始化imageLoaderEngine引擎。這個配置裏面指明瞭內存中圖片的最大寬度和最大高度、任務執行器等:
private ImageLoaderConfiguration(final Builder builder) {
		context = builder.context;
		// 內存中的圖片最大寬度
		maxImageWidthForMemoryCache = builder.maxImageWidthForMemoryCache;
		// 內存中圖片的最大高度
		maxImageHeightForMemoryCache = builder.maxImageHeightForMemoryCache;
		maxImageWidthForDiscCache = builder.maxImageWidthForDiscCache;
		maxImageHeightForDiscCache = builder.maxImageHeightForDiscCache;
		imageCompressFormatForDiscCache = builder.imageCompressFormatForDiscCache;
		// 硬盤緩存中圖片的質量
		imageQualityForDiscCache = builder.imageQualityForDiscCache;
		// 任務執行器
		taskExecutor = builder.taskExecutor;
		taskExecutorForCachedImages = builder.taskExecutorForCachedImages;
		// 線程池的大小
		threadPoolSize = builder.threadPoolSize;
		// 線程的優先級
		threadPriority = builder.threadPriority;
		// 任務處理類型
		tasksProcessingType = builder.tasksProcessingType;
		discCache = builder.discCache;
		memoryCache = builder.memoryCache;
		// 圖片顯示選項
		defaultDisplayImageOptions = builder.defaultDisplayImageOptions;
		loggingEnabled = builder.loggingEnabled;
		downloader = builder.downloader;
		decoder = builder.decoder;
		
		customExecutor = builder.customExecutor;
		customExecutorForCachedImages = builder.customExecutorForCachedImages;
		
		networkDeniedDownloader = new NetworkDeniedImageDownloader(downloader);
		// 網絡緩慢下的情況下圖片的下載器
		slowNetworkDownloader = new SlowNetworkImageDownloader(downloader);
		reserveDiscCache = DefaultConfigurationFactory.createReserveDiscCache(context);
	}
配置中涉及到的具體屬性在後面的源碼中都會涉及到,再具體分析。
上面兩部分做完之後,就可以開始加載圖片了,從ImageLoader的displayImage方法下手分析。在displayImage中首先是檢查有沒有給ImageLoader設置一些加載配置項。然後就是設置圖片加載過程的監聽,這個監聽器可以監聽圖片加載的開始、取消、結束,這樣我麼就可以很靈活的使用它了。接下來就是判斷要加載的圖片uri是否爲空了,爲空就不去加載,但是這裏還做了一個取消即將要顯示的imageview,然後開始加載在配置裏面指定的默認圖片並顯示,最後通知監聽器執行onLoadingComplete方法
<span style="white-space:pre">		</span>if (uri == null || uri.length() == 0) {
			// 取消圖片顯示,取消是根據imageview的hashCode來取消的
			// engine內部維護一個cacheKeysForImageViews,是一個map,key爲imageView的hashcode,value爲memoryCacheKey
			engine.cancelDisplayTaskFor(imageView);
			// 開始加載圖片
			listener.onLoadingStarted(uri, imageView);
			if (options.shouldShowImageForEmptyUri()) {
				imageView.setImageResource(options.getImageForEmptyUri());
			} else {
				imageView.setImageBitmap(null);
			}
			listener.onLoadingComplete(uri, imageView, null);
			return;
		}
如果uri不爲空,則要通知引擎準備加載圖片,並把imageview和imageview在緩存中對應的key大小作爲參數傳入
<span style="white-space:pre">	</span>ImageSize targetSize = ImageSizeUtils.defineTargetSizeForView(imageView, configuration.maxImageWidthForMemoryCache,
				configuration.maxImageHeightForMemoryCache);
	String memoryCacheKey = MemoryCacheUtil.generateKey(uri, targetSize);
	// 通知引擎準備加載圖片,並把圖片的緩存的唯一識別key傳入
	engine.prepareDisplayTaskFor(imageView, memoryCacheKey);
上面的操作做完之後就開始加載了,首先判斷內存中是否存在該圖片,如果圖片存在並且沒有被置爲回收的狀態則顯示圖片,顯示之前,判斷是否需要對圖片進行額外的處理,這個實在配置項中進行配置的,如果需要在顯示前自己可以對圖片進行處理就需要實現BitmapProcessor,並重寫process(Bitmap bitmap)方法。在ProcessAndDisplayImageTask類中:
<span style="white-space:pre">	</span>@Override
	public void run() {
		if (engine.configuration.loggingEnabled) L.i(LOG_POSTPROCESS_IMAGE, imageLoadingInfo.memoryCacheKey);
		BitmapProcessor processor = imageLoadingInfo.options.getPostProcessor();
		final Bitmap processedBitmap = processor.process(bitmap);
		if (processedBitmap != bitmap) {
			bitmap.recycle();
		}
		handler.post(new DisplayBitmapTask(processedBitmap, imageLoadingInfo, engine));
	}
根據配置項中指定圖片處理器處理圖片,處理完之後再顯示,顯示圖片有四種策略,這些策略也是可以配置的。在DisplayBitmapTask類中首先判斷圖片是否錯位,然後再顯示圖片
<span style="white-space:pre">	</span>public void run() {
		if (isViewWasReused()) {
			if (loggingEnabled) L.i(LOG_TASK_CANCELLED, memoryCacheKey);
			listener.onLoadingCancelled(imageUri, imageView);
		} else {
			if (loggingEnabled) L.i(LOG_DISPLAY_IMAGE_IN_IMAGEVIEW, memoryCacheKey);
			/**
			 * 開始顯示圖片,有四種顯示策略
			 * 	SimpleBitmapDisplayer:簡單的直接顯示圖片,setImageBitmap(imageView)
			 * RoundedBitmapDisplayer : 圓角圖片顯示,圓角處理roundCorners方法
			 * FadeInBitmapDisplayer:顯示的時候使用fade in動畫
			 * FakeBitmapDisplayer: 假動作顯示,也就是不顯示圖片
			 */
			Bitmap displayedBitmap = displayer.display(bitmap, imageView);
			listener.onLoadingComplete(imageUri, imageView, displayedBitmap);
			engine.cancelDisplayTaskFor(imageView);
		}
	}
如果不需要額外處理圖片的話就直接顯示圖片。
<span style="white-space:pre">	</span>if (bmp != null && !bmp.isRecycled()) {
			// 如果圖片不爲空,並且沒有被回收,則可以直接顯示
			if (configuration.loggingEnabled) L.i(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
			// 判斷圖片是否需要額外的處理  ,是否需要使用戶自己配置的
			// 如果需要在現實之前做另外的處理,可以實現接口BitmapProcessor,並重寫process(Bitmap bitmap)方法
			if (options.shouldPostProcess()) {
				// 一個實體類,裏面持有uri、imageview、size、緩存key、配置選項等屬性
				ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageView, targetSize, 
						memoryCacheKey, options, listener,engine.getLockForUri(uri));
				// 處理圖片並且顯示圖片,這個是runnable,在裏面又由handler執行了post(DisplayBitmapTask)
				ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, 
						imageLoadingInfo, options.getHandler());
				engine.submit(displayTask);
			} else {
				// 顯示圖片
				options.getDisplayer().display(bmp, imageView);
				// 通知監聽器加載完畢
				listener.onLoadingComplete(uri, imageView, bmp);
			}
		} 
如果圖片不存在緩存中,就需要嘗試從硬盤和網絡中加載了,加載之前判斷是否需要在加載的過程中顯示默認的圖片,然後開啓LoadAndDisplayImageTask自行任務
<span style="white-space:pre">	</span>// 內存緩存中不存在圖片,需要進行網絡加載
	// 判斷加載圖的過程中是否需要顯示圖片
	if (options.shouldShowStubImage()) {
		imageView.setImageResource(options.getStubImage());
	} else {
		if (options.isResetViewBeforeLoading()) {
		<span style="white-space:pre">	</span>imageView.setImageBitmap(null);
		}
	}
	ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageView, targetSize, memoryCacheKey, options, listener, engine.getLockForUri(uri));
	// 加載和顯示圖片的任務,加載策略:先從緩存中查找圖片,再從硬盤中查找,再從網絡中加載
	LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo, options.getHandler());
	engine.submit(displayTask);
在LoadAndDisplayImageTask類的run方法中:
<span style="white-space:pre">	</span>@Override
	public void run() {
		//是否需要等待
		if (waitIfPaused()) return;
		// 是否需要延時
		if (delayIfNeed()) return;
		ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
		log(LOG_START_DISPLAY_IMAGE_TASK);
		if (loadFromUriLock.isLocked()) {
			log(LOG_WAITING_FOR_IMAGE_LOADED);
		}
		// 如果鎖已經被其他線程持有,則會阻塞,當其他的線程執行完畢後會釋放該鎖,此時在等待的線程會獲得該所繼續向下面執行
		loadFromUriLock.lock();
		Bitmap bmp;
		try {
			if (checkTaskIsNotActual()) return;
			// 先從內存中查找
			bmp = configuration.memoryCache.get(memoryCacheKey);
			if (bmp == null) {
				// 在硬盤中查找,再從網絡中查找圖片
				bmp = tryLoadBitmap();
				if (bmp == null) return;
				.......
				if (bmp != null && options.isCacheInMemory()) {
					log(LOG_CACHE_IMAGE_IN_MEMORY);
					configuration.memoryCache.put(memoryCacheKey, bmp);
				}
			} else {
				log(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING);
			}
			........
		} finally {
			loadFromUriLock.unlock();
		}
		if (checkTaskIsNotActual() || checkTaskIsInterrupted()) return;
		// 有四種策略可以顯示圖片
		DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine);
		displayBitmapTask.setLoggingEnabled(loggingEnabled);
		handler.post(displayBitmapTask);
	}
大家應該注意到有這樣的代碼ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;都知道這是一個鎖機制,爲什麼這裏會出現鎖呢?在加載圖片之前會判斷loadFromUriLock.isLocked()是否被上鎖了,如果上鎖了也就意味着同一個uri對應的圖片加載任務已經在執行了,大家可以想象一下這個場景,在listview中當你快速上下滑動列表,同一個uri對對應的圖片是否應該被加載多次呢,所以這裏當第二次加載同樣的uri的時候這裏通過判斷loadFromUriLock.isLocked()返回true,執行這行代碼loadFromUriLock.lock();的時候就會造成堵塞,當這個uri對應的第一個加載任務執行完畢後,這個鎖是會釋放掉的,所以後面的任務往下執行,第一個任務執行完畢後,是會把圖片放入緩存中,所以之後的任務就會再從內粗緩存中查找是否有uri對應的圖片,至此,已經從內存緩存中查找了兩次。
如果是第一次加載這個uri,那麼兩次查找緩存肯定都是空的,那麼就要從文件和網絡中查找了,所以會執行 tryLoadBitmap();方法,加載完畢之後會根據指定的圖片顯示策略顯示圖片。
我們重點關注一下tryLoadBitmap這個方法
<span style="white-space:pre">	</span>private Bitmap tryLoadBitmap() {
		// 硬盤緩存中查找文件
		File imageFile = getImageFileInDiscCache();
		Bitmap bitmap = null;
		try {
			if (imageFile.exists()) {
				// 硬盤緩存中存在
				log(LOG_LOAD_IMAGE_FROM_DISC_CACHE);
				bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
			}
			if (bitmap == null) {
				log(LOG_LOAD_IMAGE_FROM_NETWORK);
				String imageUriForDecoding = options.isCacheOnDisc() ? tryCacheImageOnDisc(imageFile) : uri;
				// 根據uri中指定的協議從何處加載圖片,http、assert、file等協議
				bitmap = decodeImage(imageUriForDecoding);
				if (bitmap == null) {
					fireImageLoadingFailedEvent(FailType.DECODING_ERROR, null);
				}
			}
		} catch (IllegalStateException e) {
			........
		}
		return bitmap;
	}
首先根據uri從文件中查找,存在就直接解碼圖片顯示,不存在的話就要根據指定的協議去加載,這個協議可以使http、assert、file等,關注的方法是BaseImageDecoder類中的decode方法:
<span style="white-space:pre">	</span>public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
		// 返回代表圖片的輸入流,這裏也有幾種策略,緩慢網絡、基本下載器等策略
		InputStream imageStream = getImageStream(decodingInfo);
		ImageFileInfo imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo.getImageUri());
		Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
		imageStream = getImageStream(decodingInfo);
		Bitmap decodedBitmap = decodeStream(imageStream, decodingOptions);
		if (decodedBitmap == null) {
			L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
		} else {
			decodedBitmap = considerExactScaleAndOrientaiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation, imageInfo.exif.flipHorizontal);
		}
		return decodedBitmap;
	}
<span style="white-space:pre">	</span>protected InputStream getImageStream(ImageDecodingInfo decodingInfo) throws IOException {
<span style="white-space:pre">		</span>return decodingInfo.getDownloader().getStream(decodingInfo.getImageUri(), decodingInfo.getExtraForDownloader());
<span style="white-space:pre">	</span>}
getDownLoader()獲取下載器可能會返回幾種下載器,一個是SlowNetworkImageDownloader加載器、NetworkDeniedImageDownloader加載器、HttpClientImageDownloader加載器。HttpClientImageDownloader下載器中使用的是HttpGet請求網絡。我們重點關注的是SlowNetworkImageDownloader加載器,SlowNetworkImageDownloader原型如下:
<span style="white-space:pre">	</span>@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;
		}
	}
是通過FlushedInputStream來獲取流數據的:
<span style="white-space:pre">	</span>public class FlushedInputStream extends FilterInputStream {
	<span style="white-space:pre">	</span>public FlushedInputStream(InputStream inputStream) {
		<span style="white-space:pre">	</span>super(inputStream);
	<span style="white-space:pre">	</span>}

	<span style="white-space:pre">	</span>@Override
	<span style="white-space:pre">	</span>public long skip(long n) throws IOException {
		<span style="white-space:pre">	</span>long totalBytesSkipped = 0L;
		<span style="white-space:pre">	</span>while (totalBytesSkipped < n) {
			<span style="white-space:pre">	</span>long bytesSkipped = in.skip(n - totalBytesSkipped);
			<span style="white-space:pre">	</span>if (bytesSkipped == 0L) {
				<span style="white-space:pre">	</span>int by_te = read();
				<span style="white-space:pre">	</span>if (by_te < 0) {
					<span style="white-space:pre">	</span>break; // we reached EOF
				<span style="white-space:pre">	</span>} else {
					<span style="white-space:pre">	</span>bytesSkipped = 1; // we read one byte
				<span style="white-space:pre">	</span>}
			<span style="white-space:pre">	</span>}
			<span style="white-space:pre">	</span>totalBytesSkipped += bytesSkipped;
		<span style="white-space:pre">	</span>}
		<span style="white-space:pre">	</span>return totalBytesSkipped;
	<span style="white-space:pre">	</span>}
}
爲什麼使用FlushedInputStream呢?大家想想以前你們是怎麼請求網絡圖片的,一般是通過http請求,請求完後使用BitmapFactory的decodeStream方法來獲得一個bitmap。但是這個方法有個致命的bug就是在網絡很慢的請看下面會無法獲取完整的數據,從而導致imageview失真或者顯示出問題,處理這個問題我們可以繼承FilterInputStream來處理skip方法強制實現flush流中的數據。主要原理就是檢查文件是否到文件末端,告訴http是否需要繼續請求。
上述步驟執行完畢後,一個圖片的數據正常獲取,講該圖片放入緩存中,釋放鎖。

















發佈了102 篇原創文章 · 獲贊 22 · 訪問量 41萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章