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 = {
/* 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 */; };
9D98823D19BC69CA00B790C6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D98823C19BC69CA00B790C6 /* AppDelegate.swift */; };
9D98823F19BC69CA00B790C6 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D98823E19BC69CA00B790C6 /* ViewController.swift */; };
9D98824419BC69CA00B790C6 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D98824319BC69CA00B790C6 /* Images.xcassets */; };
9D98825A19BC69F600B790C6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9D98825919BC69F600B790C6 /* Main.storyboard */; };
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 */; };
/* 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 */
00B8C7331A364D4C00C188E7 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
@ -24,6 +43,7 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
EA5789A91B20C68B00A9F7D1 /* Gifu.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@ -31,7 +51,6 @@
/* End PBXCopyFilesBuildPhase 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>"; };
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; };
@ -40,6 +59,8 @@
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>"; };
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>"; };
/* End PBXFileReference section */
@ -48,8 +69,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
005656EB1A6EE471008A0ED1 /* Gifu.framework in Frameworks */,
EA9299291AE99E2900E22976 /* Runes.framework in Frameworks */,
EA5789A71B20C65E00A9F7D1 /* Gifu.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -90,16 +111,25 @@
9D98823A19BC69CA00B790C6 /* Supporting Files */ = {
isa = PBXGroup;
children = (
005656EA1A6EE471008A0ED1 /* Gifu.framework */,
9D25870519BCCB0F00A55A18 /* Info.plist */,
EA57899F1B20C5B100A9F7D1 /* almost_nailed_it.gif */,
9D25870619BCCB0F00A55A18 /* mugen.gif */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
EA5789A21B20C65800A9F7D1 /* Products */ = {
isa = PBXGroup;
children = (
EA5789A61B20C65800A9F7D1 /* Gifu.framework */,
);
name = Products;
sourceTree = "<group>";
};
EA92992C1AE9AB2100E22976 /* Frameworks */ = {
isa = PBXGroup;
children = (
EA5789A11B20C65800A9F7D1 /* Gifu.xcodeproj */,
EA9299281AE99E2900E22976 /* Runes.framework */,
);
name = Frameworks;
@ -121,6 +151,7 @@
buildRules = (
);
dependencies = (
EA5789AB1B20C68B00A9F7D1 /* PBXTargetDependency */,
);
name = "gifu-demo";
productName = "gifu-demo";
@ -152,6 +183,12 @@
mainGroup = 9D98822E19BC69CA00B790C6;
productRefGroup = 9D98823819BC69CA00B790C6 /* Products */;
projectDirPath = "";
projectReferences = (
{
ProductGroup = EA5789A21B20C65800A9F7D1 /* Products */;
ProjectRef = EA5789A11B20C65800A9F7D1 /* Gifu.xcodeproj */;
},
);
projectRoot = "";
targets = (
9D98823619BC69CA00B790C6 /* gifu-demo */,
@ -159,11 +196,22 @@
};
/* 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 */
9D98823519BC69CA00B790C6 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
EA5789A01B20C5B100A9F7D1 /* almost_nailed_it.gif in Resources */,
9D98824419BC69CA00B790C6 /* Images.xcassets in Resources */,
9D25870819BCCB0F00A55A18 /* mugen.gif in Resources */,
9D98825A19BC69F600B790C6 /* Main.storyboard in Resources */,
@ -202,6 +250,14 @@
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
EA5789AB1B20C68B00A9F7D1 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = Gifu;
targetProxy = EA5789AA1B20C68B00A9F7D1 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
9D98825119BC69CA00B790C6 /* Debug */ = {
isa = XCBuildConfiguration;

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

View File

@ -9,7 +9,7 @@ class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
imageView.animateWithImage(named: "mugen.gif")
imageView.animateWithImage(named: "almost_nailed_it.gif")
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.
var animator: Animator?
deinit {
println("deinit animatable view")
}
/// A computed property that returns whether the image view is animating.
public var isAnimatingGIF: Bool {
return animator?.isAnimating ?? isAnimating()
@ -59,5 +63,9 @@ public class AnimatableImageView: UIImageView, Animatable {
public func stopAnimatingGIF() {
animator?.pauseAnimation() ?? stopAnimating()
}
public func cleanup() {
animator = .None
}
}

View File

@ -8,10 +8,14 @@ class Animator: NSObject {
let delegate: Animatable
/// Maximum duration to increment the frame timer with.
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.
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.
private var currentFrameIndex = 0
/// 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: delegate An `Animatable` delegate.
required init(data: NSData, delegate: Animatable) {
let imageSource = CGImageSourceCreateWithData(data, nil)
imageSource = CGImageSourceCreateWithData(data, nil)
self.delegate = delegate
super.init()
attachDisplayLink()
curry(prepareFrames) <^> imageSource <*> delegate.frame.size
prepareFrames()
pauseAnimation()
}
deinit {
println("deinit animator")
}
// MARK: - Frames
/// 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: size The size to use for the cached frames.
private func prepareFrames(imageSource: CGImageSourceRef, size: CGSize) {
let numberOfFrames = Int(CGImageSourceGetCount(imageSource))
animatedFrames.reserveCapacity(numberOfFrames)
/// :param: index The index of the GIF image source to prepare
/// :returns: An AnimatedFrame object
private func prepareFrame(index: Int) -> AnimatedFrame {
let frameDuration = CGImageSourceGIFFrameDuration(imageSource, index)
let frameImageRef = CGImageSourceCreateImageAtIndex(imageSource, index, nil)
let size = delegate.frame.size
(animatedFrames, totalDuration) = reduce(0..<numberOfFrames, ([AnimatedFrame](), 0.0)) { accumulator, index in
let accumulatedFrames = accumulator.0
let accumulatedDuration = accumulator.1
let image = UIImage(CGImage: frameImageRef)
let scaledImage: UIImage?
let frameDuration = CGImageSourceGIFFrameDuration(imageSource, index)
let frameImageRef = CGImageSourceCreateImageAtIndex(imageSource, index, nil)
let image = UIImage(CGImage: frameImageRef)
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)
switch delegate.contentMode {
case .ScaleAspectFit: scaledImage = image?.resizeAspectFit(size)
case .ScaleAspectFill: scaledImage = image?.resizeAspectFill(size)
default: scaledImage = image?.resize(size)
}
return AnimatedFrame(image: scaledImage, duration: frameDuration)
}
/// Returns the frame at a particular index.
@ -78,23 +85,29 @@ class Animator: NSObject {
/// :param: index The index of the frame.
/// :returns: An optional image at a given frame.
private func frameAtIndex(index: Int) -> UIImage? {
if index >= animatedFrames.count { return .None }
return animatedFrames[index].image
return animatedFrames[index % animatedFrames.count].image
}
/// 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.
func updateCurrentFrame() {
if totalDuration == 0 { return }
if animatedFrames.count <= 1 { return }
timeSinceLastFrameChange += min(maxTimeStep, displayLink.duration)
var frameDuration = animatedFrames[currentFrameIndex].duration
var frameDuration = animatedFrames[currentFrameIndex % animatedFrames.count].duration
if timeSinceLastFrameChange >= frameDuration {
timeSinceLastFrameChange -= frameDuration
currentFrameIndex = ++currentFrameIndex % animatedFrames.count
let lastFrameIndex = currentFrameIndex
currentFrameIndex = ++currentFrameIndex % numberOfFrames
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.
func resumeAnimation() {
if totalDuration > 0 {
if animatedFrames.count > 1 {
displayLink.paused = false
}
}