Android-Universal-Image-Loader (图片异步加载缓存库)对Bitmap的优化处理

转载请注明出处:http://blog.csdn.net/u011733020

前言:

前面两篇分别介绍了:

Android-Universal-Image-Loader (图片异步加载缓存库)的使用配置 

Android-Universal-Image-Loader (图片异步加载缓存库)的源码解读 

通过前两篇,我们了解了 UIL的使用配置,UIL将服务器上的一张图片保存到本地,加载到内存的过程,以及UIL对DiscCache和MemoryCache的策略,但是还有一部分比较重要,因为它是我们的开发日常中经常要处理的一个问题:Bitmap的优化。换句话说:如何将一个大的图片,加载到内存并显示,如果我们不处理,那么很容易发生OOM。

那么UIL作为一款经典图片缓存框架接下来,我们就学习一下UIL中如何优化Bitmap,避免发生OOM的,以后在我们项目开发的时候就可以用相同的方法去解决类似的问题。

正文

大图片加载到内存的两种方法对比

首先我们先不用UIL ,直接加载一张大图片会发生什么?

Android-Universal-Image-Loader (图片异步加载缓存库)对Bitmap的优化处理_第1张图片

将上述21M的本地图片aaa.jpg直接通过加载到内存

private String uri_virtual="/mnt/sdcard/UIL/Document/pics/aaa.jpg";
Bitmap bm=BitmapFactory.decodeFile(uri_virtual);
errImage.setImageBitmap(bm);
运行一下程序会发现发生了crash

Android-Universal-Image-Loader (图片异步加载缓存库)对Bitmap的优化处理_第2张图片
在logcat中报错如下


这是一个非常常见的错误:内存溢出(Out Of Memory)。

导致这个错误的原因一般是 加载了一个超过dalivk heap 的size(一般16M) 的文件,或者 内存使用频繁,释放不及时,导致内存不够用。

解决OOM的方法就是: 使用 弱引用WeakReference,手动释放内存 System.gc(),将Bitmap压缩 等。

那么我们在用UIL去加载这一张大图片:

		image = (ImageView) findViewById(R.id.iv);
		DisplayImageOptions displayOptions = new DisplayImageOptions.Builder()
				.cacheInMemory(true).bitmapConfig(Bitmap.Config.RGB_565)
				.cacheOnDisk(true).build();
		ImageLoader.getInstance().displayImage(uri_virtual, image,
				displayOptions);
发现加载成功:


可见UIL 内部对其进行了处理,使其加载成功。

UIL加载优化分析

加载的过程 上一篇http://blog.csdn.net/u011733020/article/details/51043810已经分析过了,这里我们直接从LoadAndDisplayImageTask的run() 方法入手,因为在run() 方法里 将本地文件 转成inputStream。
在run() 方法里面找到这样一个方法 tryLoadBitmap(),在其方法内有这样一段代码:
	// 尝试 本地文件中是否有缓存
	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()));
	}
这里07行,执行了一个decodeImage 的方法,根据返回值 跟传入的参数,我们不难看出,这个方法的作用是,根据本地图片的路径,将其转成bitmap加载进内存。
	private Bitmap decodeImage(String imageUri) throws IOException {
		ViewScaleType viewScaleType = imageAware.getScaleType();
		ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
				getDownloader(), options);
		return decoder.decode(decodingInfo);
	}
从上面的 decodeImage 方法的实现来看,最终 将本地文件转成bitmap 是由 decoder.decode(decodingInfo) 来完成的。那么就去看decode() 方法:
ImageDecoder 是一个接口,BaseImageDecoder实现了ImageDecoder ,实现了decode 方法:
	/**
	 * Decodes image from URI into {@link Bitmap}. Image is scaled close to incoming {@linkplain ImageSize target size}
	 * during decoding (depend on incoming parameters).
	 * @param decodingInfo Needed data for decoding image: 如果 具体View  没有指定 wh  为手机分辨率 px 否则为 设置的px值
	 * @return Decoded bitmap
	 * @throws IOException                   if some I/O exception occurs during image reading
	 * @throws UnsupportedOperationException if image URI has unsupported scheme(protocol)
	 */
	@Override
	public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
		Bitmap decodedBitmap;
		ImageFileInfo imageInfo;
		InputStream imageStream = getImageStream(decodingInfo);
		if (imageStream == null) {
			L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey());
			return null;
		}
		try {
			imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
			imageStream = resetStream(imageStream, decodingInfo);
			Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
			decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
		} 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;
	}
再看方法以前,我先解释一下 ImageDecodingInfo 这是一个非常重要的类,它里面封装了我们布局里设置子的ImageView的一些属性,比如    android:layout_width   android:layout_height
以及一些Options 属性。

Options 属性介绍

destOptions.inDensity 
destOptions.inDither 
destOptions.inInputShareable 
destOptions.inJustDecodeBounds 
destOptions.inPreferredConfig 
destOptions.inPurgeable
destOptions.inSampleSize 
destOptions.inScaled
destOptions.inScreenDensity
destOptions.inTargetDensity 
destOptions.inTempStorage 
destOptions.inPreferQualityOverSpeed
destOptions.inBitmap
destOptions.inMutable 
而对Bitmap的压缩,都是按照bitmap的这些属性来做的。
介绍完了ImageDecodingInfo ,我们接着回到上面的decode() 方法,我们看到13行 拿到了InputStream 接下来 在19行,根据InputSream 拿到了本地图片的分辨率信息,一起看一下defineImageSizeAndRotation() 方法:

根据InpuStream 使用Options.inJustDecodeBounds 获取图片信息

	/**
	 * //options.outWidth:11935options.outHeight:8554  根据文件流 拿到 本地图片的分辨率
	 * @param imageStream: 文件流
	 * @param decodingInfo: 本地图片的文件信息
	 * @return
	 * @throws IOException
	 */
	protected ImageFileInfo defineImageSizeAndRotation(InputStream imageStream, ImageDecodingInfo decodingInfo)
			throws IOException {
		Options options = new Options();
		options.inJustDecodeBounds = true;
		BitmapFactory.decodeStream(imageStream, null, options);
		ExifInfo exif;
		String imageUri = decodingInfo.getImageUri();
		if (decodingInfo.shouldConsiderExifParams() && canDefineExifParams(imageUri, options.outMimeType)) {
			exif = defineExifOrientation(imageUri);
		} else {
			exif = new ExifInfo();
		}
		//options.outWidth:11935options.outHeight:8554  根据文件流 拿到 本地图片的分辨率
		return new ImageFileInfo(new ImageSize(options.outWidth, options.outHeight, exif.rotation), exif);
	}
关键是 10  11 12 这三行,首先设置Options.inJustDecodeBounds=true 这样设置

意思就是:
如果设置为真,解码器将返回空(无位图),但输出…字段将设置为,允许调用方查询该位图而不必为其像素分配内存。
在接下来12行执行时,将只会获取到分辨率的大小,而不会将bitmap 加载到内存。
拿到了大小,这个方法就是将本地图片的信息封装一下,然后返回,我们在回到decode() 方法,经过20 21 22 这三行,我们就拿到了最终符合我们需要的bitmap,那么一起看这三行做的事情:
imageStream = resetStream(imageStream, decodingInfo); //重新获取一次文件流
最关键的是22行 经过prepareDecodingOptions() 拿到了最终的Options:

根据本地图片属性与布局中的ImagView 比较,算出缩放比例:

       /**
	 * @param imageSize 本地图片的大小
	 * @param decodingInfo :需要的编译规格  比如 设定过 wh  或者默认的  手机分辨率
	 * @return
	 */
	protected Options prepareDecodingOptions(ImageSize imageSize, ImageDecodingInfo decodingInfo) {
		ImageScaleType scaleType = decodingInfo.getImageScaleType();
		int scale;
		if (scaleType == ImageScaleType.NONE) {
			scale = 1;
		} else if (scaleType == ImageScaleType.NONE_SAFE) {
			scale = ImageSizeUtils.computeMinImageSampleSize(imageSize);
		} else {
			ImageSize targetSize = decodingInfo.getTargetSize();
			boolean powerOf2 = scaleType == ImageScaleType.IN_SAMPLE_POWER_OF_2;
			scale = ImageSizeUtils.computeImageSampleSize(imageSize, targetSize, decodingInfo.getViewScaleType(), powerOf2);
		}
		if (scale > 1 && loggingEnabled) {
			L.d(LOG_SUBSAMPLE_IMAGE, imageSize, imageSize.scaleDown(scale), scale, decodingInfo.getImageKey());
		}
		Options decodingOptions = decodingInfo.getDecodingOptions();
		decodingOptions.inSampleSize = scale;
		// insampleSize =n  表示 缩小到原来的 1/n  比如    1/2 占的容量变小 对已经产生的bitmap 不生效,只能对 BitmapFactory
        // 只能用BitmapFactory生成的Bitmap才有用,如BitmapFactory.decodeResource(res, id, options)这种方法。把options放到参数里面就可以了。
		return decodingOptions;
	}
这个方法只做了一件事,decodingOptions.inSampleSize = scale;就是设置inSampleSize的值,Options.inSampleSize的意思就是缩小到原来的几分之一,值越大,表示缩放的倍数越大,可见上面的注释。
我觉得获取 inSampleSize = scale的值,是本篇文章最重要得地方,那么我们接下来,就详细看一下如何获取inSampleSize的值
直接看16行执行的方法computeImageSampleSize()
public static int computeImageSampleSize(ImageSize srcSize, ImageSize targetSize, ViewScaleType viewScaleType,
			boolean powerOf2Scale) {
		final int srcWidth = srcSize.getWidth();
		final int srcHeight = srcSize.getHeight();
		final int targetWidth = targetSize.getWidth();
		final int targetHeight = targetSize.getHeight();
		int scale = 1;
		switch (viewScaleType) {
			case FIT_INSIDE: // 过按比例缩小或原来的size使得图片长/宽等于或小于View的长/宽
				if (powerOf2Scale) {
					final int halfWidth = srcWidth / 2;
					final int halfHeight = srcHeight / 2;
					while ((halfWidth / scale) > targetWidth || (halfHeight / scale) > targetHeight) { // ||
						scale *= 2;
					}
				} else {
					scale = Math.max(srcWidth / targetWidth, srcHeight / targetHeight); // max
				}
				break;
			case CROP://  按比例扩大图片的size居中显示,使得图片长(宽)等于或大于View的长(宽) 
				if (powerOf2Scale) {
					final int halfWidth = srcWidth / 2;
					final int halfHeight = srcHeight / 2;
					while ((halfWidth / scale) > targetWidth && (halfHeight / scale) > targetHeight) { // &&
						scale *= 2;
					}
				} else {
					scale = Math.min(srcWidth / targetWidth, srcHeight / targetHeight); // min
				}
				break;
		}

		if (scale < 1) {
			scale = 1;
		}
		scale = considerMaxTextureSize(srcWidth, srcHeight, scale, powerOf2Scale);
		return scale;
	}
注释了两种type ,大图片肯定是要缩放,而缩放的规格就是 13 14行的运算
while ((halfWidth / scale) > targetWidth || (halfHeight / scale) > targetHeight) { // ||
						scale *= 2;
					}
根据本地图片的宽度1/2除以 scale,与 我们布局中设置的宽度(如果没设置 宽高为手机分辨率)比较,直到小于我们设置的ImageView宽 高时,拿到此时的scale值,接下来在经过36行considerMaxTextureSize() 方法去确定最终的scale
	private static int considerMaxTextureSize(int srcWidth, int srcHeight, int scale, boolean powerOf2) {
		final int maxWidth = maxBitmapSize.getWidth();
		final int maxHeight = maxBitmapSize.getHeight();
		while ((srcWidth / scale) > maxWidth || (srcHeight / scale) > maxHeight) {
			if (powerOf2) {
				scale *= 2;
			} else {
				scale++;
			}
		}
		return scale;
	}

这个方法是为了进一步确定scale的值,应该是尽可能的大,这里涉及到 OpenGL ES  的一些东西,我们先不管。 
上过程最终得到的scale 设置给了decodingOptions.inSampleSize = scale。  

然后通过前面的 decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);最终拿到了适合的Bitmap,接下来的过程就是设置显示的过程,这里就不在分析了。

数据证明

接下来我们就拿21M的图片加载,看到底把Bitmap优化到什么程度。
我们在BaseImageDecoder 的decode 方法,当 decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);执行完毕后,我们调用bitmap.getByteCount()方法获取一下存储该bitmap 所需要的最小字节数:
	public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
		Bitmap decodedBitmap;
		ImageFileInfo imageInfo;
		InputStream imageStream = getImageStream(decodingInfo);
		if (imageStream == null) {
			L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey());
			return null;
		}
		try {
			imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
			imageStream = resetStream(imageStream, decodingInfo);
			Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
			decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
			Log.e("decode"," bytecount: "+decodedBitmap.getByteCount()+"   density:"+decodedBitmap.getDensity()+" H:"
					+decodedBitmap.getHeight()+"  W:"+decodedBitmap.getWidth());
		} 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;
	}

运行我们的程序,可以看到log信息 199182b=194kb,也就是说21M的图片,加载进内存只占用了194kb,效果还是很明显的吧。
原图片分辨率 w*h= 11935*8554
模拟器 bytecount: 199182   density:160   bitmap's height :267  bitmap's width:373   scale:32

结语:

三篇文章带给我的收获:UIL的使用配置 ,缓存策略 和 图片优化, 通过分析UIL的源码,进一步梳理了图片缓存的流程,加深了对memoryCache 和diskCache的理解,并且对Bitmap的优化,也有了更清晰地理解。

前面两篇地址:

Android-Universal-Image-Loader (图片异步加载缓存库)的使用配置 

Android-Universal-Image-Loader (图片异步加载缓存库)的源码解读 


谢谢认真观读本文的每一位小伙伴,衷心欢迎小伙伴给我指出文中的错误,也欢迎小伙伴与我交流学习。

爱学习的小伙伴请戳进QQ群:230274309 。




你可能感兴趣的:(图片缓存,避免OOM,Bitmap的优化)