一、关于图片的两种格式,PNG和JPEG
图片文件被加载后必须要进行解码,解码过程是一个相当复杂的任务,需要消耗非常长的时间。解码后的图片将同样使用相当大的内存。
用于加载的CPU时间相对于解码来说根据图片格式而不同。对于iOS来说大多处理的就是PNG和JPEG了。
- JPEG可以非常好的压缩图片,但是JPEG是失真的,它去除了人眼很难察觉的信息,并且这样做可以超出像gzip这样压缩算法的限制,但是压缩算法也更复杂。相对来说,JPEG图片更小,加载更快,但是解压的步骤要消耗更长的时间。
- 对于PNG来说它的压缩是无损的,所以图片相对JPEG来说也大很多,加载时间会比JPEG更长,但是解码会相对较快,而且Xcode会把PNG图片进行解码优化之后引入工程,当Xcode优化一个 PNG 文件的时候,它将 PNG 文件变成一个从技术上讲不再是有效的PNG文件。但是 iOS 可以读取这种文件,并且这比解压缩正常的 PNG 文件更快。
当加载图片的时候,iOS通常会延迟解压图片的时间,直到加载到内存之后。这就会在准备绘制图片的时候影响性能,因为需要在绘制之前进行解压(通常是消耗时间的问题所在)。
二、图片的加载
图片的加载的几种方法,这里只说两种:
-
使用缓存,多处重复使用的图片使用imageNamed方法:
+ (nullable UIImage *)imageNamed:(NSString *)name;
此方法避免延时解压,会在加载图片之后立刻进行解压。不过+imageNamed:
只对从应用资源中的图片有效,所以对用户生成的图片内容或者是下载的图片就没法使用了。另外,此方法会在系统缓存中查找具有指定名称的图像对象,并在存在时返回该对象。如果匹配的图像对象尚未在缓存中,该方法根据指定的图片名加载图片并解压,缓存它,然后返回生成的对象。
在nib文件中引用的图片同样也是这个机制,所以你很多时候都在隐式的使用它。 大图,且就加载一次的图片:
+ (nullable UIImage *)imageWithContentsOfFile:(NSString *)path;
如果图片只显示一次,并且希望它不被添加到系统的缓存中,那么应该使用imageWithContentsOfFile:
。此方法不应用系统图片缓存,对于很多只加载单次的图片来说,使用此方法可以潜在的改善内存使用。
在说图片的解压和渲染绘制之前,我们先说下CoreGraphics中的一些我们要用到的方法等。
1.Graphics Context(图形上下文)类型:
- View Graphics Context :iOS中的绘图图形上下文,在drawRect方法中可以通过UIGraphicsGetCurrentContext获得。
- Window Graphics Context :Mac OS X中的图形上下文,不多说。
- PDF Graphics Context :用于创建PDF文档的图形上下文。
- Bitmap Graphics Context :位图图形上下文。
- Graphics Context for Printing:为打印而来的图形上下文。
2.坐标系:
UIKit默认是左上角零点,y向下的坐标系。
而Quartz是默认左下角零点,y向上的坐标系。
一般情况下,对于我们经常使用的Bitmap Context,如果其对应一个视图,视图在屏幕上的就是y下坐标系,在屏幕外的是y上坐标系。
日常使用中需要注意:
UIGraphicsBeginImageContext
、UIGraphicsBeginImageContextWithOptions
:获取到的是y向下坐标系,文档中指出如果应用程序通过调用函数UIGraphicsBeginImageContextWithOptions
创建图形上下文,则UIKit将相同的变换应用于上下文的坐标系,就像UIView对象的图形上下文一样,所以是y向下的。CGContextDrawImage
:默认使用y上坐标系来绘图。CGBitmapContextCreate
:创建的bitmapContext默认是y上坐标系。虽然是y上坐标系,但是如果是单纯绘制图片,bitmapContext加载到UIKit视图上时会自动转换坐标系,所以不需要手动变换坐标。
3.再来看CGImage的创建:
CGImageRef __nullable CGImageCreate(
size_t width, size_t height,
size_t bitsPerComponent,
size_t bitsPerPixel,
size_t bytesPerRow,
CGColorSpaceRef cg_nullable space,
CGBitmapInfo bitmapInfo,
CGDataProviderRef cg_nullable provider,
const CGFloat * __nullable decode,
bool shouldInterpolate,
CGColorRenderingIntent intent
)CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
参数真的是多,一个一个看:
width,heigth:这个不用多想,就是图片的宽高,单位是像素。
bitsPerComponent:要知道在RGB中,颜色有4个分量(R、G、B、A),这个参数表示单个分量所占位数,再iOS上颜色空间
CGColorSpaceGetDeviceRGB()
对应的是8位。bitsPerPixel:表示一个像素占多少位 =
(bitsPerComponent * number of components + 7)/8 * 8
,RGB的一个像素由4个颜色分量组成,每个分量通常是8位,那么一般单个像素总共占32位(有些RGB没有A,不是8位)。如果某个空间中每个分量是5位,由4个分量组成,则一个像素占24位。bytesPerRow:表示一行像素的总字节数 =
width * (bitsPerPixel / BYTE_SIZE)
,注意这里是字节(通常1字节=8位)。space:颜色空间,比如颜色空间RGB中(1,0,0)表示红色,比如颜色空间BGR(1,0,0)表示蓝色,同一个颜色在不同颜色空间里有不同的表达式。需要手动release。
bitmapInfo:位图的布局信息,枚举,使用
(CGImageAlphaInfo | CGBitmapInfo)
。当在ARGB-32下,并且opaque是YES即不包含alpha时,使用(kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host)
表示没有alpha并跳过第一个颜色分量,位图排序使用“主机32位序”,这是一个宏,在iPhone中通常是‘小端32位序’。需要提一下UIGraphicsBeginImageContext
返回的context的颜色空间就是32位RGB,颜色分量8位。provider:数据提供者,这个主要是提供图片数据的。如果提供的是未解压到图片数据,那么创建的就是未解压的图片;如果提供的是解压后的数据,那么创建的就是解压后的图片。
-
decode:解码数组,这个解码与图片的解压是两回事,不要混淆了。图像的解码映射数组。如果您不想允许重新映射图像的颜色值,请传递NULL解码数组。对于图像颜色空间中的每个颜色分量(包括alpha分量),解码数组提供两组,即一对表示范围上限和下限的值。例如,RGB颜色空间中的源图像的解码数组总共包含六个分量,每组由红色,绿色和蓝色组成,左三个分量是最小值,右三个分量是最大值。渲染图像时,Core Graphics使用线性变换将原始组件值映射到指定范围内适合目标颜色空间的相对数字。
- 假设原色值是蓝色(0,0,1),decode设置{0.5,1,0.8},相当于{0.5,1,0.8,0,0,0},色值会先判断如果小于最小值,那么直接取最小值,再判断如果大于最大值,那么直接取最大值,那么得出的结论就是:色值(0,0,1)变成了(0.5, 1, 0)。
shouldInterpolate:一个布尔值,指定是否应进行插值。插值设置指定Core Graphics是否要对图像应用像素平滑算法。在没有插值的情况下,当在具有比图像数据更高分辨率的输出设备上绘制时,图像可能呈现锯齿状或像素化。
intent:渲染意图常量,渲染意图常量决定了将颜色从一个颜色空间映射到另一个颜色空间的确切方法。
CGImageAlphaInfo
透明度分量信息和CGBitmapInfo
位图布局信息:
typedef CF_ENUM(uint32_t, CGImageAlphaInfo) {
kCGImageAlphaNone, /* For example, RGB. */
kCGImageAlphaPremultipliedLast, /* For example, premultiplied RGBA */
kCGImageAlphaPremultipliedFirst, /* For example, premultiplied ARGB */
kCGImageAlphaLast, /* For example, non-premultiplied RGBA */
kCGImageAlphaFirst, /* For example, non-premultiplied ARGB */
kCGImageAlphaNoneSkipLast, /* For example, RBGX. */
kCGImageAlphaNoneSkipFirst, /* For example, XRGB. */
kCGImageAlphaOnly /* No color data, alpha data only */
};
typedef CF_OPTIONS(uint32_t, CGBitmapInfo) {
kCGBitmapAlphaInfoMask = 0x1F,
kCGBitmapFloatInfoMask = 0xF00,
kCGBitmapFloatComponents = (1 << 8),
kCGBitmapByteOrderMask = kCGImageByteOrderMask,
kCGBitmapByteOrderDefault = kCGImageByteOrderDefault,
kCGBitmapByteOrder16Little = kCGImageByteOrder16Little,
kCGBitmapByteOrder32Little = kCGImageByteOrder32Little,
kCGBitmapByteOrder16Big = kCGImageByteOrder16Big,
kCGBitmapByteOrder32Big = kCGImageByteOrder32Big
}
#ifdef __BIG_ENDIAN__
//[大端序](https://zh.wikipedia.org/wiki/%E5%AD%97%E8%8A%82%E5%BA%8F#.E5.A4.A7.E7.AB.AF.E5.BA.8F)
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Big
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Big
#else /* Little endian. */
//小端序
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Little
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Little
#endif
CGColorRenderingIntent
绘制意图:
/* Color rendering intents. */
typedef CF_ENUM (int32_t, CGColorRenderingIntent) {
kCGRenderingIntentDefault,
kCGRenderingIntentAbsoluteColorimetric,
kCGRenderingIntentRelativeColorimetric,
kCGRenderingIntentPerceptual,
kCGRenderingIntentSaturation
};
kCGRenderingIntentDefault
:默认渲染意图。kCGRenderingIntentAbsoluteColorimetric
:绝对色度渲染意图。将输出设备颜色域外的颜色映射为输出设备域内与之最接近的颜色。这可以产生一个裁减效果,因为色域外的两个不同的颜色值可能被映射为色域内的同一个颜色值。当图形使用的颜色值同时包含在源色域及目标色域内时,这种方法是最好的。常用于logo或者使用专色(spot color)时。kCGRenderingIntentRelativeColorimetric
:相对色度渲染意图。转换所有的颜色(包括色域内的),以补偿图形上下文的白点与输出设备白点之间的色差。kCGRenderingIntentPerceptual
:可感知渲染意图。通过压缩图形上下文的色域来适应输出设备的色域,并保持源颜色空间的颜色之间的相对性。感知渲染意图适用于相片及其它复杂的高细度图片。kCGRenderingIntentSaturation
:饱和度渲染意图。把颜色转换到输出设备色域内时,保持颜色的相对饱和度。结果是包含亮度、饱和度颜色的图片。饱和度意图适用于生成低细度的图片,如描述性图表。
4.CGBitmapContext的创建:
CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(
void * __nullable data,
size_t width, size_t height,
size_t bitsPerComponent,
size_t bytesPerRow,
CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo
)CG_AVAILABLE_STARTING(10.0, 2.0);
在之前介绍过的参数就不详细说明了。
data:用来制定位图存储空间,保存位图数据。如果设置为NULL,那么系统会自动分配一段合适的内存并且bytesPerRow也可以设置为0;如果不为空,那么bytesPerRow也不能为0,需要填写相应的数值,而且要记得free(data)。
width,height:位图的大小,一般设置为将要绘制的图片的大小。
bitsPerComponent:单个颜色分量所占位数。
btyesPerRow:每行所占总字节。当data参数设置为NULL,bytesPerRow设置为0,那么系统就会自动计算每行的总字节数。
space:颜色空间。
bitmapInfo:位图的布局信息。
颜色空间space
下图是苹果支持的颜色空间和像素格式:
颜色空间iOS支持上述的其中8种, iOS不支持与设备无关或通用的颜色空间。根据Quartz 2D的文档所说,iOS应用程序必须使用设备颜色空间。
设备颜色空间主要由iOS应用程序使用,因为其他选项不可用。在大多数情况下,Mac OS X应用程序应使用通用颜色空间,而不是创建设备颜色空间,但是,一些Quartz例程要求具有设备颜色空间的图像,例如,如果调用CGImageCreateWithMask并指定图像作为蒙版,则必须使用设备灰色颜色空间定义图像。
-
CGColorSpaceCreateDeviceGray()
用来创建设备灰色颜色空间 -
CGColorSpaceCreateDeviceRGB()
用来创建设备RGB颜色空间 -
CGColorSpaceCreateDeviceCMYK()
用来创建设备CMYK颜色空间
所以iOS中,我们通常使用CGColorSpaceCreateDeviceRGB()
来创建颜色空间,还有bitsPerComponent和btyesPerRow要和颜色空间匹配。
三、图片的解压
这是一张30x30的图,存储大小843B,
UIImage *image = [UIImage imageNamed:@"check_green.png"];
CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
上面的代码可以获得图片解压后的大小,为3600B。解压缩后的图片大小与原始文件大小之间没有任何关系,而只与图片的像素有关:
解压缩后的图片大小 = 图片的像素宽 30 * 图片的像素高 30 * 每个像素所占的字节数 4
使用+imageNamed:
方法进行加载并解压图片,其实内部就是使用的ImageIO框架。
1.使用ImageIO库来加载本地image:使用kCGImageSourceShouldCache
来创建图片,强制图片立刻解压缓存,然后在图片的生命周期保留解压后的版本。
NSString *str = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/image"];
CGDataProviderRef provider = CGDataProviderCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:str]);
NSDictionary *options = @{(__bridge id)kCGImageSourceShouldCache: @YES};
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source1, 0,(__bridge CFDictionaryRef)options);
UIImage *image = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
CFRelease(source);
绘制图片过程中,系统会对图片自动解压,所以绘制图片也算是一种解压的方式。
2.使用UIGraphicsBeginImageContext
进行绘制图片,创建一个基于bitmap的imageContext,并把它设置成为当前正在使用的context,然后将图片绘制到context上。这方法在需要大量调用的时候不要用
,因为是系统释放内存,所以存在延迟释放的情况,短时间内大量调用(如tableView的滑动异步绘制图片)会出现内存暴涨的情况:
//创建一个基于bitmap的imageContext,并把位图立即推入到图形上下文堆栈中。坐标系是UIKit默认坐标系左上角0点y下。
UIGraphicsBeginImageContextWithOptions(CGSizeMake(160, 220), YES, [UIScreen
mainScreen].scale);
//这里要记住CGContextDrawImage会默认使用离屏bitmap的坐标系即左下角0点y上,所以要变换坐标。
//CGContextRef ctx = UIGraphicsGetCurrentContext();
//CGContextTranslateCTM(ctx, 0, 220);
//CGContextScaleCTM(ctx, 1.0, -1.0);
//CGContextDrawImage(ctx, CGRectMake(0, 0, 160, 220), img.CGImage);
[image drawInRect:CGRectMake(0, 0, width, height)];
UIGraphicsEndImageContext();
3.使用CGBitmapContextCreate
绘制图片,同方法1中绘制比较,根据官方文档说明,因为bitmap context是左下角y上坐标系,如果实际绘画中需要转换坐标系,不一定比imagContext绘制快。但是我们其实大部分都是在屏幕外绘制图片的,所以不需要变换坐标,实际证明比方法UIGraphicsBeginImageContext
绘制要快。
官方文档上示例代码:
//Creating a bitmap graphics context
CGImageRef imgRef = image.CGImage;
size_t pixelsWide = CGImageGetWidth(imgRef);
size_t pixelsHigh = CGImageGetHeight(imgRef);
CGContextRef context = NULL;
CGColorSpaceRef colorSpace;
void * bitmapData;
int bitmapByteCount;
int bitmapBytesPerRow;
bitmapBytesPerRow = (pixelsWide * 4);
bitmapByteCount = (bitmapBytesPerRow * pixelsHigh);
colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
void * bitmapData;
bitmapData = calloc( bitmapByteCount, sizeof(uint8_t) );
if (bitmapData == NULL) {
fprintf (stderr, "Memory not allocated!");
return nil;
}
context = CGBitmapContextCreate (bitmapData,
pixelsWide,
pixelsHigh,
8, // bits per component
bitmapBytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedLast);
if (context == NULL) {
free (bitmapData);
fprintf (stderr, "Context not created!");
return NULL;
}
CGColorSpaceRelease( colorSpace );
return context;
//Drawing to a bitmap graphics context
CGRect myBoundingBox;// 1
myBoundingBox = CGRectMake (0, 0, myWidth, myHeight);// 2
myBitmapContext = MyCreateBitmapContext (400, 300);// 3
// ********** Your drawing code here ********** // 4
CGContextSetRGBFillColor (myBitmapContext, 1, 0, 0, 1);
CGContextFillRect (myBitmapContext, CGRectMake (0, 0, 200, 100 ));
CGContextSetRGBFillColor (myBitmapContext, 0, 0, 1, .5);
CGContextFillRect (myBitmapContext, CGRectMake (0, 0, 100, 200 ));
myImage = CGBitmapContextCreateImage (myBitmapContext);// 5
CGContextDrawImage(myContext, myBoundingBox, myImage);// 6
char *bitmapData = CGBitmapContextGetData(myBitmapContext); // 7
CGContextRelease (myBitmapContext);// 8
if (bitmapData) free(bitmapData); // 9
CGImageRelease(myImage);
上面的代码用到了bitmapData,一定要记得手动释放。实际使用的时候,我们一般data输入NULL就好,bitmapBytesPerRow设置为0,这样系统会自动为我们分配位图存储内存,并自动计算每行字节数。
下面是我们通常使用bitmap context来绘制图片的代码:
+ (UIImage *)imageDecodeByBitmap:(UIImage *)image
{
//创建一个bitmap的context,并把它设置成为当前正在使用的context
CGImageRef imgRef = image.CGImage;
size_t width = CGImageGetWidth(imgRef);
size_t height = CGImageGetHeight(imgRef);
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imgRef);
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
CGBitmapInfo info = kCGBitmapByteOrder32Host | (hasAlpha ? kCGImageAlphaPremultipliedFirst: kCGImageAlphaNoneSkipFirst);
//创建一个bitmap的context
CGContextRef ctx = CGBitmapContextCreate(NULL, width, height, 8, 0, SharedCGColorSpaceGetDeviceRGB(), info);
CGContextDrawImage(ctx, CGRectMake(0, 0, width, height), imgRef);
CGImageRef cgImage = CGBitmapContextCreateImage(ctx);
CGContextRelease(ctx);
image = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);
return image;
}
4.使用CGDataProviderCopyData解压,然后使用CGImageCreate重新创建解压后的图片。
CGImageRef imageRef = image.CGImage;
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
CGColorSpaceRef space = CGImageGetColorSpace(imageRef);
size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
size_t bitsPerPixel = CGImageGetBitsPerPixel(imageRef);
size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
CGDataProviderRef provider = CGImageGetDataProvider(imageRef);
CFDataRef rawData = CGDataProviderCopyData(provider); //解压
provider = CGDataProviderCreateWithCFData(rawData);
CFRelease(rawData);
imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, provider, NULL, false, kCGRenderingIntentDefault);
CGDataProviderRelease(provider);
UIImage *newImage = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
这是一个单纯的解压方法,不像前面几个方法都是绘制图片,不过这个方法也有个瑕疵,在使用过程中,会出现内存不断增加的情况:进入这个视图控制器内存增加一些,pop出去后再进来又增加一些,不断增长,可能从原本的100M内存增加到800M,然后就会回落到100M,猜测可能是系统没有释放位图数据缓存。
四、图片的渲染
正常我们在使用imageView.image = image;
的时候,在屏幕显示的时候,GPU需要绘制图片,绘制就要渲染图片,然后才能显示出来。当然我们这里说的渲染图片是提前渲染和异步渲染。
1.把整张图片绘制到CGContext中,然后获取到context中的newImage并保存下来,原图片就可以不用了,加载的时候需要加载newImage,这里代码就是在解压栏目里我讲的“绘制图片”代码。
这种方法多在tableView和collectionView上使用,异步绘制图片然后回到主线程中加载,可以使滑动非常的流畅,这种操作主要是把CPU的同步解压操作换成异步操作,并且把GPU的渲染换成了CPU的离屏渲染,具体方法在“解压图片”栏目中给出的2、3两点。
2.把整个图片只绘制到一个像素大小的bitmap Context中。
CGImageRef cgImage = image.CGImage;
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
if (width == 0 || height == 0) return;
size_t bitsPerComponent = 8;
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage);
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
CGBitmapInfo info = kCGBitmapByteOrder32Host | (hasAlpha ? kCGImageAlphaPremultipliedFirst: kCGImageAlphaNoneSkipFirst);
CGContextRef context = CGBitmapContextCreate(NULL, 1, 1, bitsPerComponent, 0, SharedCGColorSpaceGetDeviceRGB(), info);
//解压渲染,但是没有优化
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
CGContextRelease(context);
这样解压整张图片并提前渲染,好处是只绘制了一个像素,绘制没有消耗任何时间。这样没有解压后的图片,只能加载原图,解压数据和渲染数据都在系统缓存中。
这种方法一般用于静态页面中加载图片。正因为只加载原图,导致原图如果被释放,解压数据等在缓存中也会释放,而且正因为依赖于缓存,如果缓存被释放,那么这个方法相当于无效操作。
实际使用中,
- 如果加载的是本地图片,不大且重复使用,那最好还是使用+imageNamed:系统会做解压缓存处理;
- 如果加载本地大图,或者就一次使用的图片,那么最好用
+imageWithContentsOfFile:
加载图片; - 对于滑动显示大量图片的页面,把整张图片异步绘制到CGContext可能是更好的选择。
这个demo是写这边文章时写的AsynDisplayImage,有兴趣可以看下。