1、发现问题
项目中有两个图片加载框架(不要问我为什么要这样,前任,也不算前任他还在公司o(╥﹏╥)o)大名鼎鼎的Glide和以前很火的ImageLoader
突然有一天,客户反馈说有个照片旋转了90度。明明自己拍照是正常的,上传到管理平台(也就是web端)也是正常的。
一开始,我们肯定觉得原因在于客户,因为有时候客户确实很SB,我们的理由是,这个列表,其他照片都正常,那说明我的代码没问题,肯定是客户传的照片本来就是旋转的。
虽然我们很肯定是客户的问题,但是问题还是要解决的,等客户去认证就慢了,于是乎,我自己调试接口拿到图片链接,然后我就。。。。裂开了
浏览器显示是正常的,没有旋转,我不死心,下载到电脑,还是正常的。
这个时候我意识到,问题并不简单,然后我试着换Glide看看。之前这里是用ImageLoader的。
真相就是,Glide正常,ImageLoader有问题。
于是我搜一下ImageLoader配置图片旋转的API,果然被我找到了considderExifParams这个方法,描述就是设置是否需要旋转,默认是false,然后我看了我们的代码,没有设置这个方法,那默认就是不旋转。
等等,好像有什么不对!
照片链接在浏览器上看是正常的,下载到电脑也是正常的,然后我在代码里也没有做旋转,然后照片显示就 旋 转 了
难道是框架内部帮我们处理了,因为Glide都没有问题,所以我试着ImageLoader的considderExifParams配置了true,运行看效果,正常了。。。。。
果然如此,此刻我心中暗喜,这就是码农的日常,找到并解决BUG的喜悦!
BUG虽然解决了,但是真正的原因其实还不知道,需要去认证自己的想法。
2、ImageLoader源码分析
如果全部分析ImageLoader框架的源码那就太费时了,我们只需要找到图片旋转的代码就可以了,带着目的去看源码才是效率最高的。
我们的目的是找到图片下载的地方,然后看看旋转是怎么实现的
顺着displayImage()往下找
ImageLoader.java
public void displayImage(String uri, ImageView imageView, DisplayImageOptions options)
{
displayImage(uri, new ImageViewAware(imageView), options, null, null);
}
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
if (options == null) {
options = configuration.defaultDisplayImageOptions;
}
//省略了一些无关代码
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
//这个对象后面会讲到,传人的engine
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
}
}
代码比较多,我去掉了无关代码,LoadAndDisplayImageTask,实现了runnable,所以到时候会执行run方法
先看一下run方法做了什么
LoadAndDisplayImageTask.java
@Override
public void run() {
//省略代码
Bitmap bmp;
try {
checkTaskNotActual();
bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp == null || bmp.isRecycled()) {
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);
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);
}
}
//省略代码
} catch (TaskCancelledException e) {
fireCancelEvent();
return;
} finally {
loadFromUriLock.unlock();
}
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
runTask(displayBitmapTask, syncLoading, handler, engine);
}
很明显,tryLoadBitmap()这可能会是获取图片的地方,跟进去看看
private Bitmap tryLoadBitmap() throws TaskCancelledException {
Bitmap bitmap = null;
try {
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()));
}
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey);
loadedFrom = LoadedFrom.NETWORK;
String imageUriForDecoding = uri;
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
imageFile = configuration.diskCache.get(uri);
if (imageFile != null) {
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
}
}
checkTaskNotActual();
//这里获取图片继续跟进
bitmap = decodeImage(imageUriForDecoding);
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
fireFailEvent(FailType.DECODING_ERROR, null);
}
}
} catch (IllegalStateException e) {
fireFailEvent(FailType.NETWORK_DENIED, null);
} catch (TaskCancelledException e) {
throw e;
} catch (IOException e) {
L.e(e);
fireFailEvent(FailType.IO_ERROR, e);
} catch (OutOfMemoryError e) {
L.e(e);
fireFailEvent(FailType.OUT_OF_MEMORY, e);
} catch (Throwable e) {
L.e(e);
fireFailEvent(FailType.UNKNOWN, e);
}
return bitmap;
}
bitmap = decodeImage(imageUriForDecoding);进行跟进这里
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);
}
继续跟进这个 decoder.decode(decodingInfo);
public interface ImageDecoder {
/**
* Decodes image to {@link Bitmap} according target size and other parameters.
*
* @param imageDecodingInfo
* @return
* @throws IOException
*/
Bitmap decode(ImageDecodingInfo imageDecodingInfo) throws IOException;
}
发现decoder是个接口,那就找到实现者,跟踪LoadAndDisplayImageTask的构造方法
LoadAndDisplayImageTask.java
public LoadAndDisplayImageTask(ImageLoaderEngine engine, ImageLoadingInfo imageLoadingInfo, Handler handler) {
this.engine = engine;
this.imageLoadingInfo = imageLoadingInfo;
this.handler = handler;
//发现是engine的配置引用
configuration = engine.configuration;
downloader = configuration.downloader;
networkDeniedDownloader = configuration.networkDeniedDownloader;
slowNetworkDownloader = configuration.slowNetworkDownloader;
//是配置类configuration的
decoder = configuration.decoder;
uri = imageLoadingInfo.uri;
//省略代码
}
engine好像有点眼熟在前面见过,在run方法里创建这个对象LoadAndDisplayImageTask,传人engine引用
好家伙,原来是在这初始化了,我们在application里会初始化这个init
ImageLoader.java
public synchronized void init(ImageLoaderConfiguration configuration) {
if (configuration == null) {
throw new IllegalArgumentException(ERROR_INIT_CONFIG_WITH_NULL);
}
if (this.configuration == null) {
L.d(LOG_INIT_CONFIG);
engine = new ImageLoaderEngine(configuration);
this.configuration = configuration;
} else {
L.w(WARNING_RE_INIT_CONFIG);
}
}
而我初始化的时候没有设置加载器decoder,所以我们进入ImageLoaderConfiguration类看看decoder的创建
ImageLoaderConfiguration.java
private ImageLoaderConfiguration(final Builder builder) {
//省略代码
decoder = builder.decoder;
//省略代码
}
接着看看它的构造器Builder
ImageLoaderConfiguration.java
public ImageLoaderConfiguration build() {
initEmptyFieldsWithDefaultValues();
return new ImageLoaderConfiguration(this);
}
很明显,默认赋值就在这里了initEmptyFieldsWithDefaultValues();
ImageLoaderConfiguration.java
private void initEmptyFieldsWithDefaultValues() {
//省略代码
if (decoder == null) {
decoder = DefaultConfigurationFactory.createImageDecoder(writeLogs);
}
if (defaultDisplayImageOptions == null) {
defaultDisplayImageOptions = DisplayImageOptions.createSimple();
}
}
找到加载器的创建了,继续跟进createImageDecoder,找到了这个BaseImageDecoder类,看看它的
BaseImageDecode.java
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;
}
终于找到我们想要的代码了imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
BaseImageDecode.java
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 {
//默认旋转角度是0
exif = new ExifInfo();
}
return new ImageFileInfo(new ImageSize(options.outWidth, options.outHeight, exif.rotation), exif);
}
exif = defineExifOrientation(imageUri);这里应该就是获取图片旋转角度的逻辑,进去看看
BaseImageDecode.java
protected ExifInfo defineExifOrientation(String imageUri) {
int rotation = 0;
boolean flip = false;
try {
//找到了,这个对象,就是获取图片信息的
ExifInterface exif = new ExifInterface(Scheme.FILE.crop(imageUri));
//这里获取到图片的旋转角度
int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (exifOrientation) {
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
flip = true;
case ExifInterface.ORIENTATION_NORMAL:
rotation = 0;
break;
case ExifInterface.ORIENTATION_TRANSVERSE:
flip = true;
case ExifInterface.ORIENTATION_ROTATE_90:
rotation = 90;
break;
case ExifInterface.ORIENTATION_FLIP_VERTICAL:
flip = true;
case ExifInterface.ORIENTATION_ROTATE_180:
rotation = 180;
break;
case ExifInterface.ORIENTATION_TRANSPOSE:
flip = true;
case ExifInterface.ORIENTATION_ROTATE_270:
rotation = 270;
break;
}
} catch (IOException e) {
L.w("Can't read EXIF tags from file [%s]", imageUri);
}
return new ExifInfo(rotation, flip);
}
ExifInterface类 是获取照片信息的,如果你对这个对象不熟悉,自行百度,我们回到decode(ImageDecodingInfo decodingInfo)方法,看看拿到旋转角度后怎么做
这段代码 decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation, imageInfo.exif.flipHorizontal);
BaseImageDecode.java
protected Bitmap considerExactScaleAndOrientatiton(Bitmap subsampledBitmap, ImageDecodingInfo decodingInfo,
int rotation, boolean flipHorizontal) {
Matrix m = new Matrix();
//省略代码
// Rotate bitmap if need
if (rotation != 0) {
m.postRotate(rotation);//矩阵旋转
if (loggingEnabled) L.d(LOG_ROTATE_IMAGE, rotation, decodingInfo.getImageKey());
}
//最终得到Bitmap
Bitmap finalBitmap = Bitmap.createBitmap(subsampledBitmap, 0, 0, subsampledBitmap.getWidth(), subsampledBitmap
.getHeight(), m, true);
if (finalBitmap != subsampledBitmap) {
subsampledBitmap.recycle();
}
return finalBitmap;
}
这里根据之前算出来的角度,矩阵旋转,得到最终的Bitmap
总结
最终获取图片角度的类是Android原生提供的ExifInterface类
获取旋转角度 int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
然后再出来,得到角度值,根据矩阵旋转,得到最终的bitmap Bitmap finalBitmap = Bitmap.createBitmap(subsampledBitmap, 0, 0, subsampledBitmap.getWidth(), subsampledBitmap
.getHeight(), m, true);//m是矩阵Matrix
、下一次看看Glide的源码,看看是不是也这样实现的
思考:原来浏览器上看到的和下载到电脑的图片方向,并不是正确的方向。我想是浏览器和电脑端都已经做了处理。