Bring back progressive loading

Loading the all the frames of a GIF at once into memory helped cut down
on the memory footprint because we could eliminate the need to hold onto
the source image. However, we see this break down when there are too
many frames. The "almost_nailed_it.gif" GIF has 545 frames and would
crash the app around 130 loaded. This brings back progressive loading
with a max frame count of 50 to prevent this issue.
This commit is contained in:
Tony DiPasquale 2015-06-04 15:38:52 -04:00
parent 1c833b16f4
commit a885c995e9
5 changed files with 115 additions and 38 deletions

View File

@ -7,16 +7,35 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
005656EB1A6EE471008A0ED1 /* Gifu.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 005656EA1A6EE471008A0ED1 /* Gifu.framework */; };
9D25870819BCCB0F00A55A18 /* mugen.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9D25870619BCCB0F00A55A18 /* mugen.gif */; }; 9D25870819BCCB0F00A55A18 /* mugen.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9D25870619BCCB0F00A55A18 /* mugen.gif */; };
9D98823D19BC69CA00B790C6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D98823C19BC69CA00B790C6 /* AppDelegate.swift */; }; 9D98823D19BC69CA00B790C6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D98823C19BC69CA00B790C6 /* AppDelegate.swift */; };
9D98823F19BC69CA00B790C6 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D98823E19BC69CA00B790C6 /* ViewController.swift */; }; 9D98823F19BC69CA00B790C6 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D98823E19BC69CA00B790C6 /* ViewController.swift */; };
9D98824419BC69CA00B790C6 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D98824319BC69CA00B790C6 /* Images.xcassets */; }; 9D98824419BC69CA00B790C6 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D98824319BC69CA00B790C6 /* Images.xcassets */; };
9D98825A19BC69F600B790C6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9D98825919BC69F600B790C6 /* Main.storyboard */; }; 9D98825A19BC69F600B790C6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9D98825919BC69F600B790C6 /* Main.storyboard */; };
9D98826719BC874C00B790C6 /* FlatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D98826619BC874C00B790C6 /* FlatButton.swift */; }; 9D98826719BC874C00B790C6 /* FlatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D98826619BC874C00B790C6 /* FlatButton.swift */; };
EA5789A01B20C5B100A9F7D1 /* almost_nailed_it.gif in Resources */ = {isa = PBXBuildFile; fileRef = EA57899F1B20C5B100A9F7D1 /* almost_nailed_it.gif */; };
EA5789A71B20C65E00A9F7D1 /* Gifu.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA5789A61B20C65800A9F7D1 /* Gifu.framework */; };
EA5789A91B20C68B00A9F7D1 /* Gifu.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EA5789A61B20C65800A9F7D1 /* Gifu.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
EA9299291AE99E2900E22976 /* Runes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA9299281AE99E2900E22976 /* Runes.framework */; }; EA9299291AE99E2900E22976 /* Runes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA9299281AE99E2900E22976 /* Runes.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
EA5789A51B20C65800A9F7D1 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = EA5789A11B20C65800A9F7D1 /* Gifu.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 00B8C73E1A364DA400C188E7;
remoteInfo = Gifu;
};
EA5789AA1B20C68B00A9F7D1 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = EA5789A11B20C65800A9F7D1 /* Gifu.xcodeproj */;
proxyType = 1;
remoteGlobalIDString = 00B8C73D1A364DA400C188E7;
remoteInfo = Gifu;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */ /* Begin PBXCopyFilesBuildPhase section */
00B8C7331A364D4C00C188E7 /* Embed Frameworks */ = { 00B8C7331A364D4C00C188E7 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase; isa = PBXCopyFilesBuildPhase;
@ -24,6 +43,7 @@
dstPath = ""; dstPath = "";
dstSubfolderSpec = 10; dstSubfolderSpec = 10;
files = ( files = (
EA5789A91B20C68B00A9F7D1 /* Gifu.framework in Embed Frameworks */,
); );
name = "Embed Frameworks"; name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -31,7 +51,6 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
005656EA1A6EE471008A0ED1 /* Gifu.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Gifu.framework; path = "../../build/Debug-iphoneos/Gifu.framework"; sourceTree = "<group>"; };
9D25870519BCCB0F00A55A18 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 9D25870519BCCB0F00A55A18 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9D25870619BCCB0F00A55A18 /* mugen.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = mugen.gif; sourceTree = "<group>"; }; 9D25870619BCCB0F00A55A18 /* mugen.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = mugen.gif; sourceTree = "<group>"; };
9D98823719BC69CA00B790C6 /* gifu-demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "gifu-demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 9D98823719BC69CA00B790C6 /* gifu-demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "gifu-demo.app"; sourceTree = BUILT_PRODUCTS_DIR; };
@ -40,6 +59,8 @@
9D98824319BC69CA00B790C6 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; }; 9D98824319BC69CA00B790C6 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
9D98825919BC69F600B790C6 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = "<group>"; }; 9D98825919BC69F600B790C6 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = "<group>"; };
9D98826619BC874C00B790C6 /* FlatButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FlatButton.swift; path = classes/FlatButton.swift; sourceTree = "<group>"; }; 9D98826619BC874C00B790C6 /* FlatButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FlatButton.swift; path = classes/FlatButton.swift; sourceTree = "<group>"; };
EA57899F1B20C5B100A9F7D1 /* almost_nailed_it.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = almost_nailed_it.gif; sourceTree = "<group>"; };
EA5789A11B20C65800A9F7D1 /* Gifu.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Gifu.xcodeproj; path = ../Gifu.xcodeproj; sourceTree = "<group>"; };
EA9299281AE99E2900E22976 /* Runes.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Runes.framework; path = ../Carthage/Build/iOS/Runes.framework; sourceTree = "<group>"; }; EA9299281AE99E2900E22976 /* Runes.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Runes.framework; path = ../Carthage/Build/iOS/Runes.framework; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -48,8 +69,8 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
005656EB1A6EE471008A0ED1 /* Gifu.framework in Frameworks */,
EA9299291AE99E2900E22976 /* Runes.framework in Frameworks */, EA9299291AE99E2900E22976 /* Runes.framework in Frameworks */,
EA5789A71B20C65E00A9F7D1 /* Gifu.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -90,16 +111,25 @@
9D98823A19BC69CA00B790C6 /* Supporting Files */ = { 9D98823A19BC69CA00B790C6 /* Supporting Files */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
005656EA1A6EE471008A0ED1 /* Gifu.framework */,
9D25870519BCCB0F00A55A18 /* Info.plist */, 9D25870519BCCB0F00A55A18 /* Info.plist */,
EA57899F1B20C5B100A9F7D1 /* almost_nailed_it.gif */,
9D25870619BCCB0F00A55A18 /* mugen.gif */, 9D25870619BCCB0F00A55A18 /* mugen.gif */,
); );
name = "Supporting Files"; name = "Supporting Files";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
EA5789A21B20C65800A9F7D1 /* Products */ = {
isa = PBXGroup;
children = (
EA5789A61B20C65800A9F7D1 /* Gifu.framework */,
);
name = Products;
sourceTree = "<group>";
};
EA92992C1AE9AB2100E22976 /* Frameworks */ = { EA92992C1AE9AB2100E22976 /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EA5789A11B20C65800A9F7D1 /* Gifu.xcodeproj */,
EA9299281AE99E2900E22976 /* Runes.framework */, EA9299281AE99E2900E22976 /* Runes.framework */,
); );
name = Frameworks; name = Frameworks;
@ -121,6 +151,7 @@
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
EA5789AB1B20C68B00A9F7D1 /* PBXTargetDependency */,
); );
name = "gifu-demo"; name = "gifu-demo";
productName = "gifu-demo"; productName = "gifu-demo";
@ -152,6 +183,12 @@
mainGroup = 9D98822E19BC69CA00B790C6; mainGroup = 9D98822E19BC69CA00B790C6;
productRefGroup = 9D98823819BC69CA00B790C6 /* Products */; productRefGroup = 9D98823819BC69CA00B790C6 /* Products */;
projectDirPath = ""; projectDirPath = "";
projectReferences = (
{
ProductGroup = EA5789A21B20C65800A9F7D1 /* Products */;
ProjectRef = EA5789A11B20C65800A9F7D1 /* Gifu.xcodeproj */;
},
);
projectRoot = ""; projectRoot = "";
targets = ( targets = (
9D98823619BC69CA00B790C6 /* gifu-demo */, 9D98823619BC69CA00B790C6 /* gifu-demo */,
@ -159,11 +196,22 @@
}; };
/* End PBXProject section */ /* End PBXProject section */
/* Begin PBXReferenceProxy section */
EA5789A61B20C65800A9F7D1 /* Gifu.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
path = Gifu.framework;
remoteRef = EA5789A51B20C65800A9F7D1 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
/* End PBXReferenceProxy section */
/* Begin PBXResourcesBuildPhase section */ /* Begin PBXResourcesBuildPhase section */
9D98823519BC69CA00B790C6 /* Resources */ = { 9D98823519BC69CA00B790C6 /* Resources */ = {
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
EA5789A01B20C5B100A9F7D1 /* almost_nailed_it.gif in Resources */,
9D98824419BC69CA00B790C6 /* Images.xcassets in Resources */, 9D98824419BC69CA00B790C6 /* Images.xcassets in Resources */,
9D25870819BCCB0F00A55A18 /* mugen.gif in Resources */, 9D25870819BCCB0F00A55A18 /* mugen.gif in Resources */,
9D98825A19BC69F600B790C6 /* Main.storyboard in Resources */, 9D98825A19BC69F600B790C6 /* Main.storyboard in Resources */,
@ -202,6 +250,14 @@
}; };
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
EA5789AB1B20C68B00A9F7D1 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = Gifu;
targetProxy = EA5789AA1B20C68B00A9F7D1 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
9D98825119BC69CA00B790C6 /* Debug */ = { 9D98825119BC69CA00B790C6 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

View File

@ -9,7 +9,7 @@ class ViewController: UIViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
imageView.animateWithImage(named: "mugen.gif") imageView.animateWithImage(named: "almost_nailed_it.gif")
UIApplication.sharedApplication().setStatusBarStyle(.LightContent, animated: false) UIApplication.sharedApplication().setStatusBarStyle(.LightContent, animated: false)
} }

View File

@ -7,6 +7,10 @@ public class AnimatableImageView: UIImageView, Animatable {
/// An `Animator` instance that holds the frames of a specific image in memory. /// An `Animator` instance that holds the frames of a specific image in memory.
var animator: Animator? var animator: Animator?
deinit {
println("deinit animatable view")
}
/// A computed property that returns whether the image view is animating. /// A computed property that returns whether the image view is animating.
public var isAnimatingGIF: Bool { public var isAnimatingGIF: Bool {
return animator?.isAnimating ?? isAnimating() return animator?.isAnimating ?? isAnimating()
@ -59,5 +63,9 @@ public class AnimatableImageView: UIImageView, Animatable {
public func stopAnimatingGIF() { public func stopAnimatingGIF() {
animator?.pauseAnimation() ?? stopAnimating() animator?.pauseAnimation() ?? stopAnimating()
} }
public func cleanup() {
animator = .None
}
} }

View File

@ -8,10 +8,14 @@ class Animator: NSObject {
let delegate: Animatable let delegate: Animatable
/// Maximum duration to increment the frame timer with. /// Maximum duration to increment the frame timer with.
private let maxTimeStep = 1.0 private let maxTimeStep = 1.0
/// The total duration of the GIF image.
private var totalDuration: NSTimeInterval = 0.0
/// An array of animated frames from a single GIF image. /// An array of animated frames from a single GIF image.
private var animatedFrames = [AnimatedFrame]() private var animatedFrames = [AnimatedFrame]()
/// Maximum number of frames to load at once
private let maxNumberOfFrames = 50
/// The total number of frames in the GIF.
private var numberOfFrames = 0
/// A reference to the original image source.
private var imageSource: CGImageSourceRef
/// The index of the current GIF frame. /// The index of the current GIF frame.
private var currentFrameIndex = 0 private var currentFrameIndex = 0
/// Time elapsed since the last frame change. Used to determine when the frame should be updated. /// Time elapsed since the last frame change. Used to determine when the frame should be updated.
@ -34,43 +38,46 @@ class Animator: NSObject {
/// :param: data The raw GIF image data. /// :param: data The raw GIF image data.
/// :param: delegate An `Animatable` delegate. /// :param: delegate An `Animatable` delegate.
required init(data: NSData, delegate: Animatable) { required init(data: NSData, delegate: Animatable) {
let imageSource = CGImageSourceCreateWithData(data, nil) imageSource = CGImageSourceCreateWithData(data, nil)
self.delegate = delegate self.delegate = delegate
super.init() super.init()
attachDisplayLink() attachDisplayLink()
curry(prepareFrames) <^> imageSource <*> delegate.frame.size prepareFrames()
pauseAnimation() pauseAnimation()
} }
deinit {
println("deinit animator")
}
// MARK: - Frames // MARK: - Frames
/// Loads the frames from an image source, resizes them, then caches them in `animatedFrames`. /// Loads the frames from an image source, resizes them, then caches them in `animatedFrames`.
private func prepareFrames() {
numberOfFrames = Int(CGImageSourceGetCount(imageSource))
let framesToProcess = numberOfFrames > maxNumberOfFrames ? maxNumberOfFrames : numberOfFrames
animatedFrames.reserveCapacity(framesToProcess)
animatedFrames = reduce(0..<framesToProcess, []) { $0 + pure(prepareFrame($1)) }
}
/// Loads a single frame from an image source, resizes it, then returns an `AnimatedFrame`.
/// ///
/// :param: imageSource The `CGImageSourceRef` image source to extract the frames from. /// :param: index The index of the GIF image source to prepare
/// :param: size The size to use for the cached frames. /// :returns: An AnimatedFrame object
private func prepareFrames(imageSource: CGImageSourceRef, size: CGSize) { private func prepareFrame(index: Int) -> AnimatedFrame {
let numberOfFrames = Int(CGImageSourceGetCount(imageSource)) let frameDuration = CGImageSourceGIFFrameDuration(imageSource, index)
animatedFrames.reserveCapacity(numberOfFrames) let frameImageRef = CGImageSourceCreateImageAtIndex(imageSource, index, nil)
let size = delegate.frame.size
(animatedFrames, totalDuration) = reduce(0..<numberOfFrames, ([AnimatedFrame](), 0.0)) { accumulator, index in let image = UIImage(CGImage: frameImageRef)
let accumulatedFrames = accumulator.0 let scaledImage: UIImage?
let accumulatedDuration = accumulator.1
let frameDuration = CGImageSourceGIFFrameDuration(imageSource, index) switch delegate.contentMode {
let frameImageRef = CGImageSourceCreateImageAtIndex(imageSource, index, nil) case .ScaleAspectFit: scaledImage = image?.resizeAspectFit(size)
case .ScaleAspectFill: scaledImage = image?.resizeAspectFill(size)
let image = UIImage(CGImage: frameImageRef) default: scaledImage = image?.resize(size)
let scaledImage: UIImage?
switch delegate.contentMode {
case .ScaleAspectFit: scaledImage = image?.resizeAspectFit(size)
case .ScaleAspectFill: scaledImage = image?.resizeAspectFill(size)
default: scaledImage = image?.resize(size)
}
let animatedFrame = AnimatedFrame(image: scaledImage, duration: frameDuration)
return (accumulatedFrames + [animatedFrame], accumulatedDuration + frameDuration)
} }
return AnimatedFrame(image: scaledImage, duration: frameDuration)
} }
/// Returns the frame at a particular index. /// Returns the frame at a particular index.
@ -78,23 +85,29 @@ class Animator: NSObject {
/// :param: index The index of the frame. /// :param: index The index of the frame.
/// :returns: An optional image at a given frame. /// :returns: An optional image at a given frame.
private func frameAtIndex(index: Int) -> UIImage? { private func frameAtIndex(index: Int) -> UIImage? {
if index >= animatedFrames.count { return .None } return animatedFrames[index % animatedFrames.count].image
return animatedFrames[index].image
} }
/// Updates the current frame if necessary using the frame timer and the duration of each frame in `animatedFrames`. /// Updates the current frame if necessary using the frame timer and the duration of each frame in `animatedFrames`.
/// ///
/// :returns: An optional image at a given frame. /// :returns: An optional image at a given frame.
func updateCurrentFrame() { func updateCurrentFrame() {
if totalDuration == 0 { return } if animatedFrames.count <= 1 { return }
timeSinceLastFrameChange += min(maxTimeStep, displayLink.duration) timeSinceLastFrameChange += min(maxTimeStep, displayLink.duration)
var frameDuration = animatedFrames[currentFrameIndex].duration var frameDuration = animatedFrames[currentFrameIndex % animatedFrames.count].duration
if timeSinceLastFrameChange >= frameDuration { if timeSinceLastFrameChange >= frameDuration {
timeSinceLastFrameChange -= frameDuration timeSinceLastFrameChange -= frameDuration
currentFrameIndex = ++currentFrameIndex % animatedFrames.count let lastFrameIndex = currentFrameIndex
currentFrameIndex = ++currentFrameIndex % numberOfFrames
delegate.layer.setNeedsDisplay() delegate.layer.setNeedsDisplay()
// load the next needed frame for progressive loading
if animatedFrames.count < numberOfFrames {
let nextFrameToLoad = (lastFrameIndex + animatedFrames.count) % numberOfFrames
animatedFrames[lastFrameIndex % animatedFrames.count] = prepareFrame(nextFrameToLoad)
}
} }
} }
@ -106,7 +119,7 @@ class Animator: NSObject {
/// Resumes the display link. /// Resumes the display link.
func resumeAnimation() { func resumeAnimation() {
if totalDuration > 0 { if animatedFrames.count > 1 {
displayLink.paused = false displayLink.paused = false
} }
} }