Android-Universal-Imageloader源码完全解析

现在网上对此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) {
<span style="white-space:pre">	</span>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缓存在本地缓存中<span style="font-family: Arial, Helvetica, sans-serif;">// TODO : process boolean result</span>
	}
}
首先看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://本地文件中获取
<span style="white-space:pre">	</span>	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);
}
主要看从网络获取图片实现:

<span style="white-space:pre">		</span>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存储到磁盘缓存中。

<span style="white-space:pre">		</span>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。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值