读 SDWebImage 六 (编码器二:SDWebImageCoderHelper 以及动图处理使用到的 SDWebImageFrame)
SDWebImageFrame
类对象
在动图处理期间,使用到了 SDWebImageFrame
对象,该对象是使用单独的类实现的,在GIF等动态图使用中作为每一帧的显示作用。该类用于通过SDWebImageCoderHelper
中的animatedImageWithFrames
创建动画图片。 如果需要指定动画图片循环计数,请在“UIImage + MultiFormat”中使用sd_imageLoopCount
属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| /** 当前帧的图片 */ @property (nonatomic, strong, readonly, nonnull) UIImage *image; /** 要显示的当前帧的持续时间。 数字是秒而不是毫秒。不应将此值设置为零。 */ @property (nonatomic, readonly, assign) NSTimeInterval duration;
/** 使用指定图片和持续时间创建框架实例
@param image 当前帧的图片 @param duration 当前帧的持续时间 @return 返回框架实例 */ + (instancetype _Nonnull)frameWithImage:(UIImage * _Nonnull)image duration:(NSTimeInterval)duration;
|
初始化方法实现
1 2 3 4 5 6 7
| + (instancetype)frameWithImage:(UIImage *)image duration:(NSTimeInterval)duration { SDWebImageFrame *frame = [[SDWebImageFrame alloc] init]; frame.image = image; frame.duration = duration; return frame; }
|
动图处理
根据SDWebImageFrame帧数组返回动图
1 2 3 4 5 6 7 8 9
| /** 根据SDWebImageFrame帧数组返回动图 对于UIKit,这将应用补丁,然后创建动画UIImage。 补丁是因为`+ [UIImage animatedImageWithImages:duration:]`只使用每个图片的平均持续时间。 因此,如果不同的帧具有不同的持续时间,则不起作用 因此,我们重复指定帧的指定帧以使其工作。 对于AppKit,NSImage不支持GIF以外的动画。 这将尝试将帧编码为GIF格式,然后创建用于渲染的动画NSImage。 注意,如果输入帧包含完整的Alpha通道,动画图片可能会丢失一些细节,因为GIF仅支持1位alpha通道。 (1个像素,透明或不透明)
@param frames SDWebImageFrame帧数组. 如果数组为空或者nil,返回nil @return 用于在UIImageView(UIKit)或NSImageView(AppKit)上渲染的动画图片 */ + (UIImage * _Nullable)animatedImageWithFrames:(NSArray<SDWebImageFrame *> * _Nullable)frames;
|
方法实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| + (UIImage *)animatedImageWithFrames:(NSArray<SDWebImageFrame *> *)frames { //如果数组元素为空,返回动图为nil NSUInteger frameCount = frames.count; if (frameCount == 0) { return nil; } // 生成临时变量保存动图 UIImage *animatedImage; //如果是iOS 、iWatch 、 Apple TV #if SD_UIKIT || SD_WATCH //生成一个元素类型为非负整数,长度为动图帧数的数组个数,保存每一帧的展示时间 NSUInteger durations[frameCount]; for (size_t i = 0; i < frameCount; i++) { //遍历SDWebImageFrame对象数组,获取每一帧的展示时间 durations[i] = frames[i].duration * 1000; } 计算所有帧展示时长的最大公约数 NSUInteger const gcd = gcdArray(frameCount, durations); //临时变量保存总时长 __block NSUInteger totalDuration = 0; //创建一个可变数组,长度为动图帧数的数组个数 NSMutableArray<UIImage *> *animatedImages = [NSMutableArray arrayWithCapacity:frameCount]; //遍历传入的动图帧数的数组 [frames enumerateObjectsUsingBlock:^(SDWebImageFrame * _Nonnull frame, NSUInteger idx, BOOL * _Nonnull stop) { // 获取SDWebImageFrame对象保存的每一帧的图像 UIImage *image = frame.image; // 获取SDWebImageFrame对象保存的每一帧的展示时间 NSUInteger duration = frame.duration * 1000; //总时长 totalDuration += duration; //临时变量,保存重复次数 NSUInteger repeatCount; // 如果计算出的最大公约数大于零,每一帧的重复次数就是展示时间除以最大公约数 // 否则每一帧只重复一次,也就说不重复 if (gcd) { repeatCount = duration / gcd; } else { repeatCount = 1; } // 根据重复次数向动图数组中重复添加同一帧 for (size_t i = 0; i < repeatCount; ++i) { [animatedImages addObject:image]; } }]; 获得动图 animatedImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.f]; #else //如果不是(iOS 、iWatch 、 Apple TV ) NSMutableData *imageData = [NSMutableData data]; CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:SDImageFormatGIF]; // 创建图像目标。 GIF不支持EXIF图像方向 CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, frameCount, NULL); if (!imageDestination) { // 处理失败。 return nil; } //遍历传入的动图帧数的数组 for (size_t i = 0; i < frameCount; i++) { @autoreleasepool { // 获取SDWebImageFrame对象保存的每一帧的图像 SDWebImageFrame *frame = frames[i]; // 获取SDWebImageFrame对象保存的每一帧的展示时间 float frameDuration = frame.duration; CGImageRef frameImageRef = frame.image.CGImage; NSDictionary *frameProperties = @{(__bridge NSString *)kCGImagePropertyGIFDictionary : @{(__bridge NSString *)kCGImagePropertyGIFDelayTime : @(frameDuration)}}; CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties); } } // Finalize the destination. if (CGImageDestinationFinalize(imageDestination) == NO) { // 处理失败。 CFRelease(imageDestination); return nil; } CFRelease(imageDestination); SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:imageData]; animatedImage = [[NSImage alloc] initWithSize:imageRep.size]; [animatedImage addRepresentation:imageRep]; #endif return animatedImage; }
|
根据动图返回SDWebImageFrame帧数组
1 2 3 4 5 6 7 8 9
| /** 根据动图返回SDWebImageFrame帧数组 对于UIKit,这将取消应用上述描述的补丁,然后创建frames数组。 这也适用于普通的动画UIImage。 对于AppKit,NSImage不支持GIF以外的动画。 这将尝试解码GIF imageRep,然后创建帧数组。
@param animatedImage 一个动图.如果不是动图,返回nil @return 返回SDWebImageFrame帧数组 */ + (NSArray<SDWebImageFrame *> * _Nullable)framesFromAnimatedImage:(UIImage * _Nullable)animatedImage;
|
方法实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| + (NSArray<SDWebImageFrame *> *)framesFromAnimatedImage:(UIImage *)animatedImage { //如果动图不存在,返回nil if (!animatedImage) { return nil; } //初始化两个临时变量: SDWebImageFrame对象 和帧个数 NSMutableArray<SDWebImageFrame *> *frames = [NSMutableArray array]; NSUInteger frameCount = 0; #if SD_UIKIT || SD_WATCH //获取动图的帧图片数组 NSArray<UIImage *> *animatedImages = animatedImage.images; //获取动图的帧图片数量 frameCount = animatedImages.count; //如果帧图片数量为0,表示不是动图,返回nil if (frameCount == 0) { return nil; } //计算每一帧的平均展示时间 NSTimeInterval avgDuration = animatedImage.duration / frameCount; //如果这个动图没有展示时间就默认每一帧展示100毫秒 ,即0.1秒(这没有像GIF或WebP那样的10ms限制,以允许自定义编码器提供限制) if (avgDuration == 0) { avgDuration = 0.1; } // 记录不同帧图片的数量 __block NSUInteger index = 0; //记录一帧图片重复次数 __block NSUInteger repeatCount = 1; //记录当前遍历到的图片之前的图片 __block UIImage *previousImage = animatedImages.firstObject; //遍历图片数组 [animatedImages enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) { // 第一张图片不处理,忽略掉 if (idx == 0) { return; } //如果这一帧的图片和之前一帧图片相同就添加重复次数 if ([image isEqual:previousImage]) { repeatCount++; } else {// 如果两帧图片不相同,就生成SDWebImageFrame对象 SDWebImageFrame *frame = [SDWebImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount]; // 数组记录对象 [frames addObject:frame]; // 重复次数设置为1次 repeatCount = 1; // 记录不同的帧图片的数量加1 index++; } //记录当前图片,用于下次遍历使用 previousImage = image; // 最后一张图片 if (idx == frameCount - 1) { // 如果是最后一张照片就直接添加 SDWebImageFrame *frame = [SDWebImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount]; [frames addObject:frame]; } }]; #else //以下非iOS实现,一些东西不同,不做分析 NSBitmapImageRep *bitmapRep; for (NSImageRep *imageRep in animatedImage.representations) { if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) { bitmapRep = (NSBitmapImageRep *)imageRep; break; } } if (bitmapRep) { frameCount = [[bitmapRep valueForProperty:NSImageFrameCount] unsignedIntegerValue]; } if (frameCount == 0) { return nil; } for (size_t i = 0; i < frameCount; i++) { @autoreleasepool { //NSBitmapImageRep需要手动更改帧。 “Good taste”API [bitmapRep setProperty:NSImageCurrentFrame withValue:@(i)]; float frameDuration = [[bitmapRep valueForProperty:NSImageCurrentFrameDuration] floatValue]; NSImage *frameImage = [[NSImage alloc] initWithCGImage:bitmapRep.CGImage size:CGSizeZero]; SDWebImageFrame *frame = [SDWebImageFrame frameWithImage:frameImage duration:frameDuration]; [frames addObject:frame]; } } #endif return frames; }
|
图片方向处理
将EXIF图片方向转换为iOS图片方向
1 2 3 4 5 6 7
| /** 将EXIF图片方向转换为iOS图片方向。
@param exifOrientation EXIF图片方向 @return iOS图片方向 */ + (UIImageOrientation)imageOrientationFromEXIFOrientation:(NSInteger)exifOrientation;
|
方法实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| // 将EXIF图片方向转换为iOS图片方向。根据EXIF图片方向做判断,返回iOS图片方向 + (UIImageOrientation)imageOrientationFromEXIFOrientation:(NSInteger)exifOrientation { // CGImagePropertyOrientation在上面的iOS 8上可用。 目前保持兼容性 UIImageOrientation imageOrientation = UIImageOrientationUp; switch (exifOrientation) { case 1: imageOrientation = UIImageOrientationUp; break; case 3: imageOrientation = UIImageOrientationDown; break; case 8: imageOrientation = UIImageOrientationLeft; break; case 6: imageOrientation = UIImageOrientationRight; break; case 2: imageOrientation = UIImageOrientationUpMirrored; break; case 4: imageOrientation = UIImageOrientationDownMirrored; break; case 5: imageOrientation = UIImageOrientationLeftMirrored; break; case 7: imageOrientation = UIImageOrientationRightMirrored; break; default: break; } return imageOrientation; }
|
将iOS方向转换为EXIF图片方向
1 2 3 4 5 6 7
| /** 将iOS方向转换为EXIF图片方向
@param imageOrientation iOS方向 @return EXIF图片方向 */ + (NSInteger)exifOrientationFromImageOrientation:(UIImageOrientation)imageOrientation;
|
方法实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| // 将iOS方向转换为EXIF图片方向,根据iOS方向做判断,返回EXIF图片方向 + (NSInteger)exifOrientationFromImageOrientation:(UIImageOrientation)imageOrientation { // CGImagePropertyOrientation 在上面的iOS 8上可用。 目前保持兼容性 NSInteger exifOrientation = 1; switch (imageOrientation) { case UIImageOrientationUp: exifOrientation = 1; break; case UIImageOrientationDown: exifOrientation = 3; break; case UIImageOrientationLeft: exifOrientation = 8; break; case UIImageOrientationRight: exifOrientation = 6; break; case UIImageOrientationUpMirrored: exifOrientation = 2; break; case UIImageOrientationDownMirrored: exifOrientation = 4; break; case UIImageOrientationLeftMirrored: exifOrientation = 5; break; case UIImageOrientationRightMirrored: exifOrientation = 7; break; default: break; } return exifOrientation; }
|
两个私有方法: 计算最大公约数
计算两个整数a和b的最大公约数
1 2 3 4 5 6 7 8 9
| static NSUInteger gcd(NSUInteger a, NSUInteger b) { NSUInteger c; while (a != 0) { c = a; a = b % a; b = c; } return b; }
|
计算一个整数数组的最大公约数
1 2 3 4 5 6 7 8 9 10
| static NSUInteger gcdArray(size_t const count, NSUInteger const * const values) { if (count == 0) { return 0; } NSUInteger result = values[0]; for (size_t i = 1; i < count; ++i) { result = gcd(values[i], result); } return result; }
|