iOS 与前端程序员

iOS让GIF动起来

2018-05-07  本文已影响17人  学习路上一个远行者

用webView来进行展示gif图片

 UIWebView *webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
webView.scalesPageToFit = YES;
WKWebViewConfiguration *conf = [[WKWebViewConfiguration alloc] init];
WKWebView *wkwebView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:conf];
[wkwebView loadRequest:[NSURLRequest requestWithURL:fileURL]];

这两者之间的区别:
在拖动webView的时候,gif动画效果「会」出现暂停的效果。
在拖动wkwebView的时候,gif动画效果「不会」出现暂停的效果。wkWebView的性能高于uiwebView

了解框架ImageIO

1.GIF图片实质:
GIF图片其实一组图片,这一组图片进行播放实现了gif的动画效果。
2.代码:
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
//根据URL获取ImageSource对象,因为创建方法含有create,如果不需要对象的时候需要进行手动释放
CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)fileURL, NULL);
//获取有几张图片。如果静态图,那么是1,如果是gif图片可能包含多张图
size_t count = CGImageSourceGetCount(source);
NSMutableArray *images = [NSMutableArray arrayWithCapacity:count];
NSMutableArray *delayTimes = [NSMutableArray arrayWithCapacity:count];
for (int i = 0 ; i < count ; i ++) {
     //根据索引创建图片
     CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, i, NULL);
     //获取每张图片的属性
    NSDictionary *dic = CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(source, i, NULL));
      //获取图片的停留时间
     NSNumber *delay = dic[(__bridge NSString *)kCGImagePropertyGIFDictionary][(__bridge NSString *)kCGImagePropertyGIFUnclampedDelayTime];
    if ([delay floatValue] <= 0) {
            delay = dic[(__bridge NSString *)kCGImagePropertyGIFDictionary][(__bridge NSString *)kCGImagePropertyGIFDelayTime];
    }
     /*
    If a time of 50 milliseconds or less is specified, then the actual delay time stored in this parameter is 100 miliseconds. See
     官方解释,如果停留时间小于0.02s,停留时间应该是0.1s
     */
     if ([delay floatValue] - 0.02 < FLT_EPSILON) {
           delay = @(0.1);
    }
            
     _totalDelayTime += [delay floatValue];
            
     [images addObject:(__bridge id)imageRef];
     [delayTimes addObject:delay];
     CFRelease(imageRef);
     }
_images = images.copy;
        
_currentIndex = 0;
        
self.layer.contents = images.firstObject;
        
 _isPlaying = NO;
        
 CFRelease(source);
 }

上面的代码解释了利用框架来解析出来gif图片中包含的图片

3.根据获取的图片来进行播放。

拿到图片我要进行播放我们想到就是循环切换这些图片,那么我们自然想起了NSTimer dispatch_source(不讲解,因为没有做),CADisplayLink,这些是关于时间的调度器,还有一种播放图片的方式是关键帧动画CAKeyFrameAnimation,下面依次实现,并且说一下此种方式的不足

3.1NSTimer
if (_timer == nil || _timer.valid == NO) {
        // timeInterval,应该设置成我们从ImageSource中获取的delaytime
        _timer = [NSTimer scheduledTimerWithTimeInterval:0.25f target:self selector:@selector(playTimer) userInfo:nil repeats:YES];
}
[self.timer fire];
- (void)playTimer {
    self.currentIndex ++;
    if (self.currentIndex >= self.images.count) {
        self.currentIndex = 0;
    }
    //展示图片,用的是contents,这个属性
    self.layer.contents = self.images[self.currentIndex];
}

灵活性比较小,不能控制每个张图片停留的时间

3.2 CAKeyFrameAnimation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
animation.values = self.images;
CGFloat time = 0.0f;
NSMutableArray<NSNumber *> *keyTimes = [NSMutableArray arrayWithCapacity:self.delayTimes.count];
for (int i = 0 ; i < self.delayTimes.count; i ++) {
     CGFloat keyTime = time / self.totalDelayTime;
     [keyTimes addObject:@(keyTime)];
     keyTime += [self.delayTimes[i] floatValue];
}
animation.keyTimes = keyTimes;
    
animation.duration = self.totalDelayTime;
animation.repeatCount = NSIntegerMax;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
    
[self.layer addAnimation:animation forKey:@"gif"];

我们是以contents来做动画,而且我们可以根据keyTimes来设置每张图片的提留时间。相对timer是更加灵活一些。

我们以NSTimer的这种形式来展示图片,缺点是无法设置每张图片的停留时间,但是这种形式有一个中好处:我们可以不用把gif图片中的所有图片都保存在内存中,这样可以减少内存。当让我前面的NSTimer代码没有体现出来,(接下来我会在CADisPlayLink中显示出来)。而Animation的有点我们可以知晓:我们可以单独的控制每张图片的停留时间通过keyTimes属性。但是我们必须把所有到的图片放在内存中,因为属性values。这样会导致内存很大,而这种方式就是我们还有介绍的CADisplayLink,他即可以控制每张图停留的时间,又可以不用加载所有的图片。

3.3CADisplayLink
3.3.1CADisplayLink如何实现减少内存。

其实这个很简单。举例子说明:

现在有个gif图片,其中包含了30张图片,那么我会通过ImageSource来获取到图片个数和每一张图片,并且把图片按照获取的顺序放在一个数组中。而我们在时间触发器的方法中,根据保存数组下标来获取下一个图片并且把它展示出来,伪代码如下

- (void)doTimerAction {
      //gifImages:保存图片的数组[1,2,3,4,5,6,7,8 ........]一直到30,因为有30张照片
      //currentIndex: 保存图片顺序,也就是下标
      UIImage *image = [self.gifImages objectAtIndex:self.currentIndex];
      self.layer.contents = image.cgimage;
      //下标应该 +1
      self.currentIndex += 1; 
}

如何减少内存?就是减少数组gifImages中包含的图片的实例,但是有点我们应该要注意,我们是减少了数组中图片的数量,但是这个数组的长度还必须是gif图片中包含的图片的数量。举例说明就是:

以前数组中包含的情况:
gifimages = [image1,image2,image3,image4,image5,image6,.......image30 ]
现在减少内存
gifimages = [image1,image2,image3,image4,image5,image6,...image10,null, null, ....null ],但是长度还是30,为什么长度还是30,「那是因为如果不是30那就是自己给自己的代码增加逻辑。」

这就是减少内存的想法,其实是很简单的。我们要一直维持数组中只有十张图片,这个数量是可以自己控制的。那么我们应该怎么维持数组呢,举例子:

gifimages = [image1,image2,image3,image4,image5,image6,...image10,null, null, ....null ];
//现在获取idx = 0 ;
i = gifimages[idx]; // i = image1;
//之后显示
contents = i;
//此时第一张图片暂时是不需要了。那么我们就可以把image1消除
gifimages = [null,image2..........image10....null];
//在这个时候我们应该生成11张图
//获取第11张图
image11 = imagesourcegetimage(11);
//把image11,放在数组中,并且位置就是11
gifimages = [null,image2..........image10,image11....null];

到这就介绍完内存减少的方法了。
对应的实际代码:

- (UIImage *)getGIFImageWithIndex:(NSUInteger)idx {
    UIImage *image = [self.gifImages objectAtIndex:idx];
    if ([image isEqual:[NSNull null]]) {
        return nil;
    }
    
    //消除刚才取出的Image
    dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
    [self.gifImages replaceObjectAtIndex:idx withObject:[NSNull null]];
    dispatch_semaphore_signal(self.lock);
    //新增之后的Image
    NSUInteger nextIndex = (idx + capacity) % _imagesCount;
    
    dispatch_async(self.imageQueue, ^{
        NSLog(@"%@", [NSThread currentThread]);
        CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_imageSourceRef, nextIndex, NULL);
        UIImage *nextImage = [UIImage imageWithCGImage:imageRef];
        CFRelease(imageRef);
        dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
        [self.gifImages replaceObjectAtIndex:nextIndex withObject:nextImage];
        dispatch_semaphore_signal(self.lock);
    });
   
    
    return image;
}

方便大家查找

3.3.2CADisplayLink实现控制每张图片的停留时间

CADisplayLink这个类是一个以屏幕刷新频率,来触发绑定的事件。所以我们就知道就有了时间这个概念。每次调用绑定方法是16.67ms。那么我们就可以使用以16.67ms为单位。做一个时间累计器。举例子说明:

- (void)绑定方法{
  如果image1的delaytime = 30ms,现在展示的是image1
  contents = image1
  acculator 这个是一个时间计时器,初始化为 0;
  acculator += displaylink.duration;  
  如果acdulator > image1.delaytime,那么说明image1的展示时间已经过去了,所以index +1 往下循环此时:
  accluator 应该减去 image1.delaytime,
  content = image2.
  循环上面的步骤就可以了 
}

对应的实际代码:

- (void)displayKeyFrame{
//    NSLog(@"\ntimestamp:%f \ntarget:%f\nduration: %f", self.displayLink.timestamp,self.displayLink.targetTimestamp, self.displayLink.duration);
    
    self.accumulator += fmin(self.displayLink.duration, 1.f);
    while (self.accumulator >= self.gifimage.frameDurations[self.currentIndex]) {
        self.accumulator -= self.gifimage.frameDurations[self.currentIndex];
        self.currentIndex ++;
        if (self.currentIndex >= self.gifimage.imagesCount) {
            self.currentIndex = 0;
        }
        [self.layer setNeedsDisplay];
    }
    
}

方便大家查找

4.总结

总结一下这两天写的代码。主要还是看的别的博客。
iOS-加载gif的四种方式
本人代码如果感觉代码写的不好,可以查看上面博客中代码。而且最后一种方法的实现,yykit也是这样实现的,你可以查看一下大神的代码

上一篇下一篇

猜你喜欢

热点阅读