iOS-图形高级处理(二、图片的编码解码)
2020/7/4 23:56:14
本文主要是介绍iOS-图形高级处理(二、图片的编码解码),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
前言
图片的编码:在当前APP的开发中,图片是经常会使用到的,关于图片有很多种格式,例如JPEG
,PNG
等。其实这些各种各样的图片格式都对应了位图(bitmap)经过不同算法编码(压缩)后的图片。(编码这里就不过多介绍了)
图片的解码:app从磁盘中读入编码后的图片,需要经过解码把图片变成位图(bitmap)读入,这样才能显示在屏幕上。
位图(bitmap):位图又被叫做点阵图像,也就是说位图包含了一大堆的像素点信息,这些像素点就是该图片中的点,有了图片中每个像素点的信息,就可以在屏幕上渲染整张图片了。
一、图片的本质
图片本质上是位图,一堆像素点组成的二维数组,其中每个像素点都记录该点位的颜色等信息。显示出来就是一张图了。
既然像素要存储颜色数据,这里就又引出一个颜色存储格式的概念。我们就以最简单普遍的32-bit RGBA 色彩存储格式为例子,他的意思是一个像素点存储的色彩所需空间是32bits或是4bytes,1byte或8bit存储是一个通道,对应下来就是:
- 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)的概念这里就不再扩展了。
而位图是装载像素点的数组,这样你大概可以理解下一张普通位图包含着多少数据!同时,这里解释颜色是为了下面计算位图大小,便于理解我们为什么要进行图片编码。
二、位图需要编码本质
通过iOS - 图形高级处理 (一、图片显示相关理论)的学习可以知道,图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。
既然如此,图片不编码也就不用解码,都使用位图可以吗?这写在这里的确是明知故问的问题,下面就解释下为什么必须对图片进行编解码操作。
1、进行位图编码的原因:
一张位图的宽和高分别都是100个像素,那这个位图的大小是多少呢?
//计算一张位图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、位图编码技术:
这里不过多介绍了,苹果提供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); 复制代码
三、图片需要解码的本质及项目中实际应用
1、编码后的图片需要解码的原因:
编码后的图片需要显示在屏幕上,我们需要获得图片所有信息,也就是对应编码前的位图。所以编码后的图片必须要经过解码才能正常显示。
2、2018WWDC 中说的三种 Buffer 理念:
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 中。
3、图片读入解码的过程:(部分图片读入理论可参考图片显示相关理论)
图片解码过程:
1、假如在本地沙盒下有一张 JPEG 格式的图片或项目资源中读入一般都这么做
UIImageView *imageView = ...; // UIImage *image = [UIImage imageNamed:@"xxx"]; UIImage *image = [UIImage imageWithContentsOfFile:@"xxx.JPG"]; imageView.image = image; 复制代码2、UIImage 是 iOS 中处理图像的高级类。创建一个 UIImage 实例只会加载 Data Buffer;也就是说以上只是把图片转为UIImage对象,该对象存储在Data Buffer里。此时并没有对图片进行解码。
3、当将图像显示到屏幕上会触发隐式解码。(必须同时满足图像被设置到 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:
1、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 函数的部分参数存在细微的差别
2、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给的大图显示方案,功能是缩小图像并解码:
1、Objective-C:
// 大图缩小为显示尺寸的图 - (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; } 复制代码
2、Swift
// 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) } 复制代码
参考文档
WWDC2018
Image and Graphics Best Practices
探讨iOS 中图片的解压缩到渲染过程
iOS 图片的解压缩
这篇关于iOS-图形高级处理(二、图片的编码解码)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值