這里介紹下iOS中加載本地gif的幾種方式箩退,我們?cè)谧詈笤倏偨Y(jié)這幾種方式的優(yōu)缺點(diǎn)
1.通過(guò)webview來(lái)進(jìn)行展示
-(void)loadGIFWithWebView
{
UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 350*2, 393)];
[webView setCenter:self.view.center];
NSData *gif = [NSData dataWithContentsOfFile: [[NSBundle mainBundle] pathForResource:@"2" ofType:@"gif"]];
webView.userInteractionEnabled = NO;
[webView loadData:gif MIMEType:@"image/gif" textEncodingName:@"UTF-8" baseURL:nil];
//設(shè)置webview背景透明,能看到gif的透明層
webView.backgroundColor = [UIColor blackColor];
webView.opaque = NO;
[self.view addSubview:webView];
}
這種方式是先創(chuàng)建一個(gè)webview苔可,然后通過(guò)加載data的方式展示出來(lái)
//畫一個(gè)分隔線表示一下區(qū)分
下面要說(shuō)的幾種方式都有一個(gè)共同點(diǎn),就是都用到了ImageI/O.framework
基本原理都是通過(guò)框架來(lái)獲取到圖片的信息,然后在配合動(dòng)畫或定時(shí)器來(lái)進(jìn)行展示亲桦。下面開始接著說(shuō)
2.這種方式是先對(duì)圖片進(jìn)行解析求晶,然后拿到圖片的相應(yīng)信息焰雕,最后再配合NSTimer進(jìn)行展示輪播。方法也是簡(jiǎn)單粗暴
自定義一個(gè)UIView來(lái)做gif的呈現(xiàn)布景
#import <UIKit/UIKit.h>
@interface CGImageGIFView : UIView
@property (nonatomic,assign,readonly) BOOL isAnimating;
-(instancetype)initWithGIFPath:(NSString *)path;
-(void)startGIF;
-(void)stopGIF;
@end
這里是實(shí)現(xiàn)文件的內(nèi)容芳杏,主要就是定義了幾個(gè)會(huì)用到的變量矩屁,別忘了引入ImageI/O.framework
#import <ImageIO/ImageIO.h>
@interface CGImageGIFView ()
{
//gif的字典屬性,定義了gif的一些特殊內(nèi)容爵赵,這里雖然設(shè)置了吝秕,但是沒啥特殊設(shè)置,一般情況下可以設(shè)置為NULL
NSDictionary *gifProperties;
size_t index;
size_t count;
CGImageSourceRef gifRef;
NSTimer *timer;
}
@property (nonatomic,assign,readwrite) BOOL isAnimating;
@end
這里是初始化完成的內(nèi)容
-(instancetype)initWithGIFPath:(NSString *)path
{
if (self = [super init]) {
//設(shè)置gif的屬性來(lái)獲取gif的圖片信息
gifProperties = [NSDictionary dictionaryWithObject:[NSDictionary dictionaryWithObject:@0 forKey:(NSString *)kCGImagePropertyGIFLoopCount]
forKey:(NSString *)kCGImagePropertyGIFDictionary];
//這個(gè)是拿到圖片的信息
gifRef = CGImageSourceCreateWithURL((CFURLRef)[NSURL fileURLWithPath:path], (CFDictionaryRef)gifProperties);
//這個(gè)拿到的是圖片的張數(shù)空幻,一張gif其實(shí)內(nèi)部是有好幾張圖片組合在一起的烁峭,如果是普通圖片的話,拿到的數(shù)就等于1
count = CGImageSourceGetCount(gifRef);
UIImage *image = [UIImage imageWithContentsOfFile:path];
self.frame = CGRectMake(0, 0, image.size.width, image.size.height);
self.isAnimating = NO;
}
return self;
}
開始和結(jié)束
-(void)startGIF
{
//開始動(dòng)畫秕铛,啟動(dòng)一個(gè)定時(shí)器约郁,每隔一段時(shí)間調(diào)用一次方法,切換圖片
if (timer == nil) {
timer = [NSTimer scheduledTimerWithTimeInterval:0.12 target:self selector:@selector(play) userInfo:nil repeats:YES];
}
[timer fire];
self.isAnimating = YES;
}
-(void)play
{
index = index + 1;
index= index % count;
//方法的內(nèi)容是根據(jù)上面拿到的imageSource來(lái)獲取gif內(nèi)部的第幾張圖片但两,拿到后在進(jìn)行l(wèi)ayer重新填充
CGImageRef currentRef = CGImageSourceCreateImageAtIndex(gifRef, index, (CFDictionaryRef)gifProperties);
self.layer.contents = (id)CFBridgingRelease(currentRef);
}
-(void)stopGIF
{
//停止定時(shí)器
self.isAnimating = NO;
[timer invalidate];
timer = nil;
}
第二種方式的介紹也到此結(jié)束鬓梅,主要就是先拿到圖片詳細(xì)詳細(xì)信息,然后根據(jù)一個(gè)定時(shí)器谨湘,在進(jìn)行切換绽快,每張圖片展示時(shí)間相同.
3.上面的方法說(shuō)到,每張圖片的展示時(shí)間相同紧阔,原因也像上面那樣是通過(guò)定時(shí)器來(lái)實(shí)現(xiàn)的谎僻,可現(xiàn)實(shí)中有的gif的圖片每張的展示時(shí)間不一定是相同的,還有可能不同寓辱,下面的方法就可以實(shí)現(xiàn)這種需求.
通過(guò)CAKeyframeAnimation來(lái)實(shí)現(xiàn)此操作
在創(chuàng)建一個(gè)自定義UIView后,第一步還是通過(guò)CGImageSourceRef來(lái)獲取圖片詳細(xì)信息赤拒,在上面的基礎(chǔ)上秫筏,這里又增加了一個(gè)內(nèi)容,定義如下變量
@interface CAKeyframeAnimationGIFView ()
{
//解析gif后每一張圖片的顯示時(shí)間
NSMutableArray *timeArray;
//解析gif后的每一張圖片數(shù)組
NSMutableArray *imageArray;
//gif動(dòng)畫總時(shí)間
CGFloat totalTime;
//gif寬度
CGFloat width;
//gif高度
CGFloat height;
}
取相應(yīng)值
void configImage(CFURLRef url,NSMutableArray *timeArray,NSMutableArray *imageArray,CGFloat *width,CGFloat *height,CGFloat *totalTime)
{
NSDictionary *gifProperty = [NSDictionary dictionaryWithObject:@{@0:(NSString *)kCGImagePropertyGIFLoopCount} forKey:(NSString *)kCGImagePropertyGIFDictionary];
//拿到ImageSourceRef后獲取gif內(nèi)部圖片個(gè)數(shù)
CGImageSourceRef ref = CGImageSourceCreateWithURL(url, (CFDictionaryRef)gifProperty);
size_t count = CGImageSourceGetCount(ref);
for (int i = 0; i < count; i++) {
//添加圖片
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(ref, i, (CFDictionaryRef)gifProperty);
[imageArray addObject:CFBridgingRelease(imageRef)];
//取每張圖片的圖片屬性,是一個(gè)字典
NSDictionary *dict = CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(ref, i, (CFDictionaryRef)gifProperty));
//取寬高
if (width != NULL && height != NULL) {
*width = [[dict valueForKey:(NSString *)kCGImagePropertyPixelWidth] floatValue];
*height = [[dict valueForKey:(NSString *)kCGImagePropertyPixelHeight] floatValue];
}
//添加每一幀時(shí)間
NSDictionary *tmp = [dict valueForKey:(NSString *)kCGImagePropertyGIFDictionary];
[timeArray addObject:[tmp valueForKey:(NSString *)kCGImagePropertyGIFDelayTime]];
//總時(shí)間
*totalTime = *totalTime + [[tmp valueForKey:(NSString *)kCGImagePropertyGIFDelayTime] floatValue];
}
}
開始gif動(dòng)畫诱鞠,是通過(guò)關(guān)鍵幀動(dòng)畫來(lái)實(shí)現(xiàn)動(dòng)畫的展示
-(void)startGIF
{
self.isAnimating = YES;
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
//獲取每幀動(dòng)畫起始時(shí)間在總時(shí)間的百分比
NSMutableArray *percentageArray = [NSMutableArray array];
CGFloat currentTime = 0.0;
for (int i = 0; i < timeArray.count; i++) {
NSNumber *percentage = [NSNumber numberWithFloat:currentTime/totalTime];
[percentageArray addObject:percentage];
currentTime = currentTime + [[timeArray objectAtIndex:i] floatValue];
}
[animation setKeyTimes:percentageArray];
//添加每幀動(dòng)畫
[animation setValues:imageArray];
//動(dòng)畫信息基本設(shè)置
[animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]];
[animation setDuration:totalTime];
[animation setDelegate:self];
[animation setRepeatCount:1000];
//添加動(dòng)畫
[self.layer addAnimation:animation forKey:@"gif"];
}
-(void)stopGIF
{
self.isAnimating = NO;
[self.layer removeAllAnimations];
}
這里設(shè)置repeatcount為1000,可以自行設(shè)置具體內(nèi)容值大小
另外这敬,你還可以自行更改每張圖片的展示時(shí)間航夺,可以自己控制
附帶動(dòng)畫結(jié)束后的回調(diào)方法
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
self.layer.contents = nil;
self.isAnimating = NO;
}
4.在嘗試了上面的三種方式后,總覺得在性能上或多或少的有些缺陷崔涂,尤其是第三種阳掐,雖說(shuō)可以自定義顯示時(shí)間,但是總是感覺很卡頓冷蚂,下面就說(shuō)下最后一種方式缭保,通過(guò)CADisplayLink來(lái)進(jìn)行g(shù)if的動(dòng)畫展示,這個(gè)方式最推薦
先來(lái)介紹下什么是CADisplayLink
文檔是這樣一句話介紹的
/** Class representing a timer bound to the display vsync. **/
我的理解是蝙茶,CADisplayLink是一個(gè)將定時(shí)器綁定到顯示屏上負(fù)責(zé)垂直同步的類
至于什么是垂直同步艺骂,那就是游戲領(lǐng)域的詞了,百度后簡(jiǎn)單理解這個(gè)詞是能在第一幀繪制成功后隆夯,在進(jìn)行第二幀的繪制钳恕,這樣就不會(huì)再低端性能機(jī)上感到跳幀
跑遠(yuǎn)了,這個(gè)類通過(guò)target-action方式來(lái)綁定一個(gè)target,然后在屏幕進(jìn)行刷新的時(shí)候調(diào)用action這個(gè)方法,特別注意,我們知道iPhone的屏幕刷新頻率是每秒60次,也就是說(shuō)fps是60,通過(guò)這個(gè)可以在每次屏幕刷新的時(shí)候都調(diào)用一次這個(gè)方法蹄衷,也就是說(shuō)調(diào)用頻率會(huì)很高
還是第一步忧额,先獲取圖片的詳細(xì)信息
這次通過(guò)自定義一個(gè)UIImage來(lái)解析圖片
先看初始化方法
//創(chuàng)建gif圖片
-(instancetype)initWithCGImageSource:(CGImageSourceRef)imageSource scale:(CGFloat)scale
{
self = [super init];
if (!imageSource || !self) {
return nil;
}
CFRetain(imageSource);
size_t numberOfFrames = CGImageSourceGetCount(imageSource);
NSDictionary *imageProperties = CFBridgingRelease(CGImageSourceCopyProperties(imageSource, NULL));
NSDictionary *gifProerties = [imageProperties objectForKey:(NSString *)kCGImagePropertyGIFDictionary];
//開辟空間
self.frameDurations = malloc(numberOfFrames);
//讀取循環(huán)次數(shù)
self.loopCount = [[gifProerties objectForKey:(NSString *)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];
//創(chuàng)建所有圖片的數(shù)值
self.images = [NSMutableArray arrayWithCapacity:numberOfFrames];
NSNull *aNull = [NSNull null];
for (NSUInteger i = 0; i < numberOfFrames; i++) {
[self.images addObject:aNull];
//讀取每張土拍的顯示時(shí)間,添加到數(shù)組中,并計(jì)算總時(shí)間
NSTimeInterval frameDuration = CGImageSourceGetGifFrameDelay(imageSource,i);
self.frameDurations[i] = frameDuration;
self.totalDuratoin += frameDuration;
}
NSUInteger num = MIN(_prefetchedNum, numberOfFrames);
for (int i = 0; i < num; i++) {
//替換讀取到的每一張圖片
CGImageRef image = CGImageSourceCreateImageAtIndex(imageSource, i, NULL);
[self.images replaceObjectAtIndex:i withObject:[UIImage imageWithCGImage:image scale:scale orientation:UIImageOrientationUp]];
CGImageRelease(image);
}
//釋放資源,創(chuàng)建子隊(duì)列
_imageSourceRef = imageSource;
CFRetain(_imageSourceRef);
CFRelease(imageSource);
_scale = scale;
readFrameQueue = dispatch_queue_create("cn.bourbonz.www", DISPATCH_QUEUE_SERIAL);
return self;
}
第二部分的關(guān)鍵是取每個(gè)位置對(duì)應(yīng)的圖片,這里用到了一個(gè)算法
每次只保留10個(gè)圖片,并隨著時(shí)間的增加愧口,新添新圖片睦番,并移除超出10各部分的就圖片,節(jié)省內(nèi)存
#pragma mark custom method
-(UIImage *)getFrameWithIndex:(NSUInteger)idx
{
//根據(jù)當(dāng)前index 來(lái)獲取gif圖片的第幾個(gè)圖片
UIImage *frame = nil;
@synchronized (self.images) {
frame = self.images[idx];
}
//放回對(duì)應(yīng)index的圖片
if (!frame) {
CGImageRef image = CGImageSourceCreateImageAtIndex(_imageSourceRef, idx, NULL);
frame = [UIImage imageWithCGImage:image scale:_scale orientation:UIImageOrientationUp];
CFRelease(image);
}
/**
* 如果圖片張數(shù)大于10调卑,進(jìn)行如下操作的目的是
由于該方法會(huì)頻繁調(diào)用抡砂,為加快速度和節(jié)省內(nèi)存,對(duì)取值所在的數(shù)組進(jìn)行了替換恬涧,只保留10個(gè)內(nèi)容
并隨著的不斷增大注益,對(duì)原來(lái)被替換的內(nèi)容進(jìn)行還原,但是被還原的個(gè)數(shù)和保留的個(gè)數(shù)總共為10個(gè)溯捆,這個(gè)是最開始進(jìn)行的設(shè)置的大小
*/
if (self.images.count > _prefetchedNum) {
if (idx != 0) {
[self.images replaceObjectAtIndex:idx withObject:[NSNull null]];
}
NSUInteger nextReadIdx = idx + _prefetchedNum;
for (NSUInteger i = idx + 1; i <= nextReadIdx; i++) {
//保證每次的index都小于數(shù)組個(gè)數(shù)丑搔,從而使最大值的下一個(gè)是最小值
NSUInteger _idx = i%self.images.count;
if ([self.images[_idx] isKindOfClass:[NSNull class]]) {
dispatch_async(readFrameQueue, ^{
CGImageRef image = CGImageSourceCreateImageAtIndex(_imageSourceRef, _idx, NULL);
@synchronized (self.images) {
[self.images replaceObjectAtIndex:_idx withObject:[UIImage imageWithCGImage:image scale:_scale orientation:UIImageOrientationUp]];
}
CFRelease(image);
});
}
}
}
return frame;
}
第三步,新建一個(gè)UIImageView的子類,來(lái)加載剛才新建的UIImage
先看一些屬性的設(shè)定提揍,由于CADisplayLink是依賴在runloop的啤月,所以需要將imageview的runloop屬性進(jìn)行重寫
-(CADisplayLink *)displayLink
{
//如果有superview就是已經(jīng)創(chuàng)建了,創(chuàng)建時(shí)新建一個(gè)CADisplayLink劳跃,并制定方法谎仲,最后加到一個(gè)Runloop中,完成創(chuàng)建
if (self.superview) {
if (!_displayLink && self.animatedImage) {
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(changeKeyframe:)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];
}
}else{
[_displayLink invalidate];
_displayLink = nil;
}
return _displayLink;
}
-(NSString *)runLoopMode
{
return _runLoopMode ?: NSRunLoopCommonModes;
}
-(void)setRunLoopMode:(NSString *)runLoopMode{
//這個(gè)地方需要重寫刨仑,因?yàn)镃ADisplayLink是依賴在runloop中的郑诺,所以如果設(shè)置了imageview的runloop的話
//就要停止動(dòng)畫夹姥,并重新設(shè)置CADisplayLink對(duì)應(yīng)的runloop,最后在根據(jù)情況是否開始動(dòng)畫
if (runLoopMode != _runLoopMode) {
[self stopAnimating];
NSRunLoop *runloop = [NSRunLoop mainRunLoop];
[self.displayLink removeFromRunLoop:runloop forMode:_runLoopMode];
[self.displayLink addToRunLoop:runloop forMode:runLoopMode];
_runLoopMode = runLoopMode;
[self startAnimating];
}
}
setImage:方法是需要重寫的辙诞,這里完成的操作是設(shè)置靜止態(tài)時(shí)UIImageView的顯示樣式辙售,判斷是否是gif。如果是飞涂,就取值第一張旦部,如果不是就直接顯示,并對(duì)一些屬性值進(jìn)行設(shè)置和重新繪制较店,最后根據(jù)情況來(lái)是否開始動(dòng)畫
-(void)setImage:(UIImage *)image
{
if (image == self.image) {
return;
}
[self stopAnimating];
self.currentFrameIndex = 0;
self.loopCountdown = 0;
self.accumulator = 0;
if ([image isKindOfClass:[CADisplayLineImage class]] && image.images) {
//設(shè)置靜止態(tài)的圖片
if (image.images[0]) {
[super setImage:image.images[0]];
}else{
[super setImage:nil];
}
self.currentFrame = nil;
self.animatedImage = (CADisplayLineImage *)image;
self.loopCountdown = self.animatedImage.loopCount ? : NSUIntegerMax;
[self startAnimating];
}else{
self.animatedImage = nil;
[super setImage:image];
}
[self.layer setNeedsDisplay];
}
這里是關(guān)鍵的方法士八,頻繁的調(diào)用,頻繁的繪制圖片
//切換動(dòng)畫的關(guān)鍵方法
-(void)changeKeyframe:(CADisplayLink *)displayLink
{
if (self.currentFrameIndex >= self.animatedImage.images.count) {
return;
}
//這里就是不停的取圖泽西,不停的設(shè)置曹铃,然后不停的調(diào)用displayLayer:方法
self.accumulator += fmin(displayLink.duration, kMaxTimeStep);
while (self.accumulator >= self.animatedImage.frameDurations[self.currentFrameIndex]) {
self.accumulator -= self.animatedImage.frameDurations[self.currentFrameIndex];
if (++self.currentFrameIndex >= self.animatedImage.images.count) {
if (--self.loopCountdown == 0) {
[self stopAnimating];
return;
}
self.currentFrameIndex = 0;
}
self.currentFrameIndex = MIN(self.currentFrameIndex, self.animatedImage.images.count - 1);
self.currentFrame = [self.animatedImage getFrameWithIndex:self.currentFrameIndex];
[self.layer setNeedsDisplay];
}
}
//繪制圖片
-(void)displayLayer:(CALayer *)layer
{
if (!self.animatedImage || [self.animatedImage.images count] == 0) {
return;
}
if(self.currentFrame && ![self.currentFrame isKindOfClass:[NSNull class]]){
layer.contents = (__bridge id)([self.currentFrame CGImage]);
}
}
這樣就基本完成了設(shè)置,就可以顯示了
最后總結(jié)下這個(gè)方法的優(yōu)缺點(diǎn)
方法 | 優(yōu)點(diǎn) | 缺點(diǎn) |
---|---|---|
1 | 方便快捷 | 新添一個(gè)webview捧杉,不能控制圖片的開始和結(jié)束 |
2 | 可以控制開始和結(jié)束 | 新建timer陕见,控制時(shí)間不準(zhǔn)確,不能確定每張顯示時(shí)間 |
3 | 可以控制開始和結(jié)束味抖,\能控制沒張顯示時(shí)間 | 性能上明顯不占優(yōu)评甜,略占用內(nèi)存 |
4 | 具備以上所有優(yōu)點(diǎn) | 相對(duì)較復(fù)雜 |
歡迎各位在評(píng)論下面進(jìn)行留言或點(diǎn)贊,(づ ̄ 3 ̄)づ
點(diǎn)我下載代碼