imageIO完成渐进加载图片
一、常见渐进加载图片模式
目前我们看到的渐进加载主要有以下三种实现方式:
1) 依次从web上加载不同尺寸的图片,从小到大。最开始先拉取一个小缩略图做拉伸显示,然后拉取中等规格的图,拉取完毕直接覆盖显示,最后拉取原图,拉取完成后显示原图。
2)直接从web上拉取最大的图片,每接受一点儿数据就显示一点儿图片,这样就会实现从上到下一点点刷新出来的效果。
3)结合第1种和第2种,先拉取一个缩略图做拉伸显示,然后采用第二种方法直接拉取原图,这样即可以实现渐进加载,也可以节省几次中间的网络请求。
二、通过imageIO实现图片的渐进加载
imageIO的guide中原话是这么说的: "If you have a very large image, or are loading image data over the web, you may want to create an incremental image source so that you can draw the image data as you accumulate it. "
翻译过来就是: "如果你想加载一副特别大的图片,或者从网络上加载一副图片,你可以通过创建一个imageSource实现渐进加载的效果。"翻译的不是很地道,大概就是这么个意思,以前在做PowerCam的时候,当时为了在iOS上处理超大图的时候也试过这种方法,当时测试使用的是一副中国地图,分辨率为10000*8000的,结果是当整幅图片加载到内存时,内存吃不消,于是就放弃了。现在想想对于这种超大图片的处理,我们可以采用分片的方式进行,每次只需要处理一小块图片即可,这个问题就留给大家思考吧。
今天我们要讨论的是CGImageSource实现从web端渐进加载图片,要达到这个目的我们需要创建一个URLConnnection,然后实现代理,每次接收到数据时更新图片即可。下面主要的实现源码:
//
// SvIncrementallyImage.m
// SvIncrementallyImage
//
// Created by maple on 6/27/13.
// Copyright (c) 2013 maple. All rights reserved.
//
#import "SvIncrementallyImage.h"
#import <ImageIO/ImageIO.h>
#import <CoreFoundation/CoreFoundation.h>
@interface SvIncrementallyImage () {
NSURLRequest *_request;
NSURLConnection *_conn;
CGImageSourceRef _incrementallyImgSource;
NSMutableData *_recieveData;
long long _expectedLeght;
bool _isLoadFinished;
}
@property (nonatomic, retain) UIImage *image;
@property (nonatomic, retain) UIImage *thumbImage;
@end
@implementation SvIncrementallyImage
@synthesize imageURL = _imageURL;
@synthesize image = _image;
@synthesize thumbImage = _thumbImage;
- (id)initWithURL:(NSURL *)imageURL
{
self = [super init];
if (self) {
_imageURL = [imageURL retain];
_request = [[NSURLRequest alloc] initWithURL:_imageURL];
_conn = [[NSURLConnection alloc] initWithRequest:_request delegate:self];
_incrementallyImgSource = CGImageSourceCreateIncremental(NULL);
_recieveData = [[NSMutableData alloc] init];
_isLoadFinished = false;
}
return self;
}
#pragma mark -
#pragma mark NSURLConnectionDataDelegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
_expectedLeght = response.expectedContentLength;
NSLog(@"expected Length: %lld", _expectedLeght);
NSString *mimeType = response.MIMEType;
NSLog(@"MIME TYPE %@", mimeType);
NSArray *arr = [mimeType componentsSeparatedByString:@"/"];
if (arr.count < 1 || ![[arr objectAtIndex:0] isEqual:@"image"]) {
NSLog(@"not a image url");
[connection cancel];
[_conn release]; _conn = nil;
}
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSLog(@"Connection %@ error, error info: %@", connection, error);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(@"Connection Loading Finished!!!");
// if download image data not complete, create final image
if (!_isLoadFinished) {
CGImageSourceUpdateData(_incrementallyImgSource, (CFDataRef)_recieveData, _isLoadFinished);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_incrementallyImgSource, 0, NULL);
self.image = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
}
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[_recieveData appendData:data];
_isLoadFinished = false;
if (_expectedLeght == _recieveData.length) {
_isLoadFinished = true;
}
CGImageSourceUpdateData(_incrementallyImgSource, (CFDataRef)_recieveData, _isLoadFinished);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_incrementallyImgSource, 0, NULL);
self.image = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
}
@end
从上面代码中我们可以看到,一开始我们根据传入的URL创建一个URLConnection,同时创建一个空的CGImageSource,然后在每次收到数据的时候调用CGImageSourceUpdateData更新imageSource的数据,接着调用CGImageSourceCreateImageAtIndex获取最新的图片即可。
怎么样,看到上面的实现是不是感觉实现从web上渐进加载图片很简单,虽然imageIO帮我们做了很多事情,但是我们也应该了解它的原理。我们知道文件都是有格式的,一般文件的头部会记录一些关于文件格式的数据,后面就是实际的文件数据。
拿最简单的BMP图片文件举例:
1) 最开始的BITMAPFILEHEADER,这部分主要记录文件的大小,以及实际的图像数据离文件头的距离。
2) 接着是BITMAPINFOHEADER,这部分主要记录图片的宽,高,位深等信息
3)可选的调色板信息
4)最后一部分就是实际的图片数据。
前三部分的信息很小,一般加起来不会超过100个字节,获取到这写信息以后,我们就可以很轻松的根据后面的数据构建出图片,当数据获取的越来越完整的时候,我们构造出的图片就会越完整,直至全部加载完成。
BMP格式是简单的图片格式,其他的JPG,PNG虽然结果更加复杂,但是总体构成都差不多。imageIO正是帮助我们完成了众多图片格式的编解码,然后一步步构造出最终的图片。
使用imageIO获取图片的exif信息
一幅图片除了包含我们能看见的像素信息,背后还包含了拍摄时间,光圈大小,曝光等信息。UIImage类将这些细节信息都隐藏了起来,只提供我们关心的图片尺寸,图片方向等。我们可以通过imageIO框架获取到图片背后的所有信息,下面就让我们一起看看。
imageIO框架是iOS中偏底层一点儿的框架,它内部提供的接口都是C风格的,关键数据也都是使用CoreFoundation进行存储。庆幸的是CoreFoundation中有很多数据类型都可以上层的数据Foundation框架中的数据类型进行无缝桥接。这也就大大方便了我们对图片信息的操作。
CGImageSourceRef是整个imageIO的入口,通过它我们可以完成从文件的加载图片。加载完成以后我们就得到一个CGImageSourceRef,通过CGImageSourceRef我们就可以获取图片文件的大小,UTI(uniform type identifier),内部包含几张图片,访问每一张图片以及获取每张图片对应的exif信息等。
你可能会有一个疑问,为什么会有几张图片呢?
这块儿我解释一下,imageSourceRef和文件是一一对应的,通常我们见到的图片文件(例如jpg,png)内部都只有一张图片,这种情况我们通过CGImageSourceGetCount方法得到的就会是1。但是不能排除一个图片文件中会有多种图片的情况,例如gif文件,这个时候一个文件中就可能包含几张甚至几十张图片。前面我写的一片博客《IOS中如何解析并显示Gif》就是通过imageSource实现加载和解析gif的功能。
下面是系统相机拍的照片的exif信息:
image property: {
ColorModel = RGB;
DPIHeight = 72;
DPIWidth = 72;
Depth = 8;
Orientation = 6;
PixelHeight = 2448;
PixelWidth = 3264;
"{Exif}" = {
ApertureValue = "2.526069";
BrightnessValue = "-0.5140446";
ColorSpace = 1;
ComponentsConfiguration = (
1,
2,
3,
0
);
DateTimeDigitized = "2013:06:24 22:11:30";
DateTimeOriginal = "2013:06:24 22:11:30";
ExifVersion = (
2,
2,
1
);
ExposureMode = 0;
ExposureProgram = 2;
ExposureTime = "0.06666667";
FNumber = "2.4";
Flash = 16;
FlashPixVersion = (
1,
0
);
FocalLenIn35mmFilm = 33;
FocalLength = "4.13";
ISOSpeedRatings = (
400
);
MeteringMode = 3;
PixelXDimension = 3264;
PixelYDimension = 2448;
SceneCaptureType = 0;
SensingMethod = 2;
ShutterSpeedValue = "3.906905";
SubjectArea = (
2815,
1187,
610,
612
);
WhiteBalance = 0;
};
"{GPS}" = {
Altitude = "27.77328";
AltitudeRef = 0;
Latitude = "22.5645";
LatitudeRef = N;
Longitude = "113.8886666666667";
LongitudeRef = E;
TimeStamp = "14:11:23.36";
};
"{TIFF}" = {
DateTime = "2013:06:24 22:11:30";
Make = Apple;
Model = "iPhone 5";
Orientation = 6;
ResolutionUnit = 2;
Software = "6.1.4";
XResolution = 72;
YResolution = 72;
"_YCbCrPositioning" = 1;
};
}
从中我们可以看出最开始的几项分别显示了当前图片的颜色模式,色深,x,y方向的DPI,实际像素以及图片的方向。我最开始看到这个方向时,心中一喜这不是UIImage中的imageOrientation,但是实验发现这个方向和UIImage中的imageOrientation并不相等,此处的方向是exif标准定义的方向,从1到8分别对应这UIImage中的8个方向,只是顺序不一样,它们对应关系如下:
enum {
exifOrientationUp = 1, // UIImageOrientationUp
exifOrientationDown = 3, // UIImageOrientationDown
exifOrientationLeft = 6, // UIImageOrientationLeft
exifOrientationRight = 8, // UIImageOrientationRight
// these four exifOrientation does not support by all camera, but IOS support these orientation
exifOrientationUpMirrored = 2, // UIImageOrientationUpMirrored
exifOrientationDownMirrored = 4, // UIImageOrientationDownMirrored
exifOrientationLeftMirrored = 5, // UIImageOrientationLeftMirrored
exifOrientationRightMirrored = 7, // UIImageOrientationRightMirrored
};
typedef NSInteger ExifOrientation;
目前市面上的大部分数码相机和手机都会内置一个方向感应器,拍出的照片中会写如方向信息,但是通常都只会有前四种方向。这几种Mirrored方向通常都是手机前置摄像头自拍的时候才会设置。
exif为什么要搞这么一个方向呢?
几乎所有的摄像头在出场的时候成相芯片都是有方向的,拍出来的照片的像素都是默认方向的。如果每拍一张照片就对这些像素进行旋转,如果数码相机每秒连拍20张来算,旋转操作将会非常耗时。更聪明的做法是拍照时只记录一个方向,然后显示的时候按方向显示出来即可。因此exif定义了一个标准的方向参数,只要读图的软件都来遵守规则,加载时候读取图片方向,然后做相应的旋转即可。这样既可以达到快速成像的目的,又能达到正确的显示,何乐而不为呢。
常见的图片浏览和编辑软件都遵守这个规则,但是有一个我们最常用的看图软件(windows自带的看图程序)不会去读这个方向,因此我们将数码相机和手机拍出来的图片导入windows上时,会经常遇到方向错误的问题。不知道windows帝国是怎么想的,或许和定义exif的组织有什么过节吧。
图片信息中除了上面看提到的那些,还有拍摄的GPS信息,iOS自带的相册软件中的地点tab就是按照GPS信息实现的。还有很多其他的信息,感兴趣的可以自己写个程序研究研究,这里就不展开了。