图片的编码:在当前APP的开发中,图片是常常会使用到的,关于图片有不少种格式,例如JPEG
,PNG
等。其实这些各类各样的图片格式都对应了位图(bitmap)通过不一样算法编码(压缩)后的图片。(编码这里就不过多介绍了)html
图片的解码:app从磁盘中读入编码后的图片,须要通过解码把图片变成位图(bitmap)读入,这样才能显示在屏幕上。ios
位图(bitmap):位图又被叫作点阵图像,也就是说位图包含了一大堆的像素点信息,这些像素点就是该图片中的点,有了图片中每一个像素点的信息,就能够在屏幕上渲染整张图片了。算法
图片本质上是位图,一堆像素点组成的二维数组,其中每一个像素点都记录该点位的颜色等信息。显示出来就是一张图了。数组
既然像素要存储颜色数据,这里就又引出一个颜色存储格式的概念。咱们就以最简单广泛的32-bit RGBA 色彩存储格式为例子,他的意思是一个像素点存储的色彩所需空间是32bits或是4bytes,1byte或8bit存储是一个通道,对应下来就是:bash
- R = red (占1byte或8bit)
- G = green (占1byte或8bit)
- B = blue (占1byte或8bit)
- A = alpha (占1byte或8bit)
这样你就知道 32-bit RGBA 格式可以显示的颜色是 2^8 * 2^8* 2^8 (256 * 256 * 256),将近一千七百多万个颜色。还有颜色空间(Color Spaces)的概念这里就再也不扩展了。session
而位图是装载像素点的数组,这样你大概能够理解下一张普通位图包含着多少数据!同时,这里解释颜色是为了下面计算位图大小,便于理解咱们为何要进行图片编码。app
经过iOS - 图形高级处理 (1、图片显示相关理论)的学习能够知道,图片的解压缩是一个很是耗时的 CPU 操做,而且它默认是在主线程中执行的。那么当须要加载的图片比较多时,就会对咱们应用的响应性形成严重的影响,尤为是在快速滑动的列表上,这个问题会表现得更加突出。
既然如此,图片不编码也就不用解码,都使用位图能够吗?这写在这里的确是明知故问的问题,下面就解释下为何必须对图片进行编解码操做。框架
一张位图的宽和高分别都是100个像素,那这个位图的大小是多少呢?ide
//计算一张位图size的公式 //bytesPerPixel每一个像素点所需空间 //32-bit RGBA 格式图片 bytesPerPixel = 4 (R,G,B,A各一个byte),理论看上面 size = width * height * bytesPerPixel 复制代码
这样把咱们100x100 的位图代入该公式,能够获得其大小:函数
size = 100 * 100 * 4 = 40000B = 39KB 复制代码
正常一张PNG或JPEG格式的100x100的图片,大概只有几KB。若是更大的图,位图所占空间更大,因此位图必须进行编码进行存储。
这里不过多介绍了,苹果提供2种图片编码格式,PNG和JPEG:
PNG 是无损压损,JPEG能够是有损压缩(0-100% ),即损失部分信息来压缩图片,这样压缩以后的图片大小将会更小。
// return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format UIKIT_EXTERN NSData * __nullable UIImagePNGRepresentation(UIImage * __nonnull image); // return image as JPEG. May return nil if image has no CGImageRef or invalid bitmap format. compression is 0(most)..1(least) UIKIT_EXTERN NSData * __nullable UIImageJPEGRepresentation(UIImage * __nonnull image, CGFloat compressionQuality); 复制代码
编码后的图片须要显示在屏幕上,咱们须要得到图片全部信息,也就是对应编码前的位图。因此编码后的图片必需要通过解码才能正常显示。
Buffer 表示一片连续的内存空间。在这里,咱们说的 Buffer 是指一系列内部结构相同、大小相同的元素组成的内存区域。有三种Buffer:Data Buffer、Image Buffer、Frame Buffer。这个理论是2018WWDC苹果上描述的概念,具体可看Image and Graphics Best Practices
- Data Buffer 是指存储在内存中的原始数据,图像可使用不一样的格式保存,如 jpg、png。Data Buffer 的信息不能用来描述图像的位图像素信息。
- Image Buffer 是指图像在内存中的存在方式,其中每一个元素描述了一个像素点。Image Buffer 的大小和位图的大小相等。
- Frame Buffer 和 Image Buffer 内容相同,不过其存储在 vRAM(video RAM)中,而 Image Buffer 存储在 RAM 中。
图片解码过程:
一、假如在本地沙盒下有一张 JPEG 格式的图片或项目资源中读入通常都这么作
UIImageView *imageView = ...; // UIImage *image = [UIImage imageNamed:@"xxx"]; UIImage *image = [UIImage imageWithContentsOfFile:@"xxx.JPG"]; imageView.image = image; 复制代码
二、UIImage 是 iOS 中处理图像的高级类。建立一个 UIImage 实例只会加载 Data Buffer;也就是说以上只是把图片转为UIImage对象,该对象存储在Data Buffer里。此时并无对图片进行解码。
三、当将图像显示到屏幕上会触发隐式解码。(必须同时知足图像被设置到 UIImageView 中、UIImageView 添加到视图,才会触发图像解码。)也就是说你就算实例了一个UIImageView,可是没有把他addSubview,显示到视图上,系统也是不会进行解码的。
现实问题产生:
这个解码过程默认是发生在主线程上面的,并且很是消耗 CPU,因此到若是在 tableView 或者 collectionView 中有至关多的图片须要显示的话,这些图片在主线程的解码操做必然会影响滑动的顺畅度。因此咱们是否能够在子线程强制将其解码,而后在主线程让系统渲染解码以后的图片呢?固然能够,如今基本上全部的开源图片库都会实现这个操做。例如:YYImage\SDWebImage。
现实中解决方式:
本身手动解码的原理就是对图片进行从新绘制,获得一张新的解码后的位图。其中,用到的最核心的函数是 CGBitmapContextCreate :
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(__MAC_10_0, __IPHONE_2_0); 复制代码
这个方法是建立一个图片处理的上下文 CGContext 对象,由于上面方法的返回值 CGContextRef 实际上就是 CGContext *。关于这个函数的详细讲解博文有不少,官方文档CGBitmapContextCreate。博客文章,图片解码。
开源框架的解决方案基础也是基于这个API:
一、YYImage 中解码的代码:
CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) { if (!imageRef) return NULL; size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); if (width == 0 || height == 0) return NULL; if (decodeForDisplay) { //decode with redraw (may lose some precision) CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask; BOOL hasAlpha = NO; if (alphaInfo == kCGImageAlphaPremultipliedLast || alphaInfo == kCGImageAlphaPremultipliedFirst || alphaInfo == kCGImageAlphaLast || alphaInfo == kCGImageAlphaFirst) { hasAlpha = YES; } // BGRA8888 (premultiplied) or BGRX8888 // same as UIGraphicsBeginImageContext() and -[UIView drawRect:] CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host; bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo); if (!context) return NULL; CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode CGImageRef newImage = CGBitmapContextCreateImage(context); CFRelease(context); return newImage; } else { ... } } 复制代码
实际上, 这个方法的做用是建立一个图像的拷贝,它接受一个原始的位图参数 imageRef ,最终返回一个新的解码后的位图 newImage ,中间主要通过了如下三个步骤:
- 使用 CGBitmapContextCreate 函数建立一个位图上下文;
- 使用 CGContextDrawImage 函数将原始位图绘制到上下文中;
- 使用 CGBitmapContextCreateImage 函数建立一张新的解压缩后的位图。
事实上,SDWebImage 中对图片的解压缩过程与上述彻底一致,只是传递给 CGBitmapContextCreate 函数的部分参数存在细微的差异
二、SDWebImage的解码实现
+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image { if (![UIImage shouldDecodeImage:image]) { return image; } // autorelease the bitmap context and all vars to help system to free memory when there are memory warning. // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory]; @autoreleasepool{ CGImageRef imageRef = image.CGImage; CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef]; size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); size_t bytesPerRow = kBytesPerPixel * width; // kCGImageAlphaNone is not supported in CGBitmapContextCreate. // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast // to create bitmap graphics contexts without alpha info. CGContextRef context = CGBitmapContextCreate(NULL, width, height, kBitsPerComponent, bytesPerRow, colorspaceRef, kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast); if (context == NULL) { return image; } // Draw the image into the context and retrieve the new bitmap image without alpha CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context); UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation]; CGContextRelease(context); CGImageRelease(imageRefWithoutAlpha); return imageWithoutAlpha; } } + (BOOL)shouldDecodeImage:(nullable UIImage *)image { // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error if (image == nil) { return NO; } // do not decode animated images if (image.images != nil) { return NO; } CGImageRef imageRef = image.CGImage; CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef); BOOL anyAlpha = (alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast); // do not decode images with alpha if (anyAlpha) { return NO; } return YES; } 复制代码
SDWebImage 中和其余不同的地方,就是若是一张图片有 alpha 份量,那就直接返回原始图片,再也不进行解码操做。这么作是由于alpha 份量不可知,为了保证原图完整信息故不作处理。
SDWebImage 在解码操做外面包了 autoreleasepool,这样在大量图片须要解码的时候,可使得局部变量尽早释放掉,不会形成内存峰值太高。
大图显示这个问题,看似和图片编码解码无关。可是大的图片会占用较多的内存资源,解码和传输到 GPU 也会耗费较多时间。 所以,实际须要显示的图像尺寸可能并非很大,若是能将大图缩小,便能达到优化的目的。如下是WWDC给的大图显示方案,功能是缩小图像并解码:
// 大图缩小为显示尺寸的图
- (UIImage *)downsampleImageAt:(NSURL *)imageURL to:(CGSize)pointSize scale:(CGFloat)scale {
// 利用图像文件地址建立 image source
NSDictionary *imageSourceOptions =
@{
(__bridge NSString *)kCGImageSourceShouldCache: @NO // 原始图像不要解码
};
CGImageSourceRef imageSource =
CGImageSourceCreateWithURL((__bridge CFURLRef)imageURL, (__bridge CFDictionaryRef)imageSourceOptions);
// 下采样
CGFloat maxDimensionInPixels = MAX(pointSize.width, pointSize.height) * scale;
NSDictionary *downsampleOptions =
@{
(__bridge NSString *)kCGImageSourceCreateThumbnailFromImageAlways: @YES,
(__bridge NSString *)kCGImageSourceShouldCacheImmediately: @YES, // 缩小图像的同时进行解码
(__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform: @YES,
(__bridge NSString *)kCGImageSourceThumbnailMaxPixelSize: @(maxDimensionInPixels)
};
CGImageRef downsampledImage =
CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (__bridge CFDictionaryRef)downsampleOptions);
UIImage *image = [[UIImage alloc] initWithCGImage:downsampledImage];
CGImageRelease(downsampledImage);
CFRelease(imageSource);
return image;
}
复制代码
// Downsampling large images for display at smaller size
func downsample(imageAt imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage {
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions)!
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let downsampleOptions =
[kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary
let downsampledImage =
CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)!
return UIImage(cgImage: downsampledImage)
}
复制代码