iOS-加載gif的四種方式

這里介紹下iOS中加載本地gif的幾種方式,我們在最后再總結(jié)這幾種方式的優(yōu)缺點(diǎn)

1.通過webview來進(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,然后通過加載data的方式展示出來

效果圖

//畫一個(gè)分隔線表示一下區(qū)分
下面要說的幾種方式都有一個(gè)共同點(diǎn),就是都用到了ImageI/O.framework
基本原理都是通過框架來獲取到圖片的信息,然后在配合動畫或定時(shí)器來進(jìn)行展示。下面開始接著說


2.這種方式是先對圖片進(jìn)行解析,然后拿到圖片的相應(yīng)信息,最后再配合NSTimer進(jìn)行展示輪播。方法也是簡單粗暴
自定義一個(gè)UIView來做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è)會用到的變量,別忘了引入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的屬性來獲取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
{
    //開始動畫,啟動一個(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來獲取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.上面的方法說到,每張圖片的展示時(shí)間相同,原因也像上面那樣是通過定時(shí)器來實(shí)現(xiàn)的,可現(xiàn)實(shí)中有的gif的圖片每張的展示時(shí)間不一定是相同的,還有可能不同,下面的方法就可以實(shí)現(xiàn)這種需求.
通過CAKeyframeAnimation來實(shí)現(xiàn)此操作
在創(chuàng)建一個(gè)自定義UIView后,第一步還是通過CGImageSourceRef來獲取圖片詳細(xì)信息,在上面的基礎(chǔ)上,這里又增加了一個(gè)內(nèi)容,定義如下變量

@interface CAKeyframeAnimationGIFView ()
{
    //解析gif后每一張圖片的顯示時(shí)間
    NSMutableArray *timeArray;
    //解析gif后的每一張圖片數(shù)組
    NSMutableArray *imageArray;
    //gif動畫總時(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動畫,是通過關(guān)鍵幀動畫來實(shí)現(xiàn)動畫的展示

-(void)startGIF
{
    self.isAnimating = YES;
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
    
    //獲取每幀動畫起始時(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];
    
    //添加每幀動畫
    [animation setValues:imageArray];
    //動畫信息基本設(shè)置
    [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]];
    [animation setDuration:totalTime];
    [animation setDelegate:self];
    [animation setRepeatCount:1000];
    
    //添加動畫
    [self.layer addAnimation:animation forKey:@"gif"];
    
}
-(void)stopGIF
{
    self.isAnimating = NO;
    [self.layer removeAllAnimations];
}

這里設(shè)置repeatcount為1000,可以自行設(shè)置具體內(nèi)容值大小
另外,你還可以自行更改每張圖片的展示時(shí)間,可以自己控制
附帶動畫結(jié)束后的回調(diào)方法

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
    self.layer.contents = nil;
    self.isAnimating = NO;
}
效果圖

4.在嘗試了上面的三種方式后,總覺得在性能上或多或少的有些缺陷,尤其是第三種,雖說可以自定義顯示時(shí)間,但是總是感覺很卡頓,下面就說下最后一種方式,通過CADisplayLink來進(jìn)行g(shù)if的動畫展示,這個(gè)方式最推薦


先來介紹下什么是CADisplayLink
文檔是這樣一句話介紹的

/** Class representing a timer bound to the display vsync. **/

我的理解是,CADisplayLink是一個(gè)將定時(shí)器綁定到顯示屏上負(fù)責(zé)垂直同步的類
至于什么是垂直同步,那就是游戲領(lǐng)域的詞了,百度后簡單理解這個(gè)詞是能在第一幀繪制成功后,在進(jìn)行第二幀的繪制,這樣就不會再低端性能機(jī)上感到跳幀
跑遠(yuǎn)了,這個(gè)類通過target-action方式來綁定一個(gè)target,然后在屏幕進(jìn)行刷新的時(shí)候調(diào)用action這個(gè)方法,特別注意,我們知道iPhone的屏幕刷新頻率是每秒60次,也就是說fps是60,通過這個(gè)可以在每次屏幕刷新的時(shí)候都調(diào)用一次這個(gè)方法,也就是說調(diào)用頻率會很高


還是第一步,先獲取圖片的詳細(xì)信息
這次通過自定義一個(gè)UIImage來解析圖片
先看初始化方法

//創(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è)位置對應(yīng)的圖片,這里用到了一個(gè)算法
每次只保留10個(gè)圖片,并隨著時(shí)間的增加,新添新圖片,并移除超出10各部分的就圖片,節(jié)省內(nèi)存

#pragma mark custom method
-(UIImage *)getFrameWithIndex:(NSUInteger)idx
{
    //根據(jù)當(dāng)前index 來獲取gif圖片的第幾個(gè)圖片
    UIImage *frame = nil;
    @synchronized (self.images) {
        frame = self.images[idx];
    }
    //放回對應(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)行如下操作的目的是
        由于該方法會頻繁調(diào)用,為加快速度和節(jié)省內(nèi)存,對取值所在的數(shù)組進(jìn)行了替換,只保留10個(gè)內(nè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的子類,來加載剛才新建的UIImage
先看一些屬性的設(shè)定,由于CADisplayLink是依賴在runloop的,所以需要將imageviewrunloop屬性進(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的話
    //就要停止動畫,并重新設(shè)置CADisplayLink對應(yīng)的runloop,最后在根據(jù)情況是否開始動畫
    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。如果是,就取值第一張,如果不是就直接顯示,并對一些屬性值進(jìn)行設(shè)置和重新繪制,最后根據(jù)情況來是否開始動畫

-(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)用,頻繁的繪制圖片

//切換動畫的關(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) 相對較復(fù)雜

歡迎各位在評論下面進(jìn)行留言或點(diǎn)贊,(づ ̄ 3 ̄)づ
點(diǎn)我下載代碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,124評論 25 708
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,206評論 4 61
  • 從小爸媽對我說,以后把成小敏介紹給你當(dāng)媳婦,你干不干(方言)?我說干(還是方言)。后來我長大了,才發(fā)現(xiàn)我干不了。人...
    白頭少日記閱讀 359評論 0 2
  • 食品與我們的生活息息相關(guān),放心食品白名單與老百姓緊密相連! 在日常生活中,我們基本每天都會吃上一些紅棗。在我們的放...
    放心365閱讀 606評論 0 2
  • 再漂亮的手機(jī)殼,剛開始看著新鮮喜歡,時(shí)間久了,漂亮的外殼會被刮花褪色,膩了厭了倦了,還是簡單實(shí)用最好。不知道自己要...
    周海雙閱讀 446評論 0 1