FLAnimatedImage iOS平臺上播放GIF動畫的一個優(yōu)秀解決方案价淌,支持可變幀間延時秒咐、內(nèi)存內(nèi)存表現(xiàn)良好漾抬、播放流暢等特點。
FLAnimatedImage有兩個類:
-
FLAnimatedImage
用來解析碑诉、封裝GIF圖像信息 (GIF幀數(shù)彪腔、GIF size、播放循環(huán)次數(shù)进栽、posterImage德挣、幀間延時) -
FLAnimatedImageView
用來控制GIF的播放
FLAnimatedImage
GIF圖像信息的解析,關(guān)鍵代碼:
關(guān)鍵是獲取循環(huán)次數(shù)快毛、幀間延時delayTimesForIndexesMutable
格嗅, 用到了底層的CGImageSourceRef
_imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data,
(__bridge CFDictionaryRef)@{(NSString *)kCGImageSourceShouldCache: @NO});
// Early return on failure!
if (!_imageSource) {
FLLog(FLLogLevelError, @"Failed to `CGImageSourceCreateWithData` for animated GIF data %@", data);
return nil;
}
// Early return if not GIF!
CFStringRef imageSourceContainerType = CGImageSourceGetType(_imageSource);
BOOL isGIFData = UTTypeConformsTo(imageSourceContainerType, kUTTypeGIF);
if (!isGIFData) {
FLLog(FLLogLevelError, @"Supplied data is of type %@ and doesn't seem to be GIF data %@", imageSourceContainerType, data);
return nil;
}
NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(_imageSource, NULL);
_loopCount = [[[imageProperties objectForKey:(id)kCGImagePropertyGIFDictionary] objectForKey:(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];
// Iterate through frame images
size_t imageCount = CGImageSourceGetCount(_imageSource);
NSUInteger skippedFrameCount = 0;
NSMutableDictionary *delayTimesForIndexesMutable = [NSMutableDictionary dictionaryWithCapacity:imageCount];
for (size_t i = 0; i < imageCount; i++) {
@autoreleasepool {
CGImageRef frameImageRef = CGImageSourceCreateImageAtIndex(_imageSource, i, NULL);
if (frameImageRef) {
UIImage *frameImage = [UIImage imageWithCGImage:frameImageRef];
// Check for valid `frameImage` before parsing its properties as frames can be corrupted (and `frameImage` even `nil` when `frameImageRef` was valid).
if (frameImage) {
// Set poster image
if (!self.posterImage) {
_posterImage = frameImage;
// Set its size to proxy our size.
_size = _posterImage.size;
// Remember index of poster image so we never purge it; also add it to the cache.
_posterImageFrameIndex = i;
[self.cachedFramesForIndexes setObject:self.posterImage forKey:@(self.posterImageFrameIndex)];
[self.cachedFrameIndexes addIndex:self.posterImageFrameIndex];
}
NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(_imageSource, i, NULL);
NSDictionary *framePropertiesGIF = [frameProperties objectForKey:(id)kCGImagePropertyGIFDictionary];
// Try to use the unclamped delay time; fall back to the normal delay time.
NSNumber *delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFUnclampedDelayTime];
if (!delayTime) {
delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFDelayTime];
}
delayTimesForIndexesMutable[@(i)] = delayTime;
} else {
skippedFrameCount++;
}
CFRelease(frameImageRef);
} else {
skippedFrameCount++;
}
}
}
FLAnimatedImage有一個關(guān)鍵接口imageLazilyCachedAtIndex
用于獲取某一幀對應(yīng)的Image。
關(guān)鍵思想是:內(nèi)存管理唠帝、內(nèi)存警告處理屯掖、緩存幀管理、子線程異步加載襟衰。
imageLazilyCachedAtIndex
獲取某一幀的時候贴铜,會進行前面幾幀的預(yù)加載,如果獲取的一幀還沒加載完成瀑晒,那么會返回 nil
值绍坝,避免卡頓的情況。
FLAnimatedImageView
FLAnimatedImageView
的職責(zé)是繪制GIF動畫苔悦。
那么如何繪制動畫轩褐?如何驅(qū)動動畫的繪制?怎么繪制间坐?
驅(qū)動的關(guān)鍵是CADisplayLink
:
- (void)startAnimating
{
if (self.animatedImage) {
// Lazily create the display link.
if (!self.displayLink) {
// It is important to note the use of a weak proxy here to avoid a retain cycle. `-displayLinkWithTarget:selector:`
// will retain its target until it is invalidated. We use a weak proxy so that the image view will get deallocated
// independent of the display link's lifetime. Upon image view deallocation, we invalidate the display
// link which will lead to the deallocation of both the display link and the weak proxy.
FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self];
self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];
}
// Note: The display link's `.frameInterval` value of 1 (default) means getting callbacks at the refresh rate of the display (~60Hz).
// Setting it to 2 divides the frame rate by 2 and hence calls back at every other display refresh.
const NSTimeInterval kDisplayRefreshRate = 60.0; // 60Hz
self.displayLink.frameInterval = MAX([self frameDelayGreatestCommonDivisor] * kDisplayRefreshRate, 1);
self.displayLink.paused = NO;
} else {
[super startAnimating];
}
}
注意:其中NSRunLoop
的mode設(shè)置灾挨, NSDefaultRunLoopMode
時邑退,滑動scrollview時竹宋,GIF會暫停播放劳澄,NSRunLoopCommonModes
模式是不會暫停。
其中蜈七,有一個問題:使用CADisplayLink
如何避免循環(huán)引用秒拔?
CADisplayLink
的target是retain這個target, 而displayLink會add到主線程的Runloop中,就會形成 Runloop -> CADisplayLink -> self 的引用關(guān)系飒硅。
解決辦法是使用FLWeakProxy
弱引用self, 這樣引用關(guān)系變成了 Runloop -> CADisplayLink -> WeakProxy砂缩, WeakProxy再弱引用self。當self釋放時移除CADisplayLink三娩,這樣就避免了循環(huán)引用庵芭。
- (void)dealloc
{
[_displayLink invalidate];
}
繪制
有了驅(qū)動,如何繪制雀监?
在CADisplayLink
的回調(diào)中:
- (void)displayDidRefresh:(CADisplayLink *)displayLink
實現(xiàn)了loopCount控制双吆、幀Index計數(shù)、延時管理(不能播放太快会前,也不能太慢:美帧)
看源碼:
- (void)displayDidRefresh:(CADisplayLink *)displayLink
{
NSNumber *delayTimeNumber = [self.animatedImage.delayTimesForIndexes objectForKey:@(self.currentFrameIndex)];
// If we don't have a frame delay (e.g. corrupt frame), don't update the view but skip the playhead to the next frame (in else-block).
if (delayTimeNumber) {
NSTimeInterval delayTime = [delayTimeNumber floatValue];
// If we have a nil image (e.g. waiting for frame), don't update the view nor playhead.
UIImage *image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex];
if (image) {
FLLog(FLLogLevelVerbose, @"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
self.currentFrame = image; //更新當前currentFrame,在繪制的時候使用
if (self.needsDisplayWhenImageBecomesAvailable) {
[self.layer setNeedsDisplay];
self.needsDisplayWhenImageBecomesAvailable = NO;
}
self.accumulator += displayLink.duration * displayLink.frameInterval;
// While-loop first inspired by & good Karma to: https://github.com/ondalabs/OLImageView/blob/master/OLImageView.m
while (self.accumulator >= delayTime) {
self.accumulator -= delayTime;
self.currentFrameIndex++;
if (self.currentFrameIndex >= self.animatedImage.frameCount) {
// If we've looped the number of times that this animated image describes, stop looping.
self.loopCountdown--;
if (self.loopCompletionBlock) {
self.loopCompletionBlock(self.loopCountdown);
}
if (self.loopCountdown == 0) {
[self stopAnimating];
return;
}
self.currentFrameIndex = 0;
}
// Calling `-setNeedsDisplay` will just paint the current frame, not the new frame that we may have moved to.
// Instead, set `needsDisplayWhenImageBecomesAvailable` to `YES` -- this will paint the new image once loaded.
self.needsDisplayWhenImageBecomesAvailable = YES;
}
} else {
}
} else {
self.currentFrameIndex++;
}
特別注意的是其中while的設(shè)計瓦宜,是為了在本次DisplayLink中拿到正確的currentFrameIndex
蔚万。
繪制
非常簡單,拿到GIF幀的圖片后临庇,直接顯示:
- (void)displayLayer:(CALayer *)layer
{
layer.contents = (__bridge id)self.image.CGImage;
}
- (UIImage *)image
{
UIImage *image = nil;
if (self.animatedImage) {
// Initially set to the poster image.
image = self.currentFrame;
} else {
image = super.image;
}
return image;
}