第三篇的写在前面
SDWebImage提供了一个用于图片解码的类——SDWebImageDecoder。在上一篇文章中,也有提及到在diskImageForKey
方法中使用了decoder类的decodedImageWithImage:image
方法对图片进行解压缩后返回。本篇文章则重点分析这个模块的源码。
图片解码功能的实现依赖于Quartz 2D的图像处理库,如果对这些功能不熟悉的话可以参考一下Quartz 2D Programming Guide。本文章也会对一些知识点进行简单的讲解。
为何需要对图片进行解码
Avoiding Image Decompression Sickness这篇文章中描述了一种情况:
Imagine you have a UIScrollView that displays UIImageViews for the individual pages of a catalog or magazine style app. As soon as even one pixel of the following page comes on screen you instantiate (or reuse) a UIImageView and pop it into the scroll view’s content area. That works quite well in Simulator, but when you test this on the device you find that every time you try to page to the next page, there is a noticeable delay. This delay results from the fact that images need to be decompressed from their file incarnation to be rendered on screen. Unfortunately UIImage does this decompression at the very latest possible moment, i.e. when it is to be displayed.
因此,可以假设一种最简单为一个UIImageView获取网络图片的流程:
- 从网络上请求到压缩过的图片(JPEG,PNG...)
- 使用这个压缩过的图片对UIImage对象进行初始化
- 当UIImage要被显示到UIImageView上面的时候,UIImage上的图片会被解压缩,然后显示到UIImageView上。
所以如何将这个解压缩的过程提前,文章中指出了几种思路:
Then there’s the question of “How fast can I get these pixels on screen?”. The answer to this is comprised of 3 main time intervals:
- time to alloc/init the UIImage with data on disk
- time to decompress the bits into an uncompressed format
- time to transfer the uncompressed bits to a CGContext, potentially resizing, blending, anti-aliasing it
SDWebImage中使用以下策略:
- 当图片从网络中获取到的时候就进行解压缩。(未来会提到)
- 当图片从磁盘缓存中获取到的时候立即解压缩。(上面已经提到了)
在这篇文章中总结了为什么我们需要解码:
在我们使用 UIImage 的时候,创建的图片通常不会直接加载到内存,而是在渲染的时候再进行解压并加载到内存。这就会导致 UIImage 在渲染的时候效率上不是那么高效。为了提高效率通过
decodedImageWithImage
方法把图片提前解压加载到内存,这样这张新图片就不再需要重复解压了,提高了渲染效率。这是一种空间换时间的做法。
接下来通过源码对这个解码过程进行分析。
DecodeWithImage 方法
这个方法传入一副图片对该图片进行解码,解码结果是另一幅图片。
static const size_t kBytesPerPixel = 4;
static const size_t kBitsPerComponent = 8;
+ (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];
//新建自动释放池,将bitmap context和临时变量都添加到池中在方法末尾自动释放以防止内存警告
@autoreleasepool{
//获取传入的UIImage对应的CGImageRef(位图)
CGImageRef imageRef = image.CGImage;
//获取彩色空间
CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];
//获取高和宽
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
//static const size_t kBytesPerPixel = 4
// 每个像素占4个字节大小 共32位 (RGBA)
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.
//初始化bitmap graphics context 上下文
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
//将CGImageRef对象画到上面生成的上下文中,且将alpha通道移除
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
//使用上下文创建位图
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
//从位图创建UIImage对象
UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
scale:image.scale
orientation:image.imageOrientation];
//释放CG对象
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
return imageWithoutAlpha;
}
}
简单来说,就是把UIImage绘制出来的图像再保存起来就完成了这个解码的过程。如果不考虑性能损耗,我们甚至可以用以下代码完成这个任务:
- (void)decompressImage:(UIImage *)image
{
UIGraphicsBeginImageContext(CGSizeMake(1, 1));
[image drawAtPoint:CGPointZero];
UIGraphicsEndImageContext();
}
关于CGImageRef
下面引用苹果开发者文档中的描述:
- Bitmap images and image masks are like any drawing primitive in Quartz. Both images and image masks in Quartz are represented by the CGImageRef data type.A bitmap image (or sampled image) is an array of pixels (or samples).
- Each pixel represents a single point in the image. JPEG, TIFF, and PNG graphics files are examples of bitmap images.
- Each sample in a bitmap contains one or more color components in a specified color space, plus one additional component that specifies the alpha value to indicate transparency. Each component can be from 1 to as many as 32 bits.
CGImageRef就是位图(bitmap image)在Quartz 框架中的具体数据结构。位图(样本)是像素的矩形阵列(Rectangular Array),每个像素对应在特定的彩色空间(color space)中的一个或多个彩色元素。关于彩色空间,请参考苹果开发者文档中的Color Management Guide。一般我们常用有灰度空间(Gray Spaces)和RGB空间。
关于Bitmap Graphics Context
Bitmap Graphics Context即位图上下文。用于接收存储了位图数据的缓存的指针,当我们位图上下文进行绘制时,缓存会进行更新。
A bitmap graphics context accepts a pointer to a memory buffer that contains storage space for the bitmap. When you paint into the bitmap graphics context, the buffer is updated. After you release the graphics context, you have a fully updated bitmap in the pixel format you specify.
因此,当我们需要自己绘制一个bitmap图片时,只需要初始化一个位图上下文,并在上面绘制自己的图形,最后从上下文中获取我们想要的bitmap图形或者数据即可。
上面说了这么多,其实就是为了解释decodedImageWithImage:image
方法对原始图片进行了什么样的操作——将图片原始图片绘制到位图上下文,然后将位图上下文保存为新的位图后返回。
图片压缩
SDWebImageDecoder还提供了另外一个核心功能——图片压缩。如果图片的体积大于特定值,则decoder会对图片进行压缩,防止内存溢出。这部分源码比较长,其中的压缩的算法稍微有些复杂,需要仔细阅读。下面先给出源码,再做具体的说明。
*
* Defines the maximum size in MB of the decoded image when the flag `SDWebImageScaleDownLargeImages` is set
该参数用于设置内存占用的最大字节数。默认为60MB,下面给出了一些旧设备的参考数值。如果图片大小大于该值,则将图片以该数值为目标进行压缩。
* Suggested value for iPad1 and iPhone 3GS: 60.
* Suggested value for iPad2 and iPhone 4: 120.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 30.
*/
static const CGFloat kDestImageSizeMB = 60.0f;
/*
* Defines the maximum size in MB of a tile used to decode image when the flag `SDWebImageScaleDownLargeImages` is set
设置压缩时对于源图像使用到的*块*的最大字节数。
* Suggested value for iPad1 and iPhone 3GS: 20.
* Suggested value for iPad2 and iPhone 4: 40.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 10.
*/
static const CGFloat kSourceImageTileSizeMB = 20.0f;
/**下面做算术题*/
//1MB中的字节数
static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
//1MB大小图像中有多少个像素
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;
//压缩的目标图像的像素点个数
static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB;
//源图像*块*中有多少个像素
static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB;
//一个常量,具体的语义不必纠结,用于后面压缩算法
static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet.
+ (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image {
//1. 先对图片解码
if (![UIImage shouldDecodeImage:image]) {
return image;
}
//2. 判断是否需要压缩(以上面kDestImageSizeMB为标准)
if (![UIImage shouldScaleDownImage:image]) {
return [UIImage decodedImageWithImage:image];
}
//3. 声明压缩目标用的上下文
CGContextRef destContext;
// 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 {
//4. 获取源图像位图
CGImageRef sourceImageRef = image.CGImage;
//5. 源图像尺寸,存储在CGSize结构体中
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
//6. 计算源图像总的像素点个数
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
//7. 获取原图像和目标图像的比例(以像素点个数为基准)
float imageScale = kDestTotalPixels / sourceTotalPixels;
//8. 使用scale计算目标图像的宽高
CGSize destResolution = CGSizeZero;
destResolution.width = (int)(sourceResolution.width*imageScale);
destResolution.height = (int)(sourceResolution.height*imageScale);
//9. 进行图像绘制前的准备工作
// current color space
CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:sourceImageRef];
size_t bytesPerRow = kBytesPerPixel * destResolution.width;
// Allocate enough pixel data to hold the output image.
void* destBitmapData = malloc( bytesPerRow * destResolution.height );
if (destBitmapData == NULL) {
return image;
}
// 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.
destContext = CGBitmapContextCreate(destBitmapData,
destResolution.width,
destResolution.height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (destContext == NULL) {
free(destBitmapData);
return image;
}
//10. 设置图像插值的质量为高
CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
//11. 定义一个称为*块*的增量矩形(incremental blits,即矩形大小在每一次迭代后都不断增长/减小)用于计算从源图像到目标图像的输出。
//*块*的宽度和图片的宽度保持一致,高度动态变化
CGRect sourceTile = CGRectZero;
sourceTile.size.width = sourceResolution.width;
//11.1 *块*的计算:根据宽度计算动态的高度
sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width );
//11.2 *块*的起始x值总是为0
sourceTile.origin.x = 0.0f;
// 12. 同样的方式初始化目标图像的块
//宽度 = 目标图像的宽度
//高度 = 源图像块的高度 * 缩放比例
CGRect destTile;
destTile.size.width = destResolution.width;
destTile.size.height = sourceTile.size.height * imageScale;
destTile.origin.x = 0.0f;
// The source seem overlap is proportionate to the destination seem overlap.
// this is the amount of pixels to overlap each tile as we assemble the ouput image.
//13. 根据kDestSeemOverlap计算源块的SeemOverlap常数
// 计算公式: sourceSeemOverlap = (int)kDestSeemOverlap / imageScale
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
//14. 声明源图像块的位图,在循环中绘制在destContext中
CGImageRef sourceTileImageRef;
// calculate the number of read/write operations required to assemble the
// output image.
//15. 计算循环次数
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
// 如果不能整除,有余数,则循环次数+1
// 余数记录下来
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
if(remainder) {
iterations++;
}
//16. 将overlap常量累加到块的高度中,保存源图像块的高度到sourceTileHeightMinusOverlap
float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height += sourceSeemOverlap;
destTile.size.height += kDestSeemOverlap;
//17. 核心部分,开始循环做插值
for( int y = 0; y < iterations; ++y ) {
@autoreleasepool {
//1. 每次循环sourceTile的坐标原点y值 + sourceTileHeightMinusOverlap
//所以sourceTileHeightMinusOverlap在此作为固定增量存在
sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
//2. destTile的坐标原点y值 = 目标图像的高度 - 固定增量
destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
//3. 使用sourceTile矩形内的源图像初始化sourceTileImageRef
sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
//最后一次循环
if( y == iterations - 1 && remainder ) {
float dify = destTile.size.height;
destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
dify -= destTile.size.height;
destTile.origin.y += dify;
}
//4. 将sourceTileImageRef绘制到destTile矩形的destConext上下文
// 注意上面我们为destContext设置了插值质量,此时图像会进行缩放,因此会进行插值操作
CGContextDrawImage( destContext, destTile, sourceTileImageRef );
//5. 释放临时变量
CGImageRelease( sourceTileImageRef );
}
}
//18. 收尾工作,绘制图片,返回UIImage对象
CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
CGContextRelease(destContext);
if (destImageRef == NULL) {
return image;
}
UIImage *destImage = [UIImage imageWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(destImageRef);
if (destImage == nil) {
return image;
}
return destImage;
}
}
压缩算法说明
上述方法内部描述了一个比较隐晦的图像压缩算法(是的,没有用现成的库调用,所以我称之为“隐晦”,我猜想可能是为了压缩代码量)。首先在上面提到了,位图其实是由像素组成的矩阵,对于数字图像处理有研究的话可以知道我们可以把图像当做一个矩阵(或多个矩阵的组合)进行处理。在SDWebImage的压缩方法中,使用了一个名为块(tile/blit)的东西,实际上是就是图像矩阵的一个子矩阵。由于种种原因,把块的宽度固定为原图像(original image not source image)的宽度。
那么这个块的目的是什么?
先尝试阅读第17步中的第3,4步代码:
//3. 使用sourceTile矩形内的源图像初始化sourceTileImageRef
sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
//4. 将sourceTileImageRef绘制到destTile矩形的destConext上下文
// 注意上面我们为destContext设置了插值质量,此时图像会进行缩放,因此会进行插值操作
CGContextDrawImage( destContext, destTile, sourceTileImageRef );
destContext
是最后我们要返回的上下文,上面绘制有压缩后的图像信息。
第三步:使用CGImageRef CGImageCreateWithImageInRect(CGImageRef image, CGRect rect)
方法将源图像sourceImageRef
中sourceTile
块内的值赋值给sourceTileImageRef
。因此可以将sourceTile
看做源图像的一小块。
第四步:将上面的块图像绘制到destContext
上下文的destTile
块中。需要注意到的是,由于sourceTile
的大小不等于destTile
的大小,因此这里CGContextDrawImage
方法会对图像使用CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh)
设置的插值质量进行插值处理。对此在NSHipster上有相关说明:
Next, CGContextSetInterpolationQuality allows for the context to interpolate pixels at various levels of fidelity. In this case, kCGInterpolationHigh is passed for best results. CGContextDrawImage allows for the image to be drawn at a given size and position, allowing for the image to be cropped on a particular edge or to fit a set of image features, such as faces. Finally, CGBitmapContextCreateImage creates a CGImage from the context.
如果对于插值精确度有疑问,可以参考这个问题。
上面两部是压缩过程的主要内容。如果理解了这部分对整个算法的理解很重要。接下来说明在循环中这个块(严格的说应该是两个块——sourceTile和destTile)在程序中如何进行操作。
源码中定义了几个与tile
有关的常量在下面会使用到:
static const CGFloat kSourceImageTileSizeMB = 20.0f;
//destSeemOverlap
static const CGFloat kDestSeemOverlap = 2.0f;
static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB;
然后初始化sourceTile
和destTile
的大小
CGRect sourceTile = CGRectZero;
sourceTile.size.width = sourceResolution.width;
sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width );
sourceTile.origin.x = 0.0f;
CGRect destTile;
destTile.size.width = destResolution.width;
destTile.size.height = sourceTile.size.height * imageScale;
destTile.origin.x = 0.0f;
所以tile
的宽度是固定的,无论是source
还是dest
都与其对应的原图片的宽度相等。接着初始化第二个用于计算的overlap
变量。与上面的destOverlap
一样,不必在意其语义。
// 计算公式: sourceSeemOverlap = (int)kDestSeemOverlap / imageScale
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
接着作进入循环前的准备工作:计算循环次数;使用overlap
更新tile
块的高度。
//15. 计算循环次数
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
// 如果不能整除,有余数,则循环次数+1
// 余数记录下来
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
if(remainder) {
iterations++;
}
//16. 将overlap常量累加到块的高度中,保存源图像块的高度到sourceTileHeightMinusOverlap
float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height += sourceSeemOverlap;
destTile.size.height += kDestSeemOverlap;
进入循环,每次循环都更新块的纵坐标。更新法则如下:
sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
经过了上面这么多的铺垫,现在可以看到tile
是以一个什么方式来进行移动。显而易见,每经过一次循环:
-
sourceTile
的y
值都以sourceTileHeightMinusOverlap
为增量增加(假设在UIKit的坐标系上就是每次向下移动增量大小) -
destTile
的y
值会逐渐从大变小。增量为sourceTileHeightMinusOverlap * imageScale
。 - 每次循环中
tile
的size
保持固定(最后一次循环除外)
为了更直观的理解,将这部分代码提取了一下作为测试,加入了用于调试的Log。
输入为一副3992 * 2442的图片,在运行过程中控制台输出如下:
2017-05-24 10:53:16.430 SDWebDecoderTest[1007:65513] 循环次数:0
2017-05-24 10:53:16.430 SDWebDecoderTest[1007:65513] 在{{0, 6}, {3992, 71}} 内绘制sourceTileImageRef
2017-05-24 10:53:16.646 SDWebDecoderTest[1007:65513] 将sourceTileImageRef 绘制到 destTile: {{0, 634.96174430847168}, {1169, 21.038255661725998}}中
2017-05-24 10:53:16.646 SDWebDecoderTest[1007:65513] 循环次数:1
2017-05-24 10:53:16.647 SDWebDecoderTest[1007:65513] 在{{0, 71}, {3992, 71}} 内绘制sourceTileImageRef
2017-05-24 10:53:16.659 SDWebDecoderTest[1007:65513] 将sourceTileImageRef 绘制到 destTile: {{0, 615.92348861694336}, {1169, 21.038255661725998}}中
2017-05-24 10:53:16.659 SDWebDecoderTest[1007:65513] 循环次数:2
2017-05-24 10:53:16.659 SDWebDecoderTest[1007:65513] 在{{0, 136}, {3992, 71}} 内绘制sourceTileImageRef
2017-05-24 10:53:16.671 SDWebDecoderTest[1007:65513] 将sourceTileImageRef 绘制到 destTile: {{0, 596.88523483276367}, {1169, 21.038255661725998}}中
2017-05-24 10:53:16.671 SDWebDecoderTest[1007:65513] 循环次数:3
2017-05-24 10:53:16.671 SDWebDecoderTest[1007:65513] 在{{0, 201}, {3992, 71}} 内绘制sourceTileImageRef
2017-05-24 10:53:16.683 SDWebDecoderTest[1007:65513] 将sourceTileImageRef 绘制到 destTile: {{0, 577.84697723388672}, {1169, 21.038255661725998}}中
·····中间的省略······
2017-05-24 10:53:17.029 SDWebDecoderTest[1007:65513] 循环次数:31
2017-05-24 10:53:17.029 SDWebDecoderTest[1007:65513] 在{{0, 2021}, {3992, 71}} 内绘制sourceTileImageRef
2017-05-24 10:53:17.041 SDWebDecoderTest[1007:65513] 将sourceTileImageRef 绘制到 destTile: {{0, 44.77581787109375}, {1169, 21.038255661725998}}中
2017-05-24 10:53:17.041 SDWebDecoderTest[1007:65513] 循环次数:32
2017-05-24 10:53:17.041 SDWebDecoderTest[1007:65513] 在{{0, 2086}, {3992, 71}} 内绘制sourceTileImageRef
2017-05-24 10:53:17.053 SDWebDecoderTest[1007:65513] 将sourceTileImageRef 绘制到 destTile: {{0, 25.737548828125}, {1169, 21.038255661725998}}中
2017-05-24 10:53:17.053 SDWebDecoderTest[1007:65513] 循环次数:33
2017-05-24 10:53:17.054 SDWebDecoderTest[1007:65513] 在{{0, 2151}, {3992, 71}} 内绘制sourceTileImageRef
2017-05-24 10:53:17.065 SDWebDecoderTest[1007:65513] 将sourceTileImageRef 绘制到 destTile: {{0, 6.69927978515625}, {1169, 21.038255661725998}}中
2017-05-24 10:53:17.066 SDWebDecoderTest[1007:65513] 循环次数:34
2017-05-24 10:53:17.066 SDWebDecoderTest[1007:65513] 在{{0, 2216}, {3992, 71}} 内绘制sourceTileImageRef
2017-05-24 10:53:17.071 SDWebDecoderTest[1007:65513] 将sourceTileImageRef 绘制到 destTile: {{0, 1.0840253829956055}, {1169, 7.6153020858764648}}中
补充一个获取图片类型的代码
在SDWebImage中的NSData+ImageContentType分类中使用以下方法获取图片类型:
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
if (!data) {
return SDImageFormatUndefined;
}
uint8_t c;
//Copies a number of bytes from the start of the receiver's data into a given buffer.
[data getBytes:&c length:1];
switch (c) {
case 0xFF:
return SDImageFormatJPEG;
case 0x89:
return SDImageFormatPNG;
case 0x47:
return SDImageFormatGIF;
case 0x49:
case 0x4D:
return SDImageFormatTIFF;
case 0x52:
// R as RIFF for WEBP
if (data.length < 12) {
return SDImageFormatUndefined;
}
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
return SDImageFormatWebP;
}
}
return SDImageFormatUndefined;
}
图片数据的第一个字节是固定的,一种类型的图片第一个字节就是它的标识。
总结
SDWebImageDecoder提供了图片解码功能,同时还允许对图片进行压缩操作,防止解压后内存暴涨。