iOS动态图实现(1)

本文介绍了iOS中动态图的分类,如GIF、WEBP和APNG,并探讨了如何在iOS相册中保存动态图。接着,文章详细讨论了使用UIImageView、UIWebView和自定义View播放GIF动态图的优缺点,提出了一种优化内存消耗的播放策略。最后,提到了YZImageView和YZImage的使用以及动态图的显示效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

动态图的分类

  • GIF : GIF图形交换格式是一种位图图形文件格式,以8位色(即256种颜色)重现真彩色的图像,诞生在windows1.0的时代,已经有27年的历史,广泛的应用在图像的网络传输中。
  • WEBP : 2010年谷歌推出的图片格式,专门用来在web中使用,只有opera和chrome支持。
  • APNG : 这东西是mozilla搞出来的,它是24位的,可以容纳1680万种颜色,也是为了取代GIF,目前有火狐和Safari支持。

各种格式的图片大小性能比较可以在 GIF vs APNG vs WEBP 中看到,腾讯的同学也开源了一些工具可以用来转换不同格式的动态图,比如 iSpatra

iOS动图的保存

iOS 的相册是支持保存 GIF 和 APNG 动图的,只是不能直接播放。用

[ALAssetsLibrary    writeImageDataToSavedPhotosAlbum:metadata:completionBlock]

可以直接把 APNG、GIF 的数据写入相册。如果图省事直接用 UIImageWriteToSavedPhotosAlbum() 写相册,那么图像会被强制转码为 PNG。

目前来说,保存 UIImage 有三种方式:1.直接用 NSKeyedArchiver 把 UIImage 序列化保存,2.用 UIImagePNGRepresentation() 先把图片转为 PNG 保存,3.用 UIImageJPEGRepresentation() 把图片压缩成 JPEG 保存。

实际上,NSKeyedArchiver 是调用了 UIImagePNGRepresentation 进行序列化的,用它来保存图片是消耗最大的。苹果对 JPEG 有硬编码和硬解码,保存成 JPEG 会大大缩减编码解码时间,也能减小文件体积。所以如果图片不包含透明像素时,UIImageJPEGRepresentation(0.9) 是最佳的图片保存方式,其次是 UIImagePNGRepresentation()

GIF动态图的播放

1.使用UIImageView来播放

//创建UIImageView,添加到界面
UIImageView *imageView = [[UIImageView alloc]   initWithFrame:CGRectMake(20, 20, 100, 100)];
[self.view addSubview:imageView];
 //创建一个数组,数组中按顺序添加要播放的图片(图片为静态的图片)
NSMutableArray *imgArray = [NSMutableArray array];
for (int i=1; i<7; i++) {
UIImage *image = [UIImage imageNamed:[NSString  stringWithFormat:@"gif%02d.png",i]];
[imgArray addObject:image];
}
//把存有UIImage的数组赋给动画图片数组
imageView.animationImages = imgArray;
//设置执行一次完整动画的时长
imageView.animationDuration = 6*0.15;
//动画重复次数 (0为重复播放)
imageView.animationRepeatCount = 0;
//开始播放动画
[imageView startAnimating];
//停止播放动画  - (void)stopAnimating;
//判断是否正在执行动画  - (BOOL)isAnimating;

如果图片数量较多的话,会占用很大的内存,每帧的播放时间一样,失去了原有动态图的播放延迟时间

2.用UIWebView来显示动态图

//得到图片的路径
 NSString *path = [[NSBundle mainBundle]    pathForResource:@"happy" ofType:@"gif"];
//将图片转为NSData
NSData *gifData = [NSData dataWithContentsOfFile:path];
//创建一个webView,添加到界面
UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 150, 200, 200)];
[self.view addSubview:webView];
//自动调整尺寸
webView.scalesPageToFit = YES;
//禁止滚动
webView.scrollView.scrollEnabled = NO;
//设置透明效果
webView.backgroundColor = [UIColor clearColor];
webView.opaque = 0;
//加载数据
[webView loadData:gifData MIMEType:@"image/gif"     textEncodingName:nil baseURL:nil];

无法控制进度,可控性不强,Web View 没有为在移动设备上播放 GIF 而做优化,经常会降低播放速度。同时也不能很好地控制回放过程或内存占用

3.使用自定义的view来播放动态图

其实使用自定义的view来播放动态图所采取的做法无非是给View加一个image属性,所以决定采用继承UIImageView和UIImage来实现动态图的播放。
动态图控件的优劣主要要考虑一下因素:

1.CPU使用率

2.内存消耗

3.滑动显示的帧率

基于以上影响性能的考虑,决定通过一个可变数组缓存动态图的10帧图像,然后在读取当前帧的图像时,删除数组中缓存的第n帧图像,异步线程加载后面对应的第n+10帧图像,以此来达到减少内存消耗的目的,示例代码如下:

- (UIImage * )getFrameImageAtIndex:(NSUInteger)index
{
    UIImage* frame = nil;
    @synchronized(self.frameImages) {
        frame = self.frameImages[index];
    }
    if (!frame || [frame isKindOfClass:[NSNull class]]) {
        CGImageSourceStatus state = CGImageSourceGetStatus(_imageSource);
        CGImageRef image = CGImageSourceCreateImageAtIndex(_imageSource, index, NULL);
        if (image != NULL) {
            frame = [UIImage imageWithCGImage:image scale:imageScale orientation:UIImageOrientationUp];
            CFRelease(image);
        }
    }

    if (self.frameCount > productLimitNum) {

        [self.frameImages replaceObjectAtIndex:index withObject:[NSNull null]];
        NSUInteger nextReadIdx = (index + productLimitNum);
        nextReadIdx %= self.frameCount;
        if([self.frameImages[nextReadIdx] isKindOfClass:[NSNull class]]) {
            dispatch_async(_serialQueue, ^{
                CGImageSourceStatus state = CGImageSourceGetStatus(_imageSource);
                CGImageRef image = CGImageSourceCreateImageAtIndex(_imageSource, nextReadIdx, NULL);
                @synchronized(self.frameImages) {
                    if (image != NULL) {
                        [self.frameImages replaceObjectAtIndex:nextReadIdx withObject:[UIImage imageWithCGImage:image scale:imageScale orientation:UIImageOrientationUp]];
                    } else {
                        [self.frameImages replaceObjectAtIndex:nextReadIdx withObject:[NSNull null]];
                    }
                    CFRelease(image);
                }
            });
        }
    }
    return frame;
}

通过CADisplayLink来同步的刷新当前UIImageView显示的图像,刷新的频率根据相应的设备而定,一般会在60fps左右,最终达到图像动态播放的目的。

- (CADisplayLink *)displayLink
{
    if (self.superview && self.animatedImage) {
        if (_displayLink) {
            return _displayLink;
        }else{
            _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(changeCurrentFrame:)];
            [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
        }
    } else {
        [_displayLink invalidate];
        _displayLink = nil;
    }
    return _displayLink;
}

- (void)changeCurrentFrame:(CADisplayLink *)displayLink
{
    if (self.currentFrameIndex >= self.animatedImage.frameCount) {
        return;
    }
    self.accumulator += fmin(displayLink.duration, 0.1);

    while (self.accumulator >=  self.animatedImage.frameDurations[self.currentFrameIndex]) {
        self.accumulator -= self.animatedImage.frameDurations[self.currentFrameIndex];
        if (++self.currentFrameIndex >= self.animatedImage.frameCount) {
            if (--self.loopCountdown == 0) {
                [self stopAnimating];
                return;
            }
            self.currentFrameIndex = 0;
        }
        self.currentFrameIndex = MIN(self.currentFrameIndex, [self.animatedImage.images count] - 1);
        self.currentFrame = [self.animatedImage getFrameImageAtIndex:self.currentFrameIndex];
        [self.layer setNeedsDisplay];
    }
}

除了通过CADisplayLink来达成动态图刷新之外还有两种方法也可以达到目的:

  • 通过设置动画属性,为layer添CAKeyframeAnimation

    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
    NSMutableArray *times = [NSMutableArray arrayWithCapacity:3];
    CGFloat currentTime = 0;
    int count = _frameDelayTimes.count;
    for (int i = 0; i < count; ++i) {
        [times addObject:[NSNumber numberWithFloat:(currentTime/_totalTime)]];
        currentTime += [[_frameDelayTimes objectAtIndex:i] floatValue];
    }
    [animation setKeyTimes:times];
    NSMutableArray *images = [NSMutableArray arrayWithCapacity:3];
    for (int i = 0; i < count; ++i) {
        [images addObject:[_frames objectAtIndex:i]];
    }
    [animation setValues:images];
    [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];
    animation.duration = _totalTime;
    animation.delegate = self;
    animation.repeatCount = 5;
    
    [self.layer addAnimation:animation forKey:@"gifAnimation"];
    
  • 通过NSTimer递归调用,传入每一帧的持续时间

    • scheduledTimerWithTimeInterval:(NSTimeInterval)time invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

YZImageView和YZImage的使用

#import "ViewController.h"
#import "YZImageView.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet YZImageView *test1;
@property (weak, nonatomic) IBOutlet YZImageView *test2;
@property (weak, nonatomic) IBOutlet YZImageView *test3;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    self.test1.animatedImage = (YZAnimationImage *)[YZAnimationImage imageNamed:@"test1"];
    self.test2.animatedImage = (YZAnimationImage *)[YZAnimationImage imageNamed:@"test2"];
    self.test3.animatedImage = (YZAnimationImage *)[YZAnimationImage imageNamed:@"test3@2x"];
}   

使用效果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值