美文网首页iOSiOS相关iOS 程序员
iOS-加载gif的四种方式

iOS-加载gif的四种方式

作者: Bourbon | 来源:发表于2016-04-27 16:28 被阅读13695次

    这里介绍下iOS中加载本地gif的几种方式,我们在最后再总结这几种方式的优缺点

    1.通过webview来进行展示

    -(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];
        //设置webview背景透明,能看到gif的透明层
        webView.backgroundColor = [UIColor blackColor];
        webView.opaque = NO;
        [self.view addSubview:webView];
        
    }
    

    这种方式是先创建一个webview,然后通过加载data的方式展示出来

    效果图

    //画一个分隔线表示一下区分
    下面要说的几种方式都有一个共同点,就是都用到了ImageI/O.framework
    基本原理都是通过框架来获取到图片的信息,然后在配合动画或定时器来进行展示。下面开始接着说


    2.这种方式是先对图片进行解析,然后拿到图片的相应信息,最后再配合NSTimer进行展示轮播。方法也是简单粗暴
    自定义一个UIView来做gif的呈现布景

    #import <UIKit/UIKit.h>
    
    @interface CGImageGIFView : UIView
    
    @property (nonatomic,assign,readonly) BOOL isAnimating;
    
    -(instancetype)initWithGIFPath:(NSString *)path;
    
    -(void)startGIF;
    -(void)stopGIF;
    
    @end
    
    

    这里是实现文件的内容,主要就是定义了几个会用到的变量,别忘了引入ImageI/O.framework

    #import <ImageIO/ImageIO.h>
    @interface CGImageGIFView ()
    {
        //gif的字典属性,定义了gif的一些特殊内容,这里虽然设置了,但是没啥特殊设置,一般情况下可以设置为NULL
        NSDictionary *gifProperties;
        size_t index;
        size_t count;
        CGImageSourceRef gifRef;
        NSTimer *timer;
    }
    @property (nonatomic,assign,readwrite) BOOL isAnimating;
    @end
    

    这里是初始化完成的内容

    -(instancetype)initWithGIFPath:(NSString *)path
    {
        if (self = [super init]) {
            
            //设置gif的属性来获取gif的图片信息
            gifProperties = [NSDictionary dictionaryWithObject:[NSDictionary dictionaryWithObject:@0 forKey:(NSString *)kCGImagePropertyGIFLoopCount]
                                                        forKey:(NSString *)kCGImagePropertyGIFDictionary];
            //这个是拿到图片的信息
            gifRef = CGImageSourceCreateWithURL((CFURLRef)[NSURL fileURLWithPath:path], (CFDictionaryRef)gifProperties);
            //这个拿到的是图片的张数,一张gif其实内部是有好几张图片组合在一起的,如果是普通图片的话,拿到的数就等于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;
    }
    

    开始和结束

    -(void)startGIF
    {
        //开始动画,启动一个定时器,每隔一段时间调用一次方法,切换图片
        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;
        //方法的内容是根据上面拿到的imageSource来获取gif内部的第几张图片,拿到后在进行layer重新填充
        CGImageRef currentRef = CGImageSourceCreateImageAtIndex(gifRef, index, (CFDictionaryRef)gifProperties);
        self.layer.contents = (id)CFBridgingRelease(currentRef);
    }
    -(void)stopGIF
    {
        //停止定时器
        self.isAnimating = NO;
        [timer invalidate];
        timer = nil;
    }
    

    第二种方式的介绍也到此结束,主要就是先拿到图片详细详细信息,然后根据一个定时器,在进行切换,每张图片展示时间相同.

    效果图

    3.上面的方法说到,每张图片的展示时间相同,原因也像上面那样是通过定时器来实现的,可现实中有的gif的图片每张的展示时间不一定是相同的,还有可能不同,下面的方法就可以实现这种需求.
    通过CAKeyframeAnimation来实现此操作
    在创建一个自定义UIView后,第一步还是通过CGImageSourceRef来获取图片详细信息,在上面的基础上,这里又增加了一个内容,定义如下变量

    @interface CAKeyframeAnimationGIFView ()
    {
        //解析gif后每一张图片的显示时间
        NSMutableArray *timeArray;
        //解析gif后的每一张图片数组
        NSMutableArray *imageArray;
        //gif动画总时间
        CGFloat totalTime;
        //gif宽度
        CGFloat width;
        //gif高度
        CGFloat height;
    }
    

    取相应值

    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内部图片个数
        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)];
            
            //取每张图片的图片属性,是一个字典
            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];
            }
            
            //添加每一帧时间
            NSDictionary *tmp = [dict valueForKey:(NSString *)kCGImagePropertyGIFDictionary];
            [timeArray addObject:[tmp valueForKey:(NSString *)kCGImagePropertyGIFDelayTime]];
            
            //总时间
            *totalTime = *totalTime + [[tmp valueForKey:(NSString *)kCGImagePropertyGIFDelayTime] floatValue];
        }
    }
    

    开始gif动画,是通过关键帧动画来实现动画的展示

    -(void)startGIF
    {
        self.isAnimating = YES;
        CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
        
        //获取每帧动画起始时间在总时间的百分比
        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];
        //动画信息基本设置
        [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];
    }
    

    这里设置repeatcount为1000,可以自行设置具体内容值大小
    另外,你还可以自行更改每张图片的展示时间,可以自己控制
    附带动画结束后的回调方法

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

    4.在尝试了上面的三种方式后,总觉得在性能上或多或少的有些缺陷,尤其是第三种,虽说可以自定义显示时间,但是总是感觉很卡顿,下面就说下最后一种方式,通过CADisplayLink来进行gif的动画展示,这个方式最推荐


    先来介绍下什么是CADisplayLink
    文档是这样一句话介绍的

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

    我的理解是,CADisplayLink是一个将定时器绑定到显示屏上负责垂直同步的类
    至于什么是垂直同步,那就是游戏领域的词了,百度后简单理解这个词是能在第一帧绘制成功后,在进行第二帧的绘制,这样就不会再低端性能机上感到跳帧
    跑远了,这个类通过target-action方式来绑定一个target,然后在屏幕进行刷新的时候调用action这个方法,特别注意,我们知道iPhone的屏幕刷新频率是每秒60次,也就是说fps是60,通过这个可以在每次屏幕刷新的时候都调用一次这个方法,也就是说调用频率会很高


    还是第一步,先获取图片的详细信息
    这次通过自定义一个UIImage来解析图片
    先看初始化方法

    //创建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);
        //读取循环次数
        self.loopCount = [[gifProerties objectForKey:(NSString *)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];
        //创建所有图片的数值
        self.images  = [NSMutableArray arrayWithCapacity:numberOfFrames];
        
        NSNull *aNull = [NSNull null];
        for (NSUInteger i = 0; i < numberOfFrames; i++) {
            [self.images addObject:aNull];
            //读取每张土拍的显示时间,添加到数组中,并计算总时间
            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);
        }
        //释放资源,创建子队列
        _imageSourceRef = imageSource;
        CFRetain(_imageSourceRef);
        CFRelease(imageSource);
        
        _scale = scale;
        
        readFrameQueue = dispatch_queue_create("cn.bourbonz.www", DISPATCH_QUEUE_SERIAL);
        
        return self;
    }
    
    

    第二部分的关键是取每个位置对应的图片,这里用到了一个算法
    每次只保留10个图片,并随着时间的增加,新添新图片,并移除超出10各部分的就图片,节省内存

    #pragma mark custom method
    -(UIImage *)getFrameWithIndex:(NSUInteger)idx
    {
        //根据当前index 来获取gif图片的第几个图片
        UIImage *frame = nil;
        @synchronized (self.images) {
            frame = self.images[idx];
        }
        //放回对应index的图片
        if (!frame) {
            CGImageRef image = CGImageSourceCreateImageAtIndex(_imageSourceRef, idx, NULL);
            frame = [UIImage imageWithCGImage:image scale:_scale orientation:UIImageOrientationUp];
            CFRelease(image);
        }
        /**
         *  如果图片张数大于10,进行如下操作的目的是
            由于该方法会频繁调用,为加快速度和节省内存,对取值所在的数组进行了替换,只保留10个内容
            并随着的不断增大,对原来被替换的内容进行还原,但是被还原的个数和保留的个数总共为10个,这个是最开始进行的设置的大小
         */
        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都小于数组个数,从而使最大值的下一个是最小值
                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;
    }
    
    效果图

    第三步,新建一个UIImageView的子类,来加载刚才新建的UIImage
    先看一些属性的设定,由于CADisplayLink是依赖在runloop的,所以需要将imageviewrunloop属性进行重写

    -(CADisplayLink *)displayLink
    {
        //如果有superview就是已经创建了,创建时新建一个CADisplayLink,并制定方法,最后加到一个Runloop中,完成创建
        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{
        //这个地方需要重写,因为CADisplayLink是依赖在runloop中的,所以如果设置了imageview的runloop的话
        //就要停止动画,并重新设置CADisplayLink对应的runloop,最后在根据情况是否开始动画
        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:方法是需要重写的,这里完成的操作是设置静止态时UIImageView的显示样式,判断是否是gif。如果是,就取值第一张,如果不是就直接显示,并对一些属性值进行设置和重新绘制,最后根据情况来是否开始动画

    -(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) {
            
            //设置静止态的图片
            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];
    }
    

    这里是关键的方法,频繁的调用,频繁的绘制图片

    //切换动画的关键方法
    -(void)changeKeyframe:(CADisplayLink *)displayLink
    {
        if (self.currentFrameIndex >= self.animatedImage.images.count) {
            return;
        }
        //这里就是不停的取图,不停的设置,然后不停的调用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]);
        }
    }
    

    这样就基本完成了设置,就可以显示了

    效果图

    最后总结下这个方法的优缺点

    方法 优点 缺点
    1 方便快捷 新添一个webview,不能控制图片的开始和结束
    2 可以控制开始和结束 新建timer,控制时间不准确,不能确定每张显示时间
    3 可以控制开始和结束,\能控制没张显示时间 性能上明显不占优,略占用内存
    4 具备以上所有优点 相对较复杂

    欢迎各位在评论下面进行留言或点赞,(づ ̄ 3 ̄)づ
    点我下载代码

    相关文章

      网友评论

      • huGh11:gif和webView大小不对应怎么办
      • 穷寇:另外,如果是网络图片有没有什么好的方式?先下载这种方式除外
      • 穷寇:第三种感觉卡顿是关键帧动画设置的问题吧,设置成kCAMediaTimingFunctionLinear类型就连贯了。
      • Ko_Neko:博主,请问下如果要在swift中使用你的类,displayImageView.image = CADisplayLineImage(named:"bg.gif") 会提示 Argument labels '(named:)' do not match any available overloads 这个你是如何解决的那?swift中的UIImage的子类是没有namd:这个初始化方法的
        Bourbon:我没有用到swift,所以这里没有处理和swift的混编,出现这个问题不太清楚具体原因
      • 心语风尚:可以 暂停 当前在进行的 gif 吗 而且继续动画
        Bourbon:不太明白,既然暂停了怎么还能继续动画
      • 2df6b75e5bec:你好,这个怎么控制GIF的旋转啊?
        Bourbon:可以直接使用view的transform进行旋转
      • wnido:不错,不错
      • IOS_绿豆糕:楼主 在IOS8 上 GIF为什么要小一些?尺寸不对了
      • VoiderSun:👍
        Bourbon:@莫lu 感谢支持
        十一遥:非常喜欢
        Bourbon:@VoiderSun 多谢多谢

      本文标题:iOS-加载gif的四种方式

      本文链接:https://www.haomeiwen.com/subject/qmecrttx.html