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:
parent
1c833b16f4
commit
a885c995e9
|
@ -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 |
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue