从源码角度分析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是否需要继续请求。
上述步骤执行完毕后,一个图片的数据正常获取,讲该图片放入缓存中,释放锁。

















  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值