常用的第三方库:
关于图片的下载第三方库,目前有三个选择:
-
SDWebImage
是使用最广泛的,目前的项目中在用,而且还是直接源码方式,连CocoaPod
都没用 -
YYImage
据说可以替代SDWebImage
,曾经看过YYModel
和YYCache
,真的有意愿选择 -
AFNetworking
,这个主要用来做网络库,图片的下载很少用。SDWebImage
作者也说AFNetworking
值得用,采用系统的NSURLCache
实现缓存。 -
SDWebImage
和YYImage
都用了自己的缓存,并且会把图片解码操作提前做了,可以加快UITableView
的显示,但缺点是内存占用会偏高。缓存和UIImage
编解码是这两个库的特色。 -
SDWebImage
项目中在用,先看他的实现方式,其他的等以后有机会再学
读取内存缓存
// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
NSData *diskData = nil;
if ([image isGIF]) {
diskData = [self diskImageDataBySearchingAllPathsForKey:key];
}
if (doneBlock) {
doneBlock(image, diskData, SDImageCacheTypeMemory);
}
return nil;
}
-
UIImage *image
很好理解,应该就是直接给UIImageView
用的。根据后面代码阅读,这里的不是普通的UIImage
,应该是考虑了乘数因子scale
和解码之后的UIImage
,是可以直接在屏幕上显示的。 - 这里的
NSData *diskData
是特指gif
动图的数据。gif
动图是不考虑解码的。
@property (strong, nonatomic, nonnull) NSCache *memCache;
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
return [self.memCache objectForKey:key];
}
软件缓存直接使用系统的NSCache
实现,比较简单
- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath];
if (data) {
return data;
}
return nil;
}
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key {
return [self cachePathForKey:key inPath:self.diskCachePath];
}
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path {
NSString *filename = [self cachedFileNameForKey:key];
return [path stringByAppendingPathComponent:filename];
}
- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
const char *str = key.UTF8String;
if (str == NULL) {
str = "";
}
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
r[11], r[12], r[13], r[14], r[15], [key.pathExtension isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", key.pathExtension]];
return filename;
}
如果是Gif
动图,不能直接用Image
,会去磁盘上读NSData
,这些数据存在磁盘某个目录下,文件名是key
的md5
值,而这个key
是图片的url
如果是普通图片,内存中Image
可以直接用,默认的话是解码过的,可以直接在屏幕上显示
读取磁盘缓存
dispatch_async(self.ioQueue, ^{
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
if (doneBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
});
}
}
});
-
self.ioQueue
是一个串行队列,放在一个@autoreleasepool
中估计是为了省内存 - 这里的
diskData
,从磁盘的某处(url
的MD5
值为文件名,带扩展名),读取NSData
格式的数据。 -
self.config.shouldCacheImagesInMemory
默认值是YES
,就是说从磁盘上读出来数据同时也会在内存中保存一份。下次就直接在内存缓存中命中了,不需要再到缓慢的磁盘缓存中读取。 - 大小
cost
是像素,要考虑乘数因子scale
NSUInteger SDCacheCostForImage(UIImage *image) {
return image.size.height * image.size.width * image.scale * image.scale;
}```
* 这里的`diskImage`和普通的`UIImage`不同,考虑了乘数因子`scale`和解码之后,是可以直接在屏幕上显示的。
#### 读取过程
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key {
NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
if (data) {
UIImage *image = [UIImage sd_imageWithData:data];
image = [self scaledImageForKey:key image:image];
if (self.config.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];
}
return image;
}
else {
return nil;
}
}
* 从磁盘读取`NSData`格式的数据,和前面用的都是同一个函数
* 将`NSData`格式的数据转化为`UIImage`
* `UIImage`考虑乘数因子`scale`
* `self.config.shouldDecompressImages`默认是`YES`,`UIImage`需要经过解码,成为能直接在屏幕上显示的`UIImage`。
不经过解码的普通`UIImage`,会在主线程进行解码后再显示在屏幕上,造成`CPU`占用率过高。平时关系不大,在`UITableView`快速滑动,并且图片数据量较大的时候,会有卡顿现象发生。
解码之后的`UIImage`,可以直接在屏幕上显示,但是数据量很大,所以高清图在低端机上内存暴涨,还发烫,就是这个原因。下面的有篇文章说的就是这个,解决方案就是把`self.config.shouldDecompressImages`设置为`NO`,这样就解决内存占用过大的问题了。
另外,对于`JPEG`图,`iPhone4s`以后的机子都有硬件编解码的,所以为了减少内存占用,也可以考虑把这个开关关闭。
**是省CPU时间,还是省内存?**默认选择了省cpu时间,减少卡顿现象,提升体验
#### `NSData`转换为`UIImage`
-
(nullable UIImage *)sd_imageWithData:(nullable NSData *)data {
if (!data) {
return nil;
}UIImage *image;
SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:data];
if (imageFormat == SDImageFormatGIF) {
image = [UIImage sd_animatedGIFWithData:data];
} else if (imageFormat == SDImageFormatWebP) {
image = [UIImage sd_imageWithWebPData:data];
} else {
image = [[UIImage alloc] initWithData:data];
UIImageOrientation orientation = [self sd_imageOrientationFromImageData:data];
if (orientation != UIImageOrientationUp) {
image = [UIImage imageWithCGImage:image.CGImage
scale:image.scale
orientation:orientation];
}
}
return image;
}
* 这是`UIImage`的一个类别`category`
* `gif`动图和`webp`格式有特殊的生成方式
* 这里还考虑了图片的方向,如果不是朝上,图片生成方式还不一样
#### 判断图片的格式
typedef NS_ENUM(NSInteger, SDImageFormat) {
SDImageFormatUndefined = -1,
SDImageFormatJPEG = 0,
SDImageFormatPNG,
SDImageFormatGIF,
SDImageFormatTIFF,
SDImageFormatWebP
};
-
(SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
if (!data) {
return SDImageFormatUndefined;
}uint8_t c;
[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;
}
* 这是`NSData`的一个类别
* 默认是`JPEG`格式
* 第一个字节代表了图片的格式
#### 普通的`UIImage`转换为像素`UIImage`
- (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image {
return SDScaledImageForKey(key, image);
}
inline UIImage *SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullable image) {
if (!image) {
return nil;
}
if ((image.images).count > 0) {
NSMutableArray
for (UIImage *tempImage in image.images) {
[scaledImages addObject:SDScaledImageForKey(key, tempImage)];
}
return [UIImage animatedImageWithImages:scaledImages duration:image.duration];
} else {
if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
CGFloat scale = 1;
if (key.length >= 8) {
NSRange range = [key rangeOfString:@"@2x."];
if (range.location != NSNotFound) {
scale = 2.0;
}
range = [key rangeOfString:@"@3x."];
if (range.location != NSNotFound) {
scale = 3.0;
}
}
UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
image = scaledImage;
}
return image;
}
}
* `gif`动图和普通图片的区别`(image.images).count > 0`
* 乘数因子`scale`默认为1,根据图片名称中的`@2x.` 以及` @3x.`来判断是2倍图还是3倍图
* `gif`动图有包含的`images`和持续时间`duration`两个重要特征
#### 图片解码
-
(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;
}
* `gif`动图不用解码`image.images != nil`和`(image.images).count > 0`是一个意思
* 有透明度信息的`Alpha`的图片不用解码
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];
@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;
}
}
* 这个过程是很耗内存的,所以用一个`@autoreleasepool`进行内存管理
* 把`pixel`化的`UIImage`转化为`bitmap`的`context`
* 利用这个`context`画一个`UIImage`
* 这个`UIImage`可以直接在屏幕上显示了,不需要`CPU`或者硬件(`JPEG`)解码了,加快了显示,避免了`UITableView`快速滑动过程中的“卡顿”现象
## 下载完成后存缓存
-
(void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
if (!image || !key) {
if (completionBlock) {
completionBlock();
}
return;
}
// if memory cache is enabled
if (self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}if (toDisk) {
dispatch_async(self.ioQueue, ^{
NSData *data = imageData;if (!data && image) { SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data]; data = [image sd_imageDataAsFormat:imageFormatFromData]; } [self storeImageDataToDisk:data forKey:key]; if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{ completionBlock(); }); } });
} else {
if (completionBlock) {
completionBlock();
}
}
}
* 先在内存中保存一份
* 是否保存到磁盘由参数`toDisk` 控制,比如设置了`SDWebImageCacheMemoryOnly`的话,就不保存到磁盘了
* 保存到磁盘过程在一个串行队列`self.ioQueue`中执行,由`dispatch_async`开辟一个子线程来完成。
* `image`和`key`(`url`)参数是必须的,不然不会保存。不管是`gif`动图或者普通的`png、jpeg`图,`image`参数都是有的
* `NSData *`格式的`imageData`参数是可以为空的,如果不为空,那么就是`gif`动图的数据,直接存磁盘了。
如果为空,那么就把`image`参数转换为`NSData *`之后存到磁盘
* 下面这段代码写的比较差;也可能是个`bug`,不理解为什么会这些写
if (!data && image) {
SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data];
data = [image sd_imageDataAsFormat:imageFormatFromData];
}```
data
只有nil
的情况,才会进入,所以sd_imageFormatForImageData
的入参是确定的nil
,没有必要给data
。imageFormatFromData
是确定的SDImageFormatUndefined
另外image
一进入函数的时候就查过,到这里肯定不是nil
,没有必要再查一下
- (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat {
NSData *imageData = nil;
if (self) {
int alphaInfo = CGImageGetAlphaInfo(self.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL usePNG = hasAlpha;
// the imageFormat param has priority here. But if the format is undefined, we relly on the alpha channel
if (imageFormat != SDImageFormatUndefined) {
usePNG = (imageFormat == SDImageFormatPNG);
}
if (usePNG) {
imageData = UIImagePNGRepresentation(self);
} else {
imageData = UIImageJPEGRepresentation(self, (CGFloat)1.0);
}
}
return imageData;
}
- 这是
UIImage
的一个类别category
; - 存入磁盘的
NSData *
格式由PNG
或者JPEG
转换而来; - 默认是
JPEG
类型 - 如果是
.png
图片,或者有Alpha
信息,那么是PNG
格式 - 由于调用者传入的参数都是
SDImageFormatUndefined
,所以就简化为有Alpha
信息就是PNG
格式数据,其他都是JPEG
格式数据。 - 磁盘上的数据是没有解码的图片数据,体积比较小;内存中的是解码过的图片数据,体积比较大。
下载后解码
#pragma mark NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
@synchronized(self) {
self.dataTask = nil;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
if (!error) {
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
}
});
}
if (error) {
[self callCompletionBlocksWithError:error];
} else {
if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
/**
* See #1608 and #1623 - apparently, there is a race condition on `NSURLCache` that causes a crash
* Limited the calls to `cachedResponseForRequest:` only for cases where we should ignore the cached response
* and images for which responseFromCached is YES (only the ones that cannot be cached).
* Note: responseFromCached is set to NO inside `willCacheResponse:`. This method doesn't get called for large images or images behind authentication
*/
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) {
// hack
[self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
} else if (self.imageData) {
UIImage *image = [UIImage sd_imageWithData:self.imageData];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
// Do not force decoding animated GIFs
if (!image.images) {
if (self.shouldDecompressImages) {
if (self.options & SDWebImageDownloaderScaleDownLargeImages) {
image = [UIImage decodedAndScaledDownImageWithImage:image];
[self.imageData setData:UIImagePNGRepresentation(image)];
} else {
image = [UIImage decodedImageWithImage:image];
}
}
}
if (CGSizeEqualToSize(image.size, CGSizeZero)) {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
} else {
[self callCompletionBlocksWithImage:image imageData:self.imageData error:nil finished:YES];
}
} else {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
}
}
}
[self done];
}
- 采用
NSURLSession
的dataTask
进行下载,并没有用downloadTask
- 下载的数据保存在self.imageData中,类型是
NSData *
@property (strong, nonatomic, nullable) NSMutableData *imageData;
- 通过函数
sd_imageWithData
将NSData *
格式的数据转换为普通的UIImage *
- 通过函数
scaledImageForKey
将普通的UIImage *
转换为考虑了乘数因子scale
的像素UIImage *
- 通过函数
decodedImageWithImage
进行解码,生成能在屏幕上直接显示的UIImage *
设置磁盘缓存最大值
// If our remaining disk cache exceeds a configured maximum size, perform a second
// size-based cleanup pass. We delete the oldest files first.
if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
// Target half of our maximum cache size for this cleanup pass.
const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;
// Sort the remaining cache files by their last modification time (oldest first).
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// Delete files until we fall below our desired cache size.
for (NSURL *fileURL in sortedFiles) {
if ([_fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
- 默认情况下
self.config.maxCacheSize = 0;
不会调整磁盘缓存的大小 - 默认缓存的有效时间是1周,在一周之内访问过的图片都缓存下来的。过期的文件会被删除。
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
- 如果有必要,可以设置
self.config.maxCacheSize
为一个合理值,减少对手机磁盘的占用。每当超过这个设定值,将会删除一些缓存文件,直到总容量小于这个设定值得一半。 - 这个控制的是磁盘缓存的文件,对于由于解码而造成的内存超标,这个参数不起作用。
清除内存
当出现内存告警时,会清缓存(内存缓存)。解码导致内存占用大,用空间换时间,使界面显示更流畅,不“卡顿”这个是有的。
不过由于内存占用过大而导致崩溃,应该不至于吧?
#pragma mark - Cache clean Ops
- (void)clearMemory {
[self.memCache removeAllObjects];
}
// Subscribe to app events
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
关于大图的压缩解码
SDWebImageDownloaderScaleDownLargeImages
以及SDWebImageScaleDownLargeImages
默认是不设置的。如果设置,就会调用decodedAndScaledDownImageWithImage
进行压缩解码
/*
* Defines the maximum size in MB of the decoded image when the flag `SDWebImageScaleDownLargeImages` is set
* 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;
static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
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 {
if (![UIImage shouldDecodeImage:image]) {
return image;
}
if (![UIImage shouldScaleDownImage:image]) {
return [UIImage decodedImageWithImage:image];
}
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 {
CGImageRef sourceImageRef = image.CGImage;
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
// Determine the scale ratio to apply to the input image
// that results in an output image of the defined size.
// see kDestImageSizeMB, and how it relates to destTotalPixels.
float imageScale = kDestTotalPixels / sourceTotalPixels;
CGSize destResolution = CGSizeZero;
destResolution.width = (int)(sourceResolution.width*imageScale);
destResolution.height = (int)(sourceResolution.height*imageScale);
// 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;
}
CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
// Now define the size of the rectangle to be used for the
// incremental blits from the input image to the output image.
// we use a source tile width equal to the width of the source
// image due to the way that iOS retrieves image data from disk.
// iOS must decode an image from disk in full width 'bands', even
// if current graphics context is clipped to a subrect within that
// band. Therefore we fully utilize all of the pixel data that results
// from a decoding opertion by achnoring our tile size to the full
// width of the input image.
CGRect sourceTile = CGRectZero;
sourceTile.size.width = sourceResolution.width;
// The source tile height is dynamic. Since we specified the size
// of the source tile in MB, see how many rows of pixels high it
// can be given the input image width.
sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width );
sourceTile.origin.x = 0.0f;
// The output tile is the same proportions as the input tile, but
// scaled to image scale.
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.
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
CGImageRef sourceTileImageRef;
// calculate the number of read/write operations required to assemble the
// output image.
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
// If tile height doesn't divide the image height evenly, add another iteration
// to account for the remaining pixels.
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
if(remainder) {
iterations++;
}
// Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations.
float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height += sourceSeemOverlap;
destTile.size.height += kDestSeemOverlap;
for( int y = 0; y < iterations; ++y ) {
@autoreleasepool {
sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
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;
}
CGContextDrawImage( destContext, destTile, sourceTileImageRef );
CGImageRelease( sourceTileImageRef );
}
}
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;
}
}
-
shouldDecodeImage
判断是否需要解码,空的,有alpha
信息的,GIF
动图都不会解码 -
shouldScaleDownImage
判断是否需要压缩;如果解压后大小超过60M
,那么就需要压缩,否则就不需要。
如果不需要压缩,那么就调用decodedImageWithImage
进行无压缩的解码 - 压缩后的目标是把大小压缩到
60M
以内
static const CGFloat kDestImageSizeMB = 60.0f;
比如原先100M的位图(解码后的格式)压缩后得到的位图在60M
以内 - 压缩不是一次完成的,而是一块一块完成的,每次的大小是
20M
static const CGFloat kSourceImageTileSizeMB = 20.0f;
- 他的方法是宽度保持不变,然后高度
y
一块一块往下移动 - 往下移多少次呢?高度除一下,有余数加1
// calculate the number of read/write operations required to assemble the
// output image.
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
// If tile height doesn't divide the image height evenly, add another iteration
// to account for the remaining pixels.
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
if(remainder) {
iterations++;
}
- 又想起了网上说的解码导致内存暴涨,对于高清图,关闭解码。这个压缩解码,不正是为了解决高清图解码后占用内存太多的问题吗?
所以除了网上说的关闭解码功能,也可以尝试一下打开这个压缩解码功能,也就是设置SDWebImageScaleDownLargeImages
参考文章
SDWebImage
iOS图片加载框架-SDWebImage解读
使用SDWebImage和YYImage下载高分辨率图,导致内存暴增的解决办法
iOS 处理图片的一些小 Tip
移动端图片格式调研
SDWebImage源码解读_之SDWebImageDecoder
CGBitmapContextCreate参数详解