最近項(xiàng)目改版价脾,要加載好幾個(gè)gif的動畫,剛開始嘗試使用了SDwebImage來加載顯示笛匙,可以正常顯示侨把,但加載多個(gè)gif的時(shí)候會被閃退,查找原因是內(nèi)存爆增引起的妹孙。
后來有嘗試使用YYkit 框架秋柄,來加載gif 動畫,YYKit 加載gif 的時(shí)候不會引起內(nèi)存問題涕蜂,但是CPU的使用率會很高华匾,
最后我使用的是lottie 動畫,加載會比較友善机隙,不會存在內(nèi)存和CPU的問題蜘拉,
接下來我們依次來分析一下相關(guān)的代碼
1、SDwebImage
@property (nonatomic, strong) UIImageView *gifImageview;
self.gifImageview = [[SDAnimatedImageView alloc]init];
[self.view addSubview:self.gifImageview];
[self.gifImageview mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.mas_equalTo(self.view);
make.centerY.mas_equalTo(self.view);
make.height.equalTo(@(200));
}];
// 這里我使用的是本地的文件有鹿,本gif 內(nèi)存是1.3M 包含60張圖片
NSString *path = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"轉(zhuǎn)場_1"] ofType:@"gif"];
NSData *data = [NSData dataWithContentsOfFile:path];
UIImage *image = [UIImage sd_imageWithGIFData:data]; [self.gifImageview setImage:image];
加載出來通過Xcode 查看旭旭,發(fā)現(xiàn)Memory 會很高,如下圖葱跋,這個(gè)是單獨(dú)一個(gè)gif持寄,我如果加載3個(gè)gif源梭,Memory會爆增到2G
1、我們通過sdwebImage 的源碼分析一下問題的存在
sd_imageWithGIFData
是SDwebImage 的 UIImage+GIF中方法稍味,里面直接調(diào)用 [[SDImageGIFCoder sharedCoder] decodedImageWithData:data options:0];
废麻,接下來我們直接看一下decodedImageWithData
的源碼
#import "SDImageIOAnimatedCoder.h"
- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
//安全判斷
if (!data) {
return nil;
}
CGFloat scale = 1;
//option 為 nil ,scalefactor thumbnailSizeValue 暫時(shí)無用
NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
if (scaleFactor != nil) {
scale = MAX([scaleFactor doubleValue], 1);
}
CGSize thumbnailSize = CGSizeZero;
//解碼縮略圖像素大小
NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
if (thumbnailSizeValue != nil) {
#if SD_MAC
thumbnailSize = thumbnailSizeValue.sizeValue;
#else
thumbnailSize = thumbnailSizeValue.CGSizeValue;
#endif
}
BOOL preserveAspectRatio = YES;
NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
if (preserveAspectRatioValue != nil) {
preserveAspectRatio = preserveAspectRatioValue.boolValue;
}
#if SD_MAC
// If don't use thumbnail, prefers the built-in generation of frames (GIF/APNG)
// Which decode frames in time and reduce memory usage
if (thumbnailSize.width == 0 || thumbnailSize.height == 0) {
SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data];
NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
imageRep.size = size;
NSImage *animatedImage = [[NSImage alloc] initWithSize:size];
[animatedImage addRepresentation:imageRep];
return animatedImage;
}
#endif
//二進(jìn)制類型的轉(zhuǎn)換
//CGImageSourceRef是個(gè)什么呢? 我們可以看到這是一個(gè)typedef CGImageSource * CGImageSourceRef;
//這是一個(gè)指針,CGImageSource是對圖像數(shù)據(jù)讀取任務(wù)的抽象模庐,通過它可以獲得圖像對象烛愧、縮略圖、圖像的屬性(包括Exif信息)掂碱。
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
if (!source) {
return nil;
}
//獲取有幾張圖片
size_t count = CGImageSourceGetCount(source);
//返回的動態(tài)圖片
UIImage *animatedImage;
BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
if (decodeFirstFrame || count <= 1) {
animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
} else {
//集合 存放單張的圖片
NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
for (size_t i = 0; i < count; i++) {
// 獲取gif每一幀圖像
UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
if (!image) {
continue;
}
// 獲取每一幀圖像對應(yīng)的顯示時(shí)間
NSTimeInterval duration = [self.class frameDurationAtIndex:i source:source];
//創(chuàng)建動圖
SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration];
[frames addObject:frame];
// 作者獲取每一幀圖像的顯示時(shí)間的目的僅僅是為了計(jì)算gif動畫的總時(shí)長怜姿,并沒有給每一幀圖像的顯示時(shí)間分配相應(yīng)的權(quán)重,導(dǎo)致每一幀圖像顯示的時(shí)間為平均時(shí)間
}
NSUInteger loopCount = [self.class imageLoopCountWithSource:source];
NSLog(@"%lu",(unsigned long)loopCount);
//把靜態(tài)的圖片轉(zhuǎn)換為動態(tài)的image,所以會有大量單張的圖片存放在內(nèi)存中
animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
animatedImage.sd_imageLoopCount = loopCount;
}
animatedImage.sd_imageFormat = self.class.imageFormat;
//釋放圖像數(shù)據(jù)讀取任務(wù)的抽象對象
CFRelease(source);
return animatedImage;
}
發(fā)現(xiàn)SDWebImage處理gif圖片的方法是:將gif資源中每一張imgae寫入到內(nèi)存中疼燥,通過animatedImageWithImages的方式播放動畫沧卢。這樣的好處是,gif輪詢播放時(shí)醉者,直接從內(nèi)存中取資源就好了但狭,降低了cpu的占用。也就是說撬即,SDWebImage是以空間換取的流暢度熟空。
2 YYKIt
我們來創(chuàng)建 imageView ,展示我們的內(nèi)容
@property (nonatomic,strong) YYAnimatedImageView *YYImageView;
self.YYImageView = [[YYAnimatedImageView alloc]init];
[self.view addSubview:self.YYImageView];
[self.YYImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.mas_equalTo(self.view);
make.centerY.mas_equalTo(self.view);
make.height.equalTo(@(200));
}];
YYImage *yyimage = [YYImage imageNamed:@"轉(zhuǎn)場_1"];
[self.YYImageView setImage:yyimage];
YYkit 加載gif 的過程中搞莺,memory 基本不會過度使用息罗,但是最初會有一段時(shí)間CPU的使用率會很高達(dá)到 98 %,
if (!_finalized && index > 0) return NULL;
if (_frames.count <= index) return NULL;
_YYImageDecoderFrame *frame = _frames[index];
if (_source) {
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_source, index, (CFDictionaryRef)@{(id)kCGImageSourceShouldCache:@(YES)});
if (imageRef && extendToCanvas) {
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
if (width == _width && height == _height) {
// 每次的展示都會調(diào)用create 方法
CGImageRef imageRefExtended = YYCGImageCreateDecodedCopy(imageRef, YES);
if (imageRefExtended) {
CFRelease(imageRef);
imageRef = imageRefExtended;
if (decoded) *decoded = YES;
}
} else {
CGContextRef context = CGBitmapContextCreate(NULL, _width, _height, 8, 0, YYCGColorSpaceGetDeviceRGB(), kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst);
if (context) {
CGContextDrawImage(context, CGRectMake(0, _height - height, width, height), imageRef);
CGImageRef imageRefExtended = CGBitmapContextCreateImage(context);
CFRelease(context);
if (imageRefExtended) {
CFRelease(imageRef);
imageRef = imageRefExtended;
if (decoded) *decoded = YES;
}
}
}
}
return imageRef;
}
發(fā)現(xiàn)了YYKit處理gif圖片的方法是:每次從緩存的gif中才沧,讀取當(dāng)前需要展示的image迈喉,進(jìn)行動畫展示。這樣做的好處是温圆,不用為gif的每張image開辟空間了挨摸,每次都是從一份gif資源中讀取一張image就好了。以一定的幀率從緩沖中解析出當(dāng)前需要展示的image岁歉,肯定是需要耗用cpu的得运。
3、lottie
我們先來創(chuàng)建一個(gè)lottie锅移,lottie 加載gif 動畫需要把動畫轉(zhuǎn)換成json數(shù)據(jù)熔掺,
@property (nonatomic, strong) LOTAnimationView *loImageView;
self.loImageView = [LOTAnimationView animationNamed:@"1"];
[self.loImageView play];
self.loImageView.loopAnimation = YES;
[self.view addSubview:self.loImageView];
[self.loImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.mas_equalTo(self.view);
make.centerY.mas_equalTo(self.view);
make.height.equalTo(@(200));
}];
self.loImageView2 = [LOTAnimationView animationNamed:@"2"];
[self.loImageView2 play];
[self.view addSubview:self.loImageView2];
通過lottie 加載同樣的gif動畫看到的CPU 和memory 的數(shù)據(jù)
3-1 接下來我們看一下lottie 的原理,具體使用方法請參考()iOS Lottie動畫接入過程詳解
1非剃、Lottie是Airbnb開源的一個(gè)動畫渲染庫置逻,支持多平臺,包括iOS备绽、Android券坞、React Native以及Flutter(https://github.com/airbnb/lottie-ios)鬓催。除了官方支持的平臺,更有大神實(shí)現(xiàn)了支持Windows恨锚、Qt宇驾、Skia以及React、Vue猴伶、Angular等平臺
Lottie動畫產(chǎn)生的流程如下:
注意點(diǎn):Lottie 3.0之后已經(jīng)全部使用swift實(shí)現(xiàn)飞苇,所以如果需要使用Objective-C版本需要使用Lottie 2.5.3版本
3-2 Lottie 原理
1、需要我們的設(shè)計(jì)把gif 動畫專程json
例如:
Lottie整體的原理如下:
1)首先要知道蜗顽,一個(gè)完整動畫View,是由很多個(gè)子Layer 組成雨让,而每個(gè)子Layer主要通過shapes(形狀)雇盖,masks(蒙版),transform三大部分進(jìn)行動畫栖忠。
2)Lottie框架通過讀取JSON文件崔挖,獲取到每個(gè)子Layer 的shapes,masks庵寞,以及出現(xiàn)時(shí)間狸相,消失時(shí)間以及Transform各個(gè)屬性的關(guān)鍵幀數(shù)組。
3)動畫則是通過給CompositionLayer (所有的子layer都添加在這個(gè)Layer 上)的 currentFrame屬性添加一個(gè)CABaseAnimation 來實(shí)現(xiàn)捐川。
4)所有的子Layer根據(jù)currentFrame 屬性的變化脓鹃,根據(jù)JSON中的關(guān)鍵幀數(shù)組計(jì)算出自己的當(dāng)前狀態(tài)并進(jìn)行顯示。
接下來讓我們深入它的源碼(OC版本)去看看古沥,對它的原理有一個(gè)更深刻的認(rèn)識
1)入口類為LOTAnimationView瘸右,提供了一系列加載和設(shè)置動畫的方法及屬性以及對動畫的操作,這里列舉一二
+ (nonnull instancetype)animationNamed:(nonnull NSString *)animationName NS_SWIFT_NAME(init(name:));
+ (nonnull instancetype)animationFromJSON:(nonnull NSDictionary *)animationJSON NS_SWIFT_NAME(init(json:));
+ (nonnull instancetype)animationFromJSON:(nullable NSDictionary *)animationJSON
inBundle:(nullable NSBundle *)bundle NS_SWIFT_NAME(init(json:bundle:));
...
@property (nonatomic, assign) CGFloat animationProgress;
@property (nonatomic, assign) CGFloat animationSpeed;
...
- (void)play;
- (void)pause;
LOTAnimationView所有的加載方法岩齿,最終執(zhí)行的都是把JSON字典傳到LOTComposition類中太颤,組裝LOTComposition對象,當(dāng)然還會有一些緩存獲取盹沈,值判斷等的邏輯龄章,但是核心就是產(chǎn)生一個(gè)LOTComposition對象:
+ (nullable instancetype)animationNamed:(nonnull NSString *)animationName inBundle:(nonnull NSBundle *)bundle {
...
if (JSONObject && !error) {
LOTComposition *laScene = [[self alloc] initWithJSON:JSONObject withAssetBundle:bundle];
[[LOTAnimationCache sharedCache] addAnimation:laScene forKey:animationName];
laScene.cacheKey = animationName;
return laScene;
}
NSLog(@"%s: Animation Not Found", __PRETTY_FUNCTION__);
return nil;
}
2)LOTComposition類用來解析整個(gè)動畫的json字典,獲取整個(gè)動畫所需的數(shù)據(jù)乞封。
- (void)_mapFromJSON:(NSDictionary *)jsonDictionary
withAssetBundle:(NSBundle *)bundle {
NSNumber *width = jsonDictionary[@"w"];
NSNumber *height = jsonDictionary[@"h"];
if (width && height) {
CGRect bounds = CGRectMake(0, 0, width.floatValue, height.floatValue);
_compBounds = bounds;
}
_startFrame = [jsonDictionary[@"ip"] copy];
_endFrame = [jsonDictionary[@"op"] copy];
_framerate = [jsonDictionary[@"fr"] copy];
if (_startFrame && _endFrame && _framerate) {
NSInteger frameDuration = (_endFrame.integerValue - _startFrame.integerValue) - 1;
NSTimeInterval timeDuration = frameDuration / _framerate.floatValue;
_timeDuration = timeDuration;
}
NSArray *assetArray = jsonDictionary[@"assets"];
if (assetArray.count) {
_assetGroup = [[LOTAssetGroup alloc] initWithJSON:assetArray withAssetBundle:bundle withFramerate:_framerate];
}
NSArray *layersJSON = jsonDictionary[@"layers"];
if (layersJSON) {
_layerGroup = [[LOTLayerGroup alloc] initWithLayerJSON:layersJSON
withAssetGroup:_assetGroup
withFramerate:_framerate];
}
[_assetGroup finalizeInitializationWithFramerate:_framerate];
}
在對JSON字典的解析過程中做裙,會拆分成幾種不同的信息,包括:整體關(guān)鍵幀信息肃晚、所需圖片資源信息菇用、所有子layer的信息。并將圖片組和layer組分別傳入到LOTAssetGroup和LOTLayerGroup中做進(jìn)一步處理陷揪。
3)LOTLayerGroup類用于解析JSON中“l(fā)ayers”層的數(shù)據(jù)惋鸥,并將單獨(dú)的layer數(shù)據(jù)傳遞給LOTLayer處理杂穷。核心代碼如下:
- (void)_mapFromJSON:(NSArray *)layersJSON
withAssetGroup:(LOTAssetGroup * _Nullable)assetGroup
withFramerate:(NSNumber *)framerate {
NSMutableArray *layers = [NSMutableArray array];
NSMutableDictionary *modelMap = [NSMutableDictionary dictionary];
NSMutableDictionary *referenceMap = [NSMutableDictionary dictionary];
for (NSDictionary *layerJSON in layersJSON) {
LOTLayer *layer = [[LOTLayer alloc] initWithJSON:layerJSON
withAssetGroup:assetGroup
withFramerate:framerate];
[layers addObject:layer];
modelMap[layer.layerID] = layer;
if (layer.referenceID) {
referenceMap[layer.referenceID] = layer;
}
}
_referenceIDMap = referenceMap;
_modelMap = modelMap;
_layers = layers;
}
4)接下來進(jìn)入LOTLayer類中,這里最終把json文件中單個(gè)layer對應(yīng)的數(shù)據(jù)映射出來卦绣。
@property (nonatomic, readonly) NSString *layerName;
@property (nonatomic, readonly, nullable) NSString *referenceID;
@property (nonatomic, readonly) NSNumber *layerID;
@property (nonatomic, readonly) LOTLayerType layerType;
@property (nonatomic, readonly, nullable) NSNumber *parentID;
@property (nonatomic, readonly) NSNumber *startFrame;
@property (nonatomic, readonly) NSNumber *inFrame;
@property (nonatomic, readonly) NSNumber *outFrame;
@property (nonatomic, readonly) NSNumber *timeStretch;
@property (nonatomic, readonly) CGRect layerBounds;
@property (nonatomic, readonly, nullable) NSArray<LOTShapeGroup *> *shapes;
@property (nonatomic, readonly, nullable) NSArray<LOTMask *> *masks;
以上屬性和json文件對應(yīng)的key有一一對應(yīng)關(guān)系耐量,比如layerName對應(yīng)json文件中的nm,layerType對應(yīng)ty等等滤港,每個(gè)layer中包含Layer所需的基本信息廊蜒,transform變化需要的則是每個(gè)LOTKeyframeGroup 類型的屬性。這里面包含了該Layer 的 transform變化的關(guān)鍵幀數(shù)組溅漾,而masks 和 shapes 的信息包含在上面的兩個(gè)同名數(shù)組中山叮。
5)前面四步,已經(jīng)把動畫需要的數(shù)據(jù)全部準(zhǔn)備好了添履,接下來就需要進(jìn)行動畫顯示屁倔。
最底層的LOTLayerContainer繼承自CALayer,添加了currentFrame屬性暮胧,LOTCompositionContainer又是繼承自LOTLayerContainer锐借,為LOTCompositionContainer對象添加了一個(gè)CABaseAnimation動畫,然后重寫CALayer的display方法往衷,在display方法中通過 CALayer中的presentationLayer獲取在動畫中變化的currentFrame數(shù)值 钞翔,再通過遍歷每一個(gè)子layer,將更新后的currentFrame傳入席舍,來實(shí)時(shí)更新每一個(gè)子Layer的顯示布轿。核心代碼在LOTLayerContainer中,如下:
- (void)displayWithFrame:(NSNumber *)frame forceUpdate:(BOOL)forceUpdate {
NSNumber *newFrame = @(frame.floatValue / self.timeStretchFactor.floatValue);
if (ENABLE_DEBUG_LOGGING) NSLog(@"View %@ Displaying Frame %@, with local time %@", self, frame, newFrame);
BOOL hidden = NO;
if (_inFrame && _outFrame) {
hidden = (frame.floatValue < _inFrame.floatValue ||
frame.floatValue > _outFrame.floatValue);
}
self.hidden = hidden;
if (hidden) {
return;
}
if (_opacityInterpolator && [_opacityInterpolator hasUpdateForFrame:newFrame]) {
self.opacity = [_opacityInterpolator floatValueForFrame:newFrame];
}
if (_transformInterpolator && [_transformInterpolator hasUpdateForFrame:newFrame]) {
_wrapperLayer.transform = [_transformInterpolator transformForFrame:newFrame];
}
[_contentsGroup updateWithFrame:newFrame withModifierBlock:nil forceLocalUpdate:forceUpdate];
_maskLayer.currentFrame = newFrame;
}
它實(shí)際上完成了以下幾件事:
1.根據(jù)子Layer的起始幀和結(jié)束幀判斷當(dāng)前幀子Layer是否顯示
2.更新子Layer當(dāng)前幀的透明度
3.更新子Layer當(dāng)前幀的transform
4.更新子Layer中路徑和形狀等內(nèi)容的變化
6)上面動畫顯示的2来颤,3驮捍,4步都是通過XXInterpolator這些類,來從當(dāng)前frame中計(jì)算出我們需要的值脚曾,我們以LOTTransformInterpolator為例东且,其他類似,看看它都有些什么:
@property (nonatomic, readonly) LOTPointInterpolator *positionInterpolator;
@property (nonatomic, readonly) LOTPointInterpolator *anchorInterpolator;
@property (nonatomic, readonly) LOTSizeInterpolator *scaleInterpolator;
@property (nonatomic, readonly) LOTNumberInterpolator *rotationInterpolator;
@property (nonatomic, readonly) LOTNumberInterpolator *positionXInterpolator;
@property (nonatomic, readonly) LOTNumberInterpolator *positionYInterpolator;
針對transform變換需要很多的信息本讥,LOTTransformInterpolator中提供了這些所需的信息珊泳。
當(dāng)傳入當(dāng)前frame時(shí),這些interpolator會返回不同的數(shù)值拷沸,從而組成當(dāng)前的transform色查。這些不同的Interpolar會根據(jù)自己的算法返回當(dāng)前所需要的值,但是他們大體的流程都是一樣的:
1.在關(guān)鍵幀數(shù)組中找到當(dāng)前frame的前一個(gè)關(guān)鍵幀(leadingKeyframe)和后一個(gè)關(guān)鍵幀(trailingKeyframe)
2.計(jì)算當(dāng)前frame 在 leadingKeyframe 和 trailingKeyframe 的進(jìn)度(progress)
3.根據(jù)這個(gè)progress以及 leadingKeyframe撞芍,trailingKeyframe算出當(dāng)前frame下的值秧了。(不同的Interpolator算法不同)
總結(jié):
Lottie提供了多種便利的方式,供我們加載酷炫的動畫序无,對用戶體驗(yàn)有極大的提升验毡。對使用者來說衡创,只需要引入包含動效的json文件和資源文件,調(diào)用lottie提供的屬性和api完成動畫繪制晶通。Lottie內(nèi)部幫我們做了json文件映射到不同類的不同屬性中璃氢,通過一系列的計(jì)算,確定出每一幀的數(shù)據(jù)狮辽,然后完美的顯示在屏幕上一也,這樣的神器,以后要多多用起來啦喉脖!
參考文章:Lottie動畫使用及原理分析
YYText 源碼剖析:CoreText 與異步繪制
YYImage 設(shè)計(jì)思路椰苟,實(shí)現(xiàn)細(xì)節(jié)剖析