فهرست منبع

Merge pull request #300 from onevcat/xspyhack-master

GIF performance
Wei Wang 9 سال پیش
والد
کامیت
1109e9bff1

+ 6 - 0
Kingfisher.xcodeproj/project.pbxproj

@@ -16,6 +16,8 @@
 		4B3766841C478F940001443F /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D13F49D61BEDA67C00CE335D /* Kingfisher.framework */; };
 		4B3766A01C4794460001443F /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B37669F1C4794460001443F /* CFNetwork.framework */; };
 		4B3766A21C47944D0001443F /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B3766A11C47944D0001443F /* CFNetwork.framework */; };
+		4B98674F1CD1CF42003ADAC7 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B98674E1CD1CF42003ADAC7 /* AnimatedImageView.swift */; };
+		4B9867501CD1CF42003ADAC7 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B98674E1CD1CF42003ADAC7 /* AnimatedImageView.swift */; };
 		B43007AC86DBFFFD1AC6EDD1 /* libPods-KingfisherTests-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 798E024A9311DC80470CF240 /* libPods-KingfisherTests-tvOS.a */; };
 		D10945F71C526B86001408EB /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = D10945EA1C526B6C001408EB /* Image.swift */; };
 		D10945F81C526B86001408EB /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D10945EB1C526B6C001408EB /* ImageCache.swift */; };
@@ -258,6 +260,7 @@
 		4B37669F1C4794460001443F /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS9.1.sdk/System/Library/Frameworks/CFNetwork.framework; sourceTree = DEVELOPER_DIR; };
 		4B3766A11C47944D0001443F /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/System/Library/Frameworks/CFNetwork.framework; sourceTree = DEVELOPER_DIR; };
 		4B3E714D1B01FEB200F5AAED /* WatchKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WatchKit.framework; path = System/Library/Frameworks/WatchKit.framework; sourceTree = SDKROOT; };
+		4B98674E1CD1CF42003ADAC7 /* AnimatedImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AnimatedImageView.swift; path = Sources/AnimatedImageView.swift; sourceTree = "<group>"; };
 		50ECD18204CB0CD37B49F631 /* libPods-KingfisherTests-OSX.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-KingfisherTests-OSX.a"; sourceTree = BUILT_PRODUCTS_DIR; };
 		74477D1C4379728A8DA673FB /* Pods-KingfisherTests-OSX.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KingfisherTests-OSX.debug.xcconfig"; path = "Pods/Target Support Files/Pods-KingfisherTests-OSX/Pods-KingfisherTests-OSX.debug.xcconfig"; sourceTree = "<group>"; };
 		798E024A9311DC80470CF240 /* libPods-KingfisherTests-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-KingfisherTests-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -440,6 +443,7 @@
 		D10EC22A1C3D62D200A4211C /* Sources */ = {
 			isa = PBXGroup;
 			children = (
+				4B98674E1CD1CF42003ADAC7 /* AnimatedImageView.swift */,
 				D10945EA1C526B6C001408EB /* Image.swift */,
 				D10945EB1C526B6C001408EB /* ImageCache.swift */,
 				D10945EC1C526B6C001408EB /* ImageDownloader.swift */,
@@ -1332,6 +1336,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				D109460E1C526C0D001408EB /* Image.swift in Sources */,
+				4B9867501CD1CF42003ADAC7 /* AnimatedImageView.swift in Sources */,
 				D109460F1C526C0D001408EB /* ImageCache.swift in Sources */,
 				D10946101C526C0D001408EB /* ImageDownloader.swift in Sources */,
 				D10946111C526C0D001408EB /* ImageTransition.swift in Sources */,
@@ -1387,6 +1392,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				D10945F71C526B86001408EB /* Image.swift in Sources */,
+				4B98674F1CD1CF42003ADAC7 /* AnimatedImageView.swift in Sources */,
 				D10945F81C526B86001408EB /* ImageCache.swift in Sources */,
 				D10945F91C526B86001408EB /* ImageDownloader.swift in Sources */,
 				D10945FA1C526B86001408EB /* ImageTransition.swift in Sources */,

+ 7 - 0
README.md

@@ -331,6 +331,13 @@ prefetcher.stop()
 
 After prefetching, you could retrieve image or set the image view with other Kingfisher's methods, with the same `ImageCache` object you used for the prefetching.
 
+### Animated GIF
+
+You can load animated GIF by replacing `UIImageView` with `AnimatedImageView`
+```swift
+imageView = AnimatedImageView()
+```
+
 ## Future of Kingfisher
 
 I want to keep Kingfisher slim. This framework will focus on providing a simple solution for image downloading and caching. But that does not mean the framework will not be improved. Kingfisher is far away from perfect, and necessary and useful features will be added later to make it better.

+ 369 - 0
Sources/AnimatedImageView.swift

@@ -0,0 +1,369 @@
+//
+//  AnimatableImageView.swift
+//  Kingfisher
+//
+//  Created by bl4ckra1sond3tre on 4/22/16.
+//
+//  The AnimatableImageView, AnimatedFrame and Animator is a modified version of 
+//  some classes from kaishin's Gifu project (https://github.com/kaishin/Gifu)
+//
+//  The MIT License (MIT)
+//
+//  Copyright (c) 2014-2016 Reda Lemeden.
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy of
+//  this software and associated documentation files (the "Software"), to deal in
+//  the Software without restriction, including without limitation the rights to
+//  use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+//  the Software, and to permit persons to whom the Software is furnished to do so,
+//  subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in all
+//  copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+//  FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+//  COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+//  IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+//  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+//  The name and characters used in the demo of this software are property of their
+//  respective owners.
+
+import UIKit
+import ImageIO
+
+/// `AnimatedImageView` is a subclass of `UIImageView` for displaying animated image.
+public class AnimatedImageView: UIImageView {
+    
+    /// Proxy object for prevending a reference cycle between the CADDisplayLink and AnimatedImageView.
+    class TargetProxy {
+        private weak var target: AnimatedImageView?
+        
+        init(target: AnimatedImageView) {
+            self.target = target
+        }
+        
+        @objc func onScreenUpdate() {
+            target?.updateFrame()
+        }
+    }
+    
+    // MARK: - Public property
+    /// Whether automatically play the animation when the view become visible. Default is true.
+    public var autoPlayAnimatedImage = true
+    
+    /// The size of the frame cache.
+    public var framePreloadCount = 10
+    
+    /// Specifies whether the GIF frames should be pre-scaled to save memory. Default is true.
+    public var needsPrescaling = true
+    
+    /// The animation timer's run loop mode. Default is `NSRunLoopCommonModes`. Set this property to `NSDefaultRunLoopMode` will make the animation pause during UIScrollView scrolling.
+    public var runLoopMode = NSRunLoopCommonModes {
+        willSet {
+            if runLoopMode == newValue {
+                return
+            } else {
+                stopAnimating()
+                displayLink.removeFromRunLoop(NSRunLoop.mainRunLoop(), forMode: runLoopMode)
+                displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: newValue)
+                startAnimating()
+            }
+        }
+    }
+    
+    // MARK: - Private property
+    /// `Animator` instance that holds the frames of a specific image in memory.
+    private var animator: Animator?
+    
+    /// A flag to avoid invalidating the displayLink on deinit if it was never created, because displayLink is so lazy. :D
+    private var displayLinkInitialized: Bool = false
+    
+    /// A display link that keeps calling the `updateFrame` method on every screen refresh.
+    private lazy var displayLink: CADisplayLink = {
+        self.displayLinkInitialized = true
+        let displayLink = CADisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate))
+        displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: self.runLoopMode)
+        displayLink.paused = true
+        return displayLink
+    }()
+    
+    // MARK: - Override
+    override public var image: Image? {
+        didSet {
+            if image != oldValue {
+                reset()
+            }
+            setNeedsDisplay()
+            layer.setNeedsDisplay()
+        }
+    }
+    
+    deinit {
+        if displayLinkInitialized {
+            displayLink.invalidate()
+        }
+    }
+    
+    override public func isAnimating() -> Bool {
+        return !displayLink.paused
+    }
+    
+    /// Starts the animation.
+    override public func startAnimating() {
+        if self.isAnimating() {
+            return
+        } else {
+            displayLink.paused = false
+        }
+    }
+    
+    /// Stops the animation.
+    override public func stopAnimating() {
+        super.stopAnimating()
+        displayLink.paused = true
+    }
+    
+    override public func displayLayer(layer: CALayer) {
+        if let currentFrame = animator?.currentFrame {
+            layer.contents = currentFrame.CGImage
+        } else {
+            layer.contents = image?.CGImage
+        }
+    }
+    
+    override public func didMoveToWindow() {
+        super.didMoveToWindow()
+        didMove()
+    }
+    
+    override public func didMoveToSuperview() {
+        super.didMoveToSuperview()
+        didMove()
+    }
+    
+    // This is for back compatibility that using regular UIImageView to show GIF.
+    override func shouldPreloadAllGIF() -> Bool {
+        return false
+    }
+    
+    // MARK: - Private method
+    /// Reset the animator.
+    private func reset() {
+        animator = nil
+        if let imageSource = image?.kf_imageSource?.imageRef {
+            animator = Animator(imageSource: imageSource, contentMode: contentMode, size: bounds.size, framePreloadCount: framePreloadCount)
+            animator?.needsPrescaling = needsPrescaling
+            animator?.prepareFrames()
+        }
+        didMove()
+    }
+    
+    private func didMove() {
+        if autoPlayAnimatedImage && animator != nil {
+            if let _ = superview, _ = window {
+                startAnimating()
+            } else {
+                stopAnimating()
+            }
+        }
+    }
+    
+    /// Update the current frame with the displayLink duration.
+    private func updateFrame() {
+        if animator?.updateCurrentFrame(displayLink.duration) ?? false {
+            layer.setNeedsDisplay()
+        }
+    }
+}
+
+/// Keeps a reference to an `Image` instance and its duration as a GIF frame.
+struct AnimatedFrame {
+    var image: Image?
+    let duration: NSTimeInterval
+    
+    static func null() -> AnimatedFrame {
+        return AnimatedFrame(image: .None, duration: 0.0)
+    }
+}
+
+// MARK: - Animator
+///
+class Animator {
+    // MARK: Private property
+    private let size: CGSize
+    private let maxFrameCount: Int
+    private let imageSource: CGImageSourceRef
+    
+    private var animatedFrames = [AnimatedFrame]()
+    private let maxTimeStep: NSTimeInterval = 1.0
+    private var frameCount = 0
+    private var currentFrameIndex = 0
+    private var currentPreloadIndex = 0
+    private var timeSinceLastFrameChange: NSTimeInterval = 0.0
+    private var needsPrescaling = true
+    
+    /// Loop count of animatd image.
+    private var loopCount = 0
+    
+    var currentFrame: UIImage? {
+        return frameAtIndex(currentFrameIndex)
+    }
+    
+    var contentMode: UIViewContentMode = .ScaleToFill
+    
+    /**
+     Init an animator with image source reference.
+     
+     - parameter imageSource: The reference of animated image.
+     
+     - parameter contentMode: Content mode of AnimatedImageView.
+     
+     - parameter size: Size of AnimatedImageView.
+     
+     - framePreloadCount: Frame cache size.
+     
+     - returns: The animator object.
+     */
+    init(imageSource src: CGImageSourceRef, contentMode mode: UIViewContentMode, size: CGSize, framePreloadCount: Int) {
+        self.imageSource = src
+        self.contentMode = mode
+        self.size = size
+        self.maxFrameCount = framePreloadCount
+    }
+    
+    func frameAtIndex(index: Int) -> Image? {
+        return animatedFrames[index].image
+    }
+    
+    func prepareFrames() {
+        frameCount = CGImageSourceGetCount(imageSource)
+        
+        if let properties = CGImageSourceCopyProperties(imageSource, nil),
+            gifInfo = (properties as NSDictionary)[kCGImagePropertyGIFDictionary as String] as? NSDictionary,
+            loopCount = gifInfo[kCGImagePropertyGIFLoopCount as String] as? Int {
+            self.loopCount = loopCount
+        }
+        
+        let frameToProcess = min(frameCount, maxFrameCount)
+        animatedFrames.reserveCapacity(frameToProcess)
+        animatedFrames = (0..<frameToProcess).reduce([]) { $0 + pure(prepareFrame($1))}
+    }
+    
+    func prepareFrame(index: Int) -> AnimatedFrame {
+        guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, index, nil) else {
+            return AnimatedFrame.null()
+        }
+        
+        let frameDuration = imageSource.kf_GIFPropertiesAtIndex(index).flatMap { (gifInfo) -> Double? in
+            let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as Double?
+            let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as Double?
+            let duration = unclampedDelayTime ?? delayTime
+            /**
+             http://opensource.apple.com/source/WebCore/WebCore-7600.1.25/platform/graphics/cg/ImageSourceCG.cpp
+             Many annoying ads specify a 0 duration to make an image flash as quickly as
+             possible. We follow Safari and Firefox's behavior and use a duration of 100 ms
+             for any frames that specify a duration of <= 10 ms.
+             See <rdar://problem/7689300> and <http://webkit.org/b/36082> for more information.
+             
+             See also: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser.
+             */
+            return duration > 0.011 ? duration : 0.100
+        }
+        
+        let image = Image(CGImage: imageRef)
+        let scaledImage: Image?
+        
+        if needsPrescaling {
+            scaledImage = image.kf_resizeToSize(size, contentMode: contentMode)
+        } else {
+            scaledImage = image
+        }
+        
+        return AnimatedFrame(image: scaledImage, duration: frameDuration ?? 0.0)
+    }
+    
+    /**
+     Updates the current frame if necessary using the frame timer and the duration of each frame in `animatedFrames`.
+     */
+    func updateCurrentFrame(duration: CFTimeInterval) -> Bool {
+        timeSinceLastFrameChange += min(maxTimeStep, duration)
+        guard let frameDuration = animatedFrames[safe: currentFrameIndex]?.duration where frameDuration <= timeSinceLastFrameChange else {
+            return false
+        }
+        
+        timeSinceLastFrameChange -= frameDuration
+        let lastFrameIndex = currentFrameIndex
+        currentFrameIndex += 1
+        currentFrameIndex = currentFrameIndex % animatedFrames.count
+        
+        if animatedFrames.count < frameCount {
+            animatedFrames[lastFrameIndex] = prepareFrame(currentPreloadIndex)
+            currentPreloadIndex += 1
+            currentPreloadIndex = currentPreloadIndex % frameCount
+        }
+        return true
+    }
+}
+
+// MARK: - Resize
+extension Image {
+    func kf_resizeToSize(size: CGSize, contentMode: UIViewContentMode) -> Image {
+        switch contentMode {
+        case .ScaleAspectFit:
+            let newSize = self.size.kf_sizeConstrainedSize(size)
+            return kf_resizeToSize(newSize)
+        case .ScaleAspectFill:
+            let newSize = self.size.kf_sizeFillingSize(size)
+            return kf_resizeToSize(newSize)
+        default:
+            return kf_resizeToSize(size)
+        }
+    }
+    
+    private func kf_resizeToSize(size: CGSize) -> Image {
+        UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
+        drawInRect(CGRect(origin: CGPoint.zero, size: size))
+        let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+        return resizedImage ?? self
+    }
+}
+
+extension CGSize {
+    func kf_sizeConstrainedSize(size: CGSize) -> CGSize {
+        let aspectWidth = round(kf_aspectRatio * size.height)
+        let aspectHeight = round(size.width / kf_aspectRatio)
+        
+        return aspectWidth > size.width ? CGSize(width: size.width, height: aspectHeight) : CGSize(width: aspectWidth, height: size.height)
+    }
+    
+    func kf_sizeFillingSize(size: CGSize) -> CGSize {
+        let aspectWidth = round(kf_aspectRatio * size.height)
+        let aspectHeight = round(size.width / kf_aspectRatio)
+        
+        return aspectWidth < size.width ? CGSize(width: size.width, height: aspectHeight) : CGSize(width: aspectWidth, height: size.height)
+    }
+    private var kf_aspectRatio: CGFloat {
+        return height == 0.0 ? 1.0 : width / height
+    }
+}
+
+extension CGImageSourceRef {
+    func kf_GIFPropertiesAtIndex(index: Int) -> [String: Double]? {
+        let properties = CGImageSourceCopyPropertiesAtIndex(self, index, nil) as Dictionary?
+        return properties?[kCGImagePropertyGIFDictionary as String] as? [String: Double]
+    }
+}
+
+extension Array {
+    subscript(safe index: Int) -> Element? {
+        return indices ~= index ? self[index] : .None
+    }
+}
+
+func pure<T>(a: T) -> [T] {
+    return [a]
+}

+ 102 - 38
Sources/Image.swift

@@ -35,6 +35,9 @@ private var durationKey: Void?
 import UIKit.UIImage
 import MobileCoreServices
 public typealias Image = UIImage
+
+private var imageSourceKey: Void?
+private var animatedImageDataKey: Void?
 #endif
 
 import ImageIO
@@ -81,6 +84,24 @@ extension Image {
     var kf_duration: NSTimeInterval {
         return duration
     }
+    
+    private(set) var kf_imageSource: ImageSource? {
+            get {
+                return objc_getAssociatedObject(self, &imageSourceKey) as? ImageSource
+            }
+            set {
+                objc_setAssociatedObject(self, &imageSourceKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+            }
+        }
+        
+    private(set) var kf_animatedImageData: NSData? {
+            get {
+                return objc_getAssociatedObject(self, &animatedImageDataKey) as? NSData
+            }
+            set {
+                objc_setAssociatedObject(self, &animatedImageDataKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+            }
+        }
 #endif
 }
 
@@ -167,7 +188,11 @@ func ImageJPEGRepresentation(image: Image, _ compressionQuality: CGFloat) -> NSD
 
 // MARK: - GIF
 func ImageGIFRepresentation(image: Image) -> NSData? {
+#if os(OSX)
     return ImageGIFRepresentation(image, duration: 0.0, repeatCount: 0)
+#else
+    return image.kf_animatedImageData
+#endif
 }
 
 func ImageGIFRepresentation(image: Image, duration: NSTimeInterval, repeatCount: Int) -> NSData? {
@@ -195,74 +220,99 @@ func ImageGIFRepresentation(image: Image, duration: NSTimeInterval, repeatCount:
     return CGImageDestinationFinalize(destination) ? NSData(data: data) : nil
 }
 
+func ImagesCountWithImageSource(ref: CGImageSourceRef) -> Int {
+    return CGImageSourceGetCount(ref)
+}
+
 extension Image {
-    static func kf_animatedImageWithGIFData(gifData data: NSData) -> Image? {
-        return kf_animatedImageWithGIFData(gifData: data, scale: 1.0, duration: 0.0)
+    static func kf_animatedImageWithGIFData(gifData data: NSData, preloadAll: Bool) -> Image? {
+        return kf_animatedImageWithGIFData(gifData: data, scale: 1.0, duration: 0.0, preloadAll: preloadAll)
     }
     
-    static func kf_animatedImageWithGIFData(gifData data: NSData, scale: CGFloat, duration: NSTimeInterval) -> Image? {
+    static func kf_animatedImageWithGIFData(gifData data: NSData, scale: CGFloat, duration: NSTimeInterval, preloadAll: Bool) -> Image? {
+        
+        func decodeFromSource(imageSource: CGImageSource, options: NSDictionary) -> ([Image], NSTimeInterval)? {
+
+            let frameCount = CGImageSourceGetCount(imageSource)
+            var images = [Image]()
+            var gifDuration = 0.0
+            for i in 0 ..< frameCount {
+                
+                guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, options) else {
+                    return nil
+                }
+                
+                if frameCount == 1 {
+                    // Single frame
+                    gifDuration = Double.infinity
+                } else {
+                    // Animated GIF
+                    guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil),
+                        gifInfo = (properties as NSDictionary)[kCGImagePropertyGIFDictionary as String] as? NSDictionary,
+                        frameDuration = (gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber) else
+                    {
+                        return nil
+                    }
+                    gifDuration += frameDuration.doubleValue
+                }
+                
+                images.append(Image.kf_imageWithCGImage(imageRef, scale: scale, refImage: nil))
+            }
+            
+            return (images, gifDuration)
+        }
         
+        // Start of kf_animatedImageWithGIFData
         let options: NSDictionary = [kCGImageSourceShouldCache as String: NSNumber(bool: true), kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF]
         guard let imageSource = CGImageSourceCreateWithData(data, options) else {
             return nil
         }
         
-        let frameCount = CGImageSourceGetCount(imageSource)
-        var images = [Image]()
-        
-        var gifDuration = 0.0
-        
-        for i in 0 ..< frameCount {
-            guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, options) else {
+#if os(OSX)
+        guard let (images, gifDuration) = decodeFromSource(imageSource, options: options) else {
+            return nil
+        }
+        let image = Image(data: data)
+        image?.kf_images = images
+        image?.kf_duration = gifDuration
+    
+        return image
+#else
+    
+        if preloadAll {
+            guard let (images, gifDuration) = decodeFromSource(imageSource, options: options) else {
                 return nil
             }
-            
-            if frameCount == 1 {
-                // Single frame
-                gifDuration = Double.infinity
-            } else {
-                // Animated GIF
-                guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil),
-                    gifInfo = (properties as NSDictionary)[kCGImagePropertyGIFDictionary as String] as? NSDictionary,
-                    frameDuration = (gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber) else
-                {
-                    return nil
-                }
-                gifDuration += frameDuration.doubleValue
-            }
-            
-            images.append(Image.kf_imageWithCGImage(imageRef, scale: scale, refImage: nil))
-        }
-         
-#if os(OSX)
-        if let image = Image(data: data) {
-            image.kf_images = images
-            image.kf_duration = gifDuration
+            let image = Image.kf_animatedImageWithImages(images, duration: duration <= 0.0 ? gifDuration : duration)
+            image?.kf_animatedImageData = data
+            return image
+        } else {
+            let image = Image(data: data)
+            image?.kf_animatedImageData = data
+            image?.kf_imageSource = ImageSource(ref: imageSource)
             return image
         }
-        return nil
-#else
-        return Image.kf_animatedImageWithImages(images, duration: duration <= 0.0 ? gifDuration : duration)
 #endif
+        
     }
 }
 
 // MARK: - Create images from data
 extension Image {
-    static func kf_imageWithData(data: NSData, scale: CGFloat) -> Image? {
+    static func kf_imageWithData(data: NSData, scale: CGFloat, preloadAllGIFData: Bool) -> Image? {
         var image: Image?
         #if os(OSX)
             switch data.kf_imageFormat {
             case .JPEG: image = Image(data: data)
             case .PNG: image = Image(data: data)
-            case .GIF: image = Image.kf_animatedImageWithGIFData(gifData: data, scale: scale, duration: 0.0)
+            case .GIF: image = Image.kf_animatedImageWithGIFData(gifData: data, scale: scale, duration: 0.0, preloadAll: preloadAllGIFData)
             case .Unknown: image = Image(data: data)
             }
         #else
             switch data.kf_imageFormat {
             case .JPEG: image = Image(data: data, scale: scale)
             case .PNG: image = Image(data: data, scale: scale)
-            case .GIF: image = Image.kf_animatedImageWithGIFData(gifData: data, scale: scale, duration: 0.0)
+            case .GIF: image = Image.kf_animatedImageWithGIFData(gifData: data, scale: scale, duration: 0.0, preloadAll: preloadAllGIFData)
             case .Unknown: image = Image(data: data, scale: scale)
             }
         #endif
@@ -279,9 +329,15 @@ extension Image {
     
     func kf_decodedImage(scale scale: CGFloat) -> Image? {
         // prevent animated image (GIF) lose it's images
+#if os(iOS)
+        if kf_imageSource != nil {
+            return self
+        }
+#else
         if kf_images != nil {
             return self
         }
+#endif
         
         let imageRef = self.CGImage
         let colorSpace = CGColorSpaceCreateDeviceRGB()
@@ -299,6 +355,14 @@ extension Image {
     }
 }
 
+/// Reference the source image reference
+class ImageSource {
+    var imageRef: CGImageSourceRef?
+    init(ref: CGImageSourceRef) {
+        self.imageRef = ref
+    }
+}
+
 // MARK: - Image format
 private let pngHeader: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
 private let jpgHeaderSOI: [UInt8] = [0xFF, 0xD8]

+ 8 - 6
Sources/ImageCache.swift

@@ -260,7 +260,7 @@ extension ImageCache {
             var sSelf: ImageCache! = self
             block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS) {
                 // Begin to load image from disk
-                if let image = sSelf.retrieveImageInDiskCacheForKey(key, scale: options.scaleFactor) {
+                if let image = sSelf.retrieveImageInDiskCacheForKey(key, scale: options.scaleFactor, preloadAllGIFData: options.preloadAllGIFData) {
                     if options.backgroundDecode {
                         dispatch_async(sSelf.processQueue, { () -> Void in
                             let result = image.kf_decodedImage(scale: options.scaleFactor)
@@ -308,12 +308,14 @@ extension ImageCache {
     Get an image for a key from disk.
     
     - parameter key: Key for the image.
-    - param scale: The scale factor to assume when interpreting the image data.
+    - parameter scale: The scale factor to assume when interpreting the image data.
+    - parameter preloadAllGIFData: Whether all GIF data should be loaded. If true, you can set the loaded image to a regular UIImageView to play 
+      the GIF animation. Otherwise, you should use `AnimatedImageView` to play it. Default is `false`
 
     - returns: The image object if it is cached, or `nil` if there is no such key in the cache.
     */
-    public func retrieveImageInDiskCacheForKey(key: String, scale: CGFloat = 1.0) -> Image? {
-        return diskImageForKey(key, scale: scale)
+    public func retrieveImageInDiskCacheForKey(key: String, scale: CGFloat = 1.0, preloadAllGIFData: Bool = false) -> Image? {
+        return diskImageForKey(key, scale: scale, preloadAllGIFData: preloadAllGIFData)
     }
 }
 
@@ -605,9 +607,9 @@ extension ImageCache {
 // MARK: - Internal Helper
 extension ImageCache {
     
-    func diskImageForKey(key: String, scale: CGFloat) -> Image? {
+    func diskImageForKey(key: String, scale: CGFloat, preloadAllGIFData: Bool) -> Image? {
         if let data = diskImageDataForKey(key) {
-            return Image.kf_imageWithData(data, scale: scale)
+            return Image.kf_imageWithData(data, scale: scale, preloadAllGIFData: preloadAllGIFData)
         } else {
             return nil
         }

+ 1 - 1
Sources/ImageDownloader.swift

@@ -449,7 +449,7 @@ class ImageDownloaderSessionHandler: NSObject, NSURLSessionDataDelegate, Authent
             if let fetchLoad = downloader.fetchLoadForKey(URL) {
                 
                 let options = fetchLoad.options ?? KingfisherEmptyOptionsInfo
-                if let image = Image.kf_imageWithData(fetchLoad.responseData, scale: options.scaleFactor) {
+                if let image = Image.kf_imageWithData(fetchLoad.responseData, scale: options.scaleFactor, preloadAllGIFData: options.preloadAllGIFData) {
                     
                     downloader.delegate?.imageDownloader?(downloader, didDownloadImage: image, forURL: URL, withResponse: task.response!)
                     

+ 13 - 1
Sources/ImageView+Kingfisher.swift

@@ -102,7 +102,12 @@ extension ImageView {
         
         kf_setWebURL(resource.downloadURL)
         
-        let task = KingfisherManager.sharedManager.retrieveImageWithResource(resource, optionsInfo: optionsInfo,
+        var options = optionsInfo ?? []
+        if shouldPreloadAllGIF() {
+            options.append(.PreloadAllGIFData)
+        }
+
+        let task = KingfisherManager.sharedManager.retrieveImageWithResource(resource, optionsInfo: options,
             progressBlock: { receivedSize, totalSize in
                 if let progressBlock = progressBlock {
                     progressBlock(receivedSize: receivedSize, totalSize: totalSize)
@@ -135,6 +140,7 @@ extension ImageView {
                                         UIView.transitionWithView(sSelf, duration: transition.duration,
                                             options: [transition.animationOptions, .AllowUserInteraction],
                                             animations: {
+                                                // Set image property in the animation.
                                                 transition.animations?(sSelf, image)
                                             },
                                             completion: { finished in
@@ -157,6 +163,12 @@ extension ImageView {
     }
 }
 
+extension ImageView {
+    func shouldPreloadAllGIF() -> Bool {
+        return true
+    }
+}
+
 extension ImageView {
     /**
      Cancel the image download task bounded to the image view if it is running.

+ 17 - 9
Sources/KingfisherOptionsInfo.swift

@@ -49,6 +49,7 @@ Items could be added into KingfisherOptionsInfo.
 - BackgroundDecode: Decode the image in background thread before using.
 - CallbackDispatchQueue: The associated value of this member will be used as the target queue of dispatch callbacks when retrieving images from cache. If not set, `Kingfisher` will use main quese for callbacks.
 - ScaleFactor: The associated value of this member will be used as the scale factor when converting retrieved data to an image.
+- PreloadAllGIFData: Whether all the GIF data should be preloaded. Default it false, which means following frames will be loaded on need. If true, all the GIF data will be loaded and decoded into memory. This option is mainly used for back compatibility internally. You should not set it directly. `AnimatedImageView` will not preload all data, while a normal image view (`UIImageView` or `NSImageView`) will load all data. Choose to use corresponding image view type instead of setting this option.
 */
 public enum KingfisherOptionsInfoItem {
     case TargetCache(ImageCache?)
@@ -60,6 +61,7 @@ public enum KingfisherOptionsInfoItem {
     case BackgroundDecode
     case CallbackDispatchQueue(dispatch_queue_t?)
     case ScaleFactor(CGFloat)
+    case PreloadAllGIFData
 }
 
 infix operator <== {
@@ -70,15 +72,17 @@ infix operator <== {
 // This operator returns true if two `KingfisherOptionsInfoItem` enum is the same, without considering the associated values.
 func <== (lhs: KingfisherOptionsInfoItem, rhs: KingfisherOptionsInfoItem) -> Bool {
     switch (lhs, rhs) {
-    case (.TargetCache(_), .TargetCache(_)): return true
-    case (.Downloader(_), .Downloader(_)): return true
-    case (.Transition(_), .Transition(_)): return true
-    case (.DownloadPriority(_), .DownloadPriority(_)): return true
-    case (.ForceRefresh, .ForceRefresh): return true
-    case (.CacheMemoryOnly, .CacheMemoryOnly): return true
-    case (.BackgroundDecode, .BackgroundDecode): return true
-    case (.CallbackDispatchQueue(_), .CallbackDispatchQueue(_)): return true
-    case (.ScaleFactor(_), .ScaleFactor(_)): return true
+    case (.TargetCache(_), .TargetCache(_)): fallthrough
+    case (.Downloader(_), .Downloader(_)): fallthrough
+    case (.Transition(_), .Transition(_)): fallthrough
+    case (.DownloadPriority(_), .DownloadPriority(_)): fallthrough
+    case (.ForceRefresh, .ForceRefresh): fallthrough
+    case (.CacheMemoryOnly, .CacheMemoryOnly): fallthrough
+    case (.BackgroundDecode, .BackgroundDecode): fallthrough
+    case (.CallbackDispatchQueue(_), .CallbackDispatchQueue(_)): fallthrough
+    case (.ScaleFactor(_), .ScaleFactor(_)): fallthrough
+    case (.PreloadAllGIFData, .PreloadAllGIFData): return true
+        
     default: return false
     }
 }
@@ -142,6 +146,10 @@ extension CollectionType where Generator.Element == KingfisherOptionsInfoItem {
         return contains{ $0 <== .BackgroundDecode }
     }
     
+    var preloadAllGIFData: Bool {
+        return contains { $0 <== .PreloadAllGIFData }
+    }
+    
     var callbackDispatchQueue: dispatch_queue_t {
         if let item = kf_firstMatchIgnoringAssociatedValue(.CallbackDispatchQueue(nil)),
             case .CallbackDispatchQueue(let queue) = item

+ 28 - 7
Tests/KingfisherTests/ImageExtensionTests.swift

@@ -56,30 +56,51 @@ class ImageExtensionTests: XCTestCase {
     }
     
     func testGenerateGIFImage() {
-        let image = Image.kf_animatedImageWithGIFData(gifData: testImageGIFData)
+        let image = Image.kf_animatedImageWithGIFData(gifData: testImageGIFData, preloadAll: false)
         XCTAssertNotNil(image, "The image should be initiated.")
+#if os(iOS) || os(tvOS)
+        let count = ImagesCountWithImageSource(image!.kf_imageSource!.imageRef!)
+        XCTAssertEqual(count, 8, "There should be 8 frames.")
+#else
         XCTAssertEqual(image!.kf_images!.count, 8, "There should be 8 frames.")
         
         XCTAssertEqualWithAccuracy(image!.kf_duration, 0.8, accuracy: 0.001, "The image duration should be 0.8s")
+#endif
     }
     
     func testGIFRepresentation() {
-        let image = Image.kf_animatedImageWithGIFData(gifData: testImageGIFData)!
+        let image = Image.kf_animatedImageWithGIFData(gifData: testImageGIFData, preloadAll: false)!
         let data = ImageGIFRepresentation(image)
         
         XCTAssertNotNil(data, "Data should not be nil")
         XCTAssertEqual(data?.kf_imageFormat, ImageFormat.GIF)
         
-        let image1 = Image.kf_animatedImageWithGIFData(gifData: data!)!
-        XCTAssertEqual(image1.kf_duration, image.kf_duration)
-        XCTAssertEqual(image1.kf_images!.count, image.kf_images!.count)
+        let allLoadImage = Image.kf_animatedImageWithGIFData(gifData: data!, preloadAll: true)!
+        let allLoadData = ImageGIFRepresentation(allLoadImage)
+        XCTAssertNotNil(allLoadData, "Data1 should not be nil")
+        XCTAssertEqual(allLoadData?.kf_imageFormat, ImageFormat.GIF)
     }
     
     func testGenerateSingleFrameGIFImage() {
-        let image = Image.kf_animatedImageWithGIFData(gifData: testImageSingleFrameGIFData)
+        let image = Image.kf_animatedImageWithGIFData(gifData: testImageSingleFrameGIFData, preloadAll: false)
         XCTAssertNotNil(image, "The image should be initiated.")
-        XCTAssertEqual(image!.kf_images!.count, 1, "There should be 8 frames.")
+#if os(iOS) || os(tvOS)
+        let count = ImagesCountWithImageSource(image!.kf_imageSource!.imageRef!)
+        XCTAssertEqual(count, 1, "There should be 1 frames.")
+#else
+        XCTAssertEqual(image!.kf_images!.count, 1, "There should be 1 frames.")
         
         XCTAssertEqual(image!.kf_duration, Double.infinity, "The image duration should be 0 since it is not animated image.")
+#endif
+    }
+    
+    func testPreloadAllGIFData() {
+        let image = Image.kf_animatedImageWithGIFData(gifData: testImageSingleFrameGIFData, preloadAll: true)!
+        XCTAssertNotNil(image, "The image should be initiated.")
+#if os(iOS) || os(tvOS)
+        XCTAssertNil(image.kf_imageSource, "Image source should be nil")
+#endif
+        XCTAssertEqual(image.kf_duration, image.kf_duration)
+        XCTAssertEqual(image.kf_images!.count, image.kf_images!.count)
     }
 }