/ /
/ / FLAnimatedImage . m
/ / Flipboard
/ /
/ / Created by Raphael Schaad on 7 / 8 / 13.
/ / Copyright ( c ) 2013 - 2015 Flipboard . All rights reserved .
/ /
#import "FLAnimatedImage . h "
#import < ImageIO / ImageIO . h >
#import < MobileCoreServices / MobileCoreServices . h >
/ / From vm_param . h , define for iOS 8.0 or higher to build on device .
#ifndef BYTE_SIZE
#define BYTE_SIZE 8 / / byte size in bits
#endif
#define MEGABYTE ( 1024 * 1024 )
#if FLLumberjackIntegrationEnabled && defined ( FLLumberjackAvailable )
#if defined ( DEBUG ) && DEBUG
#if defined ( LOG_LEVEL_DEBUG ) / / CocoaLumberjack 1. x
int flAnimatedImageLogLevel = LOG_LEVEL_DEBUG ;
#else / / CocoaLumberjack 2. x
int flAnimatedImageLogLevel = DDLogFlagDebug ;
#endif
#else
#if defined ( LOG_LEVEL_WARN ) / / CocoaLumberjack 1. x
int flAnimatedImageLogLevel = LOG_LEVEL_WARN ;
#else / / CocoaLumberjack 2. x
int flAnimatedImageLogLevel = DDLogFlagWarning ;
#endif
#endif
#endif
/ / An animated image ' s data size ( dimensions * frameCount ) category ; its value is the max allowed memory ( in MB ) .
/ / E . g . : A 100 x200px GIF with 30 frames is ~ 2.3 MB in our pixel format and would fall into the `FLAnimatedImageDataSizeCategoryAll ` category .
typedef NS_ENUM ( NSUInteger , FLAnimatedImageDataSizeCategory ) {
FLAnimatedImageDataSizeCategoryAll = 10 , / / All frames permanently in memory ( be nice to the CPU )
FLAnimatedImageDataSizeCategoryDefault = 75 , / / A frame cache of default size in memory ( usually real - time performance and keeping low memory profile )
FLAnimatedImageDataSizeCategoryOnDemand = 250 , / / Only keep one frame at the time in memory ( easier on memory , slowest performance )
FLAnimatedImageDataSizeCategoryUnsupported / / Even for one frame too large , computer says no .
} ;
typedef NS_ENUM ( NSUInteger , FLAnimatedImageFrameCacheSize ) {
FLAnimatedImageFrameCacheSizeNoLimit = 0 , / / 0 means no specific limit
FLAnimatedImageFrameCacheSizeLowMemory = 1 , / / The minimum frame cache size ; this will produce frames on - demand .
FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning = 2 , / / If we can produce the frames faster than we consume , one frame ahead will already result in a stutter - free playback .
FLAnimatedImageFrameCacheSizeDefault = 5 / / Build up a comfy buffer window to cope with CPU hiccups etc .
} ;
@ interface FLAnimatedImage ( )
@ property ( nonatomic , assign , readonly ) NSUInteger frameCacheSizeOptimal ; / / The optimal number of frames to cache based on image size & number of frames ; never changes
@ property ( nonatomic , assign ) NSUInteger frameCacheSizeMaxInternal ; / / Allow to cap the cache size e . g . when memory warnings occur ; 0 means no specific limit ( default )
@ property ( nonatomic , assign ) NSUInteger requestedFrameIndex ; / / Most recently requested frame index
@ property ( nonatomic , assign , readonly ) NSUInteger posterImageFrameIndex ; / / Index of non - purgable poster image ; never changes
@ property ( nonatomic , strong , readonly ) NSMutableDictionary * cachedFramesForIndexes ;
@ property ( nonatomic , strong , readonly ) NSMutableIndexSet * cachedFrameIndexes ; / / Indexes of cached frames
@ property ( nonatomic , strong , readonly ) NSMutableIndexSet * requestedFrameIndexes ; / / Indexes of frames that are currently produced in the background
@ property ( nonatomic , strong , readonly ) NSIndexSet * allFramesIndexSet ; / / Default index set with the full range of indexes ; never changes
@ property ( nonatomic , assign ) NSUInteger memoryWarningCount ;
@ property ( nonatomic , strong , readonly ) dispatch_queue_t serialQueue ;
@ property ( nonatomic , strong , readonly ) __attribute__ ( ( NSObject ) ) CGImageSourceRef imageSource ;
/ / The weak proxy is used to break retain cycles with delayed actions from memory warnings .
/ / We are lying about the actual type here to gain static type checking and eliminate casts .
/ / The actual type of the object is `FLWeakProxy `.
@ property ( nonatomic , strong , readonly ) FLAnimatedImage * weakProxy ;
@ end
/ / For custom dispatching of memory warnings to avoid deallocation races since NSNotificationCenter doesn ' t retain objects it is notifying .
static NSHashTable * allAnimatedImagesWeak ;
@ implementation FLAnimatedImage
#pragma mark - Accessors
#pragma mark Public
/ / This is the definite value the frame cache needs to size itself to .
- ( NSUInteger ) frameCacheSizeCurrent
{
NSUInteger frameCacheSizeCurrent = self . frameCacheSizeOptimal ;
/ / If set , respect the caps .
if ( self . frameCacheSizeMax > FLAnimatedImageFrameCacheSizeNoLimit ) {
frameCacheSizeCurrent = MIN ( frameCacheSizeCurrent , self . frameCacheSizeMax ) ;
}
if ( self . frameCacheSizeMaxInternal > FLAnimatedImageFrameCacheSizeNoLimit ) {
frameCacheSizeCurrent = MIN ( frameCacheSizeCurrent , self . frameCacheSizeMaxInternal ) ;
}
return frameCacheSizeCurrent ;
}
- ( void ) setFrameCacheSizeMax : ( NSUInteger ) frameCacheSizeMax
{
if ( _frameCacheSizeMax != frameCacheSizeMax ) {
/ / Remember whether the new cap will cause the current cache size to shrink ; then we ' ll make sure to purge from the cache if needed .
BOOL willFrameCacheSizeShrink = ( frameCacheSizeMax < self . frameCacheSizeCurrent ) ;
/ / Update the value
_frameCacheSizeMax = frameCacheSizeMax ;
if ( willFrameCacheSizeShrink ) {
[ self purgeFrameCacheIfNeeded ] ;
}
}
}
#pragma mark Private
- ( void ) setFrameCacheSizeMaxInternal : ( NSUInteger ) frameCacheSizeMaxInternal
{
if ( _frameCacheSizeMaxInternal != frameCacheSizeMaxInternal ) {
/ / Remember whether the new cap will cause the current cache size to shrink ; then we ' ll make sure to purge from the cache if needed .
BOOL willFrameCacheSizeShrink = ( frameCacheSizeMaxInternal < self . frameCacheSizeCurrent ) ;
/ / Update the value
_frameCacheSizeMaxInternal = frameCacheSizeMaxInternal ;
if ( willFrameCacheSizeShrink ) {
[ self purgeFrameCacheIfNeeded ] ;
}
}
}
#pragma mark - Life Cycle
+ ( void ) initialize
{
if ( self == [ FLAnimatedImage class ] ) {
/ / UIKit memory warning notification handler shared by all of the instances
allAnimatedImagesWeak = [ NSHashTable weakObjectsHashTable ] ;
[ [ NSNotificationCenter defaultCenter ] addObserverForName : UIApplicationDidReceiveMemoryWarningNotification object : nil queue : nil usingBlock : ^( NSNotification * note ) {
/ / UIKit notifications are posted on the main thread . didReceiveMemoryWarning : is expecting the main run loop , and we don ' t lock on allAnimatedImagesWeak
NSAssert ( [ NSThread isMainThread ] , @ "Received memory warning on non - main thread ") ;
/ / Get a strong reference to all of the images . If an instance is returned in this array , it is still live and has not entered dealloc .
/ / Note that FLAnimatedImages can be created on any thread , so the hash table must be locked .
NSArray * images = nil ;
@ synchronized ( allAnimatedImagesWeak ) {
images = [ [ allAnimatedImagesWeak allObjects ] copy ] ;
}
/ / Now issue notifications to all of the images while holding a strong reference to them
[ images makeObjectsPerformSelector : @ selector ( didReceiveMemoryWarning : ) withObject : note ] ;
} ] ;
}
}
- ( instancetype ) init
{
FLAnimatedImage * animatedImage = [ self initWithAnimatedGIFData : nil ] ;
if ( !animatedImage ) {
FLLogError ( @ "Use `- initWithAnimatedGIFData : ` and supply the animated GIF data as an argument to initialize an object of type `FLAnimatedImage `. ") ;
}
return animatedImage ;
}
- ( instancetype ) initWithAnimatedGIFData : ( NSData * ) data
{
/ / Early return if no data supplied !
BOOL hasData = ( [ data length ] > 0 ) ;
if ( !hasData ) {
FLLogError ( @ "No animated GIF data supplied . ") ;
return nil ;
}
self = [ super init ] ;
if ( self ) {
/ / Do one - time initializations of `readonly ` properties directly to ivar to prevent implicit actions and avoid need for private `readwrite ` property overrides .
/ / Keep a strong reference to `data ` and expose it read - only publicly .
/ / However , we will use the `_imageSource ` as handler to the image data throughout our life cycle .
_data = data ;
/ / Initialize internal data structures
_cachedFramesForIndexes = [ [ NSMutableDictionary alloc ] init ] ;
_cachedFrameIndexes = [ [ NSMutableIndexSet alloc ] init ] ;
_requestedFrameIndexes = [ [ NSMutableIndexSet alloc ] init ] ;
/ / Note : We could leverage `CGImageSourceCreateWithURL ` too to add a second initializer `- initWithAnimatedGIFContentsOfURL : `.
_imageSource = CGImageSourceCreateWithData ( ( __bridge CFDataRef ) data , NULL ) ;
/ / Early return on failure !
if ( !_imageSource ) {
FLLogError ( @ "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 ) {
FLLogError ( @ "Supplied data is of type %@ and doesn't seem to be GIF data %@", imageSourceContainerType, data);
return nil ;
}
/ / Get `LoopCount `
/ / Note : 0 means repeating the animation indefinitely .
/ / Image properties example :
/ / {
/ / FileSize = 314446 ;
/ / "{ GIF } " = {
/ / HasGlobalColorMap = 1 ;
/ / LoopCount = 0 ;
/ / } ;
/ / }
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 + + ) {
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 ] ;
}
/ / Get `DelayTime `
/ / Note : It ' s not in ( 1 / 100 ) of a second like still falsely described in the documentation as per iOS 8 ( rdar : / / 19507384 ) but in seconds stored as `kCFNumberFloat32Type `.
/ / Frame properties example :
/ / {
/ / ColorModel = RGB ;
/ / Depth = 8 ;
/ / PixelHeight = 960 ;
/ / PixelWidth = 640 ;
/ / "{ GIF } " = {
/ / DelayTime = "0.4 ";
/ / UnclampedDelayTime = "0.4 ";
/ / } ;
/ / }
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 ] ;
}
/ / If we don ' t get a delay time from the properties , fall back to `kDelayTimeIntervalDefault ` or carry over the preceding frame ' s value .
const NSTimeInterval kDelayTimeIntervalDefault = 0.1 ;
if ( !delayTime ) {
if ( i == 0 ) {
FLLogInfo ( @ "Falling back to default delay time for first frame %@ because none found in GIF properties %@", frameImage, frameProperties);
delayTime = @ ( kDelayTimeIntervalDefault ) ;
} else {
FLLogInfo ( @ "Falling back to preceding delay time for frame %zu %@ because none found in GIF properties %@", i, frameImage, frameProperties);
delayTime = delayTimesForIndexesMutable [ @ ( i - 1 ) ] ;
}
}
/ / Support frame delays as low as `kDelayTimeIntervalMinimum `, with anything below being rounded up to `kDelayTimeIntervalDefault ` for legacy compatibility .
/ / This is how the fastest browsers do it as per 2012 : http : / / nullsleep . tumblr . com / post / 16524517190 / animated - gif - minimum - frame - delay - browser - compatibility
const NSTimeInterval kDelayTimeIntervalMinimum = 0.02 ;
/ / To support the minimum even when rounding errors occur , use an epsilon when comparing . We downcast to float because that ' s what we get for delayTime from ImageIO .
if ( [ delayTime floatValue ] < ( ( float ) kDelayTimeIntervalMinimum - FLT_EPSILON ) ) {
FLLogInfo ( @ "Rounding frame %zu's `delayTime` from %f up to default %f (minimum supported: %f).", i, [delayTime floatValue], kDelayTimeIntervalDefault, kDelayTimeIntervalMinimum);
delayTime = @ ( kDelayTimeIntervalDefault ) ;
}
delayTimesForIndexesMutable [ @ ( i ) ] = delayTime ;
} else {
skippedFrameCount + + ;
FLLogInfo ( @ "Dropping frame %zu because valid `CGImageRef` %@ did result in `nil`-`UIImage`.", i, frameImageRef);
}
CFRelease ( frameImageRef ) ;
} else {
skippedFrameCount + + ;
FLLogInfo ( @ "Dropping frame %zu because failed to `CGImageSourceCreateImageAtIndex` with image source %@", i, _imageSource);
}
}
_delayTimesForIndexes = [ delayTimesForIndexesMutable copy ] ;
_frameCount = imageCount ;
if ( self . frameCount == 0 ) {
FLLogInfo ( @ "Failed to create any valid frames for GIF with properties %@", imageProperties);
return nil ;
} else if ( self . frameCount == 1 ) {
/ / Warn when we only have a single frame but return a valid GIF .
FLLogInfo ( @ "Created valid GIF but with only a single frame . Image properties : %@", imageProperties);
} else {
/ / We have multiple frames , rock on !
}
/ / Calculate the optimal frame cache size : try choosing a larger buffer window depending on the predicted image size .
/ / It ' s only dependent on the image size & number of frames and never changes .
CGFloat animatedImageDataSize = CGImageGetBytesPerRow ( self . posterImage . CGImage ) * self . size . height * ( self . frameCount - skippedFrameCount ) / MEGABYTE ;
if ( animatedImageDataSize < = FLAnimatedImageDataSizeCategoryAll ) {
_frameCacheSizeOptimal = self . frameCount ;
} else if ( animatedImageDataSize < = FLAnimatedImageDataSizeCategoryDefault ) {
/ / This value doesn ' t depend on device memory much because if we ' re not keeping all frames in memory we will always be decoding 1 frame up ahead per 1 frame that gets played and at this point we might as well just keep a small buffer just large enough to keep from running out of frames .
_frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeDefault ;
} else {
/ / The predicted size exceeds the limits to build up a cache and we go into low memory mode from the beginning .
_frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeLowMemory ;
}
/ / In any case , cap the optimal cache size at the frame count .
_frameCacheSizeOptimal = MIN ( _frameCacheSizeOptimal , self . frameCount ) ;
/ / Convenience / minor performance optimization ; keep an index set handy with the full range to return in `- frameIndexesToCache `.
_allFramesIndexSet = [ [ NSIndexSet alloc ] initWithIndexesInRange : NSMakeRange ( 0 , self . frameCount ) ] ;
/ / See the property declarations for descriptions .
_weakProxy = ( id ) [ FLWeakProxy weakProxyForObject : self ] ;
/ / Register this instance in the weak table for memory notifications . The NSHashTable will clean up after itself when we ' re gone .
/ / Note that FLAnimatedImages can be created on any thread , so the hash table must be locked .
@ synchronized ( allAnimatedImagesWeak ) {
[ allAnimatedImagesWeak addObject : self ] ;
}
}
return self ;
}
+ ( instancetype ) animatedImageWithGIFData : ( NSData * ) data
{
FLAnimatedImage * animatedImage = [ [ FLAnimatedImage alloc ] initWithAnimatedGIFData : data ] ;
return animatedImage ;
}
- ( void ) dealloc
{
if ( _weakProxy ) {
[ NSObject cancelPreviousPerformRequestsWithTarget : _weakProxy ] ;
}
if ( _imageSource ) {
CFRelease ( _imageSource ) ;
}
}
#pragma mark - Public Methods
/ / See header for more details .
/ / Note : both consumer and producer are throttled : consumer by frame timings and producer by the available memory ( max buffer window size ) .
- ( UIImage * ) imageLazilyCachedAtIndex : ( NSUInteger ) index
{
/ / Early return if the requested index is beyond bounds .
/ / Note : We ' re comparing an index with a count and need to bail on greater than or equal to .
if ( index > = self . frameCount ) {
FLLogWarn ( @ "Skipping requested frame %lu beyond bounds (total frame count: %lu) for animated image: %@", (unsigned long)index, (unsigned long)self.frameCount, self);
return nil ;
}
/ / Remember requested frame index , this influences what we should cache next .
self . requestedFrameIndex = index ;
#if defined ( DEBUG ) && DEBUG
if ( [ self . debug_delegate respondsToSelector : @ selector ( debug_animatedImage : didRequestCachedFrame : ) ] ) {
[ self . debug_delegate debug_animatedImage : self didRequestCachedFrame : index ] ;
}
#endif
/ / Quick check to avoid doing any work if we already have all possible frames cached , a common case .
if ( [ self . cachedFrameIndexes count ] < self . frameCount ) {
/ / If we have frames that should be cached but aren ' t and aren ' t requested yet , request them .
/ / Exclude existing cached frames , frames already requested , and specially cached poster image .
NSMutableIndexSet * frameIndexesToAddToCacheMutable = [ [ self frameIndexesToCache ] mutableCopy ] ;
[ frameIndexesToAddToCacheMutable removeIndexes : self . cachedFrameIndexes ] ;
[ frameIndexesToAddToCacheMutable removeIndexes : self . requestedFrameIndexes ] ;
[ frameIndexesToAddToCacheMutable removeIndex : self . posterImageFrameIndex ] ;
NSIndexSet * frameIndexesToAddToCache = [ frameIndexesToAddToCacheMutable copy ] ;
/ / Asynchronously add frames to our cache .
if ( [ frameIndexesToAddToCache count ] > 0 ) {
[ self addFrameIndexesToCache : frameIndexesToAddToCache ] ;
}
}
/ / Get the specified image .
UIImage * image = self . cachedFramesForIndexes [ @ ( index ) ] ;
/ / Purge if needed based on the current playhead position .
[ self purgeFrameCacheIfNeeded ] ;
return image ;
}
/ / Only called once from `- imageLazilyCachedAtIndex ` but factored into its own method for logical grouping .
- ( void ) addFrameIndexesToCache : ( NSIndexSet * ) frameIndexesToAddToCache
{
/ / Order matters . First , iterate over the indexes starting from the requested frame index .
/ / Then , if there are any indexes before the requested frame index , do those .
NSRange firstRange = NSMakeRange ( self . requestedFrameIndex , self . frameCount - self . requestedFrameIndex ) ;
NSRange secondRange = NSMakeRange ( 0 , self . requestedFrameIndex ) ;
if ( firstRange . length + secondRange . length != self . frameCount ) {
FLLogWarn ( @ "Two - part frame cache range doesn ' t equal full range . ") ;
}
/ / Add to the requested list before we actually kick them off , so they don ' t get into the queue twice .
[ self . requestedFrameIndexes addIndexes : frameIndexesToAddToCache ] ;
/ / Lazily create dedicated isolation queue .
if ( !self . serialQueue ) {
_serialQueue = dispatch_queue_create ( "com . flipboard . framecachingqueue ", DISPATCH_QUEUE_SERIAL ) ;
}
/ / Start streaming requested frames in the background into the cache .
/ / Avoid capturing self in the block as there ' s no reason to keep doing work if the animated image went away .
FLAnimatedImage * __weak weakSelf = self ;
dispatch_async ( self . serialQueue , ^{
/ / Produce and cache next needed frame .
void ( ^frameRangeBlock ) ( NSRange , BOOL * ) = ^( NSRange range , BOOL * stop ) {
/ / Iterate through contiguous indexes ; can be faster than `enumerateIndexesInRange : options : usingBlock : `.
for ( NSUInteger i = range . location ; i < NSMaxRange ( range ) ; i + + ) {
#if defined ( DEBUG ) && DEBUG
CFTimeInterval predrawBeginTime = CACurrentMediaTime ( ) ;
#endif
UIImage * image = [ weakSelf predrawnImageAtIndex : i ] ;
#if defined ( DEBUG ) && DEBUG
CFTimeInterval predrawDuration = CACurrentMediaTime ( ) - predrawBeginTime ;
CFTimeInterval slowdownDuration = 0.0 ;
if ( [ self . debug_delegate respondsToSelector : @ selector ( debug_animatedImagePredrawingSlowdownFactor : ) ] ) {
CGFloat predrawingSlowdownFactor = [ self . debug_delegate debug_animatedImagePredrawingSlowdownFactor : self ] ;
slowdownDuration = predrawDuration * predrawingSlowdownFactor - predrawDuration ;
[ NSThread sleepForTimeInterval : slowdownDuration ] ;
}
FLLogVerbose ( @ "Predrew frame %lu in %f ms for animated image: %@", (unsigned long)i, (predrawDuration + slowdownDuration) * 1000, self);
#endif
/ / The results get returned one by one as soon as they ' re ready ( and not in batch ) .
/ / The benefits of having the first frames as quick as possible outweigh building up a buffer to cope with potential hiccups when the CPU suddenly gets busy .
if ( image && weakSelf ) {
dispatch_async ( dispatch_get_main_queue ( ) , ^{
weakSelf . cachedFramesForIndexes [ @ ( i ) ] = image ;
[ weakSelf . cachedFrameIndexes addIndex : i ] ;
[ weakSelf . requestedFrameIndexes removeIndex : i ] ;
#if defined ( DEBUG ) && DEBUG
if ( [ weakSelf . debug_delegate respondsToSelector : @ selector ( debug_animatedImage : didUpdateCachedFrames : ) ] ) {
[ weakSelf . debug_delegate debug_animatedImage : weakSelf didUpdateCachedFrames : weakSelf . cachedFrameIndexes ] ;
}
#endif
} ) ;
}
}
} ;
[ frameIndexesToAddToCache enumerateRangesInRange : firstRange options : 0 usingBlock : frameRangeBlock ] ;
[ frameIndexesToAddToCache enumerateRangesInRange : secondRange options : 0 usingBlock : frameRangeBlock ] ;
} ) ;
}
+ ( CGSize ) sizeForImage : ( id ) image
{
CGSize imageSize = CGSizeZero ;
/ / Early return for nil
if ( !image ) {
return imageSize ;
}
if ( [ image isKindOfClass : [ UIImage class ] ] ) {
UIImage * uiImage = ( UIImage * ) image ;
imageSize = uiImage . size ;
} else if ( [ image isKindOfClass : [ FLAnimatedImage class ] ] ) {
FLAnimatedImage * animatedImage = ( FLAnimatedImage * ) image ;
imageSize = animatedImage . size ;
} else {
/ / Bear trap to capture bad images ; we have seen crashers cropping up on iOS 7.
FLLogError ( @ "`image ` isn ' t of expected types `UIImage ` or `FLAnimatedImage `: %@", image);
}
return imageSize ;
}
#pragma mark - Private Methods
#pragma mark Frame Loading
- ( UIImage * ) predrawnImageAtIndex : ( NSUInteger ) index
{
/ / It ' s very important to use the cached `_imageSource ` since the random access to a frame with `CGImageSourceCreateImageAtIndex ` turns from an O ( 1 ) into an O ( n ) operation when re - initializing the image source every time .
CGImageRef imageRef = CGImageSourceCreateImageAtIndex ( _imageSource , index , NULL ) ;
UIImage * image = [ UIImage imageWithCGImage : imageRef ] ;
CFRelease ( imageRef ) ;
/ / Loading in the image object is only half the work , the displaying image view would still have to synchronosly wait and decode the image , so we go ahead and do that here on the background thread .
image = [ [ self class ] predrawnImageFromImage : image ] ;
return image ;
}
#pragma mark Frame Caching
- ( NSIndexSet * ) frameIndexesToCache
{
NSIndexSet * indexesToCache = nil ;
/ / Quick check to avoid building the index set if the number of frames to cache equals the total frame count .
if ( self . frameCacheSizeCurrent == self . frameCount ) {
indexesToCache = self . allFramesIndexSet ;
} else {
NSMutableIndexSet * indexesToCacheMutable = [ [ NSMutableIndexSet alloc ] init ] ;
/ / Add indexes to the set in two separate blocks - the first starting from the requested frame index , up to the limit or the end .
/ / The second , if needed , the remaining number of frames beginning at index zero .
NSUInteger firstLength = MIN ( self . frameCacheSizeCurrent , self . frameCount - self . requestedFrameIndex ) ;
NSRange firstRange = NSMakeRange ( self . requestedFrameIndex , firstLength ) ;
[ indexesToCacheMutable addIndexesInRange : firstRange ] ;
NSUInteger secondLength = self . frameCacheSizeCurrent - firstLength ;
if ( secondLength > 0 ) {
NSRange secondRange = NSMakeRange ( 0 , secondLength ) ;
[ indexesToCacheMutable addIndexesInRange : secondRange ] ;
}
/ / Double check our math , before we add the poster image index which may increase it by one .
if ( [ indexesToCacheMutable count ] != self . frameCacheSizeCurrent ) {
FLLogWarn ( @ "Number of frames to cache doesn ' t equal expected cache size . ") ;
}
[ indexesToCacheMutable addIndex : self . posterImageFrameIndex ] ;
indexesToCache = [ indexesToCacheMutable copy ] ;
}
return indexesToCache ;
}
- ( void ) purgeFrameCacheIfNeeded
{
/ / Purge frames that are currently cached but don ' t need to be .
/ / But not if we ' re still under the number of frames to cache .
/ / This way , if all frames are allowed to be cached ( the common case ) , we can skip all the `NSIndexSet ` math below .
if ( [ self . cachedFrameIndexes count ] > self . frameCacheSizeCurrent ) {
NSMutableIndexSet * indexesToPurge = [ self . cachedFrameIndexes mutableCopy ] ;
[ indexesToPurge removeIndexes : [ self frameIndexesToCache ] ] ;
[ indexesToPurge enumerateRangesUsingBlock : ^( NSRange range , BOOL * stop ) {
/ / Iterate through contiguous indexes ; can be faster than `enumerateIndexesInRange : options : usingBlock : `.
for ( NSUInteger i = range . location ; i < NSMaxRange ( range ) ; i + + ) {
[ self . cachedFrameIndexes removeIndex : i ] ;
[ self . cachedFramesForIndexes removeObjectForKey : @ ( i ) ] ;
/ / Note : Don ' t `CGImageSourceRemoveCacheAtIndex ` on the image source for frames that we don ' t want cached any longer to maintain O ( 1 ) time access .
#if defined ( DEBUG ) && DEBUG
if ( [ self . debug_delegate respondsToSelector : @ selector ( debug_animatedImage : didUpdateCachedFrames : ) ] ) {
dispatch_async ( dispatch_get_main_queue ( ) , ^{
[ self . debug_delegate debug_animatedImage : self didUpdateCachedFrames : self . cachedFrameIndexes ] ;
} ) ;
}
#endif
}
} ] ;
}
}
- ( void ) growFrameCacheSizeAfterMemoryWarning : ( NSNumber * ) frameCacheSize
{
self . frameCacheSizeMaxInternal = [ frameCacheSize unsignedIntegerValue ] ;
FLLogDebug ( @ "Grew frame cache size max to %lu after memory warning for animated image: %@", (unsigned long)self.frameCacheSizeMaxInternal, self);
/ / Schedule resetting the frame cache size max completely after a while .
const NSTimeInterval kResetDelay = 3.0 ;
[ self . weakProxy performSelector : @ selector ( resetFrameCacheSizeMaxInternal ) withObject : nil afterDelay : kResetDelay ] ;
}
- ( void ) resetFrameCacheSizeMaxInternal
{
self . frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeNoLimit ;
FLLogDebug ( @ "Reset frame cache size max ( current frame cache size : %lu) for animated image: %@", (unsigned long)self.frameCacheSizeCurrent, self);
}
#pragma mark System Memory Warnings Notification Handler
- ( void ) didReceiveMemoryWarning : ( NSNotification * ) notification
{
self . memoryWarningCount + + ;
/ / If we were about to grow larger , but got rapped on our knuckles by the system again , cancel .
[ NSObject cancelPreviousPerformRequestsWithTarget : self . weakProxy selector : @ selector ( growFrameCacheSizeAfterMemoryWarning : ) object : @ ( FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning ) ] ;
[ NSObject cancelPreviousPerformRequestsWithTarget : self . weakProxy selector : @ selector ( resetFrameCacheSizeMaxInternal ) object : nil ] ;
/ / Go down to the minimum and by that implicitly immediately purge from the cache if needed to not get jettisoned by the system and start producing frames on - demand .
FLLogDebug ( @ "Attempt setting frame cache size max to %lu (previous was %lu) after memory warning #%lu for animated image: %@", (unsigned long)FLAnimatedImageFrameCacheSizeLowMemory, (unsigned long)self.frameCacheSizeMaxInternal, (unsigned long)self.memoryWarningCount, self);
self . frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeLowMemory ;
/ / Schedule growing larger again after a while , but cap our attempts to prevent a periodic sawtooth wave ( ramps upward and then sharply drops ) of memory usage .
/ /
/ / [ mem ] ^ ( 2 ) ( 5 ) ( 6 ) 1 ) Loading frames for the first time
/ / ( * ) | , , , 2 ) Mem warning #1 ; purge cache
/ / | / | ( 4 ) / | / | 3 ) Grow cache size a bit after a while , if no mem warning occurs
/ / | / | _ / | _ / | 4 ) Try to grow cache size back to optimum after a while , if no mem warning occurs
/ / | ( 1 ) / | _ / | / | __ ( 7 ) 5 ) Mem warning #2 ; purge cache
/ / | __ / ( 3 ) 6 ) After repetition of ( 3 ) and ( 4 ) , mem warning #3 ; purge cache
/ / + - - - - - - - - - - - - - - - - - - - - - - > 7 ) After 3 mem warnings , stay at minimum cache size
/ / [ t ]
/ / * ) The mem high water mark before we get warned might change for every cycle .
/ /
const NSUInteger kGrowAttemptsMax = 2 ;
const NSTimeInterval kGrowDelay = 2.0 ;
if ( ( self . memoryWarningCount - 1 ) < = kGrowAttemptsMax ) {
[ self . weakProxy performSelector : @ selector ( growFrameCacheSizeAfterMemoryWarning : ) withObject : @ ( FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning ) afterDelay : kGrowDelay ] ;
}
/ / Note : It ' s not possible to get the level of a memory warning with a public API : http : / / stackoverflow . com / questions / 2915247 / iphone - os - memory - warnings - what - do - the - different - levels - mean / 2915477 #2915477
}
#pragma mark Image Decoding
/ / Decodes the image ' s data and draws it off - screen fully in memory ; it ' s thread - safe and hence can be called on a background thread .
/ / On success , the returned object is a new `UIImage ` instance with the same content as the one passed in .
/ / On failure , the returned object is the unchanged passed in one ; the data will not be predrawn in memory though and an error will be logged .
/ / First inspired by & good Karma to : https : / / gist . github . com / steipete / 1144242
+ ( UIImage * ) predrawnImageFromImage : ( UIImage * ) imageToPredraw
{
/ / Always use a device RGB color space for simplicity and predictability what will be going on .
CGColorSpaceRef colorSpaceDeviceRGBRef = CGColorSpaceCreateDeviceRGB ( ) ;
/ / Early return on failure !
if ( !colorSpaceDeviceRGBRef ) {
FLLogError ( @ "Failed to `CGColorSpaceCreateDeviceRGB ` for image %@", imageToPredraw);
return imageToPredraw ;
}
/ / Even when the image doesn ' t have transparency , we have to add the extra channel because Quartz doesn ' t support other pixel formats than 32 bpp / 8 bpc for RGB :
/ / kCGImageAlphaNoneSkipFirst , kCGImageAlphaNoneSkipLast , kCGImageAlphaPremultipliedFirst , kCGImageAlphaPremultipliedLast
/ / ( source : docs "Quartz 2 D Programming Guide > Graphics Contexts > Table 2 - 1 Pixel formats supported for bitmap graphics contexts ")
size_t numberOfComponents = CGColorSpaceGetNumberOfComponents ( colorSpaceDeviceRGBRef ) + 1 ; / / 4 : RGB + A
/ / "In iOS 4.0 and later , and OS X v10 . 6 and later , you can pass NULL if you want Quartz to allocate memory for the bitmap . " ( source : docs )
void * data = NULL ;
size_t width = ( size_t ) imageToPredraw . size . width ;
size_t height = ( size_t ) imageToPredraw . size . height ;
size_t bitsPerComponent = CHAR_BIT ;
size_t bitsPerPixel = ( bitsPerComponent * numberOfComponents ) ;
size_t bytesPerPixel = ( bitsPerPixel / BYTE_SIZE ) ;
size_t bytesPerRow = ( bytesPerPixel * width ) ;
CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault ;
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo ( imageToPredraw . CGImage ) ;
/ / If the alpha info doesn ' t match to one of the supported formats ( see above ) , pick a reasonable supported one .
/ / "For bitmaps created in iOS 3.2 and later , the drawing environment uses the premultiplied ARGB format to store the bitmap data . " ( source : docs )
if ( alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaOnly ) {
alphaInfo = kCGImageAlphaNoneSkipFirst ;
} else if ( alphaInfo == kCGImageAlphaFirst ) {
alphaInfo = kCGImageAlphaPremultipliedFirst ;
} else if ( alphaInfo == kCGImageAlphaLast ) {
alphaInfo = kCGImageAlphaPremultipliedLast ;
}
/ / "The constants for specifying the alpha channel information are declared with the `CGImageAlphaInfo ` type but can be passed to this parameter safely . " ( source : docs )
bitmapInfo | = alphaInfo ;
/ / Create our own graphics context to draw to ; `UIGraphicsGetCurrentContext `/ `UIGraphicsBeginImageContextWithOptions ` doesn ' t create a new context but returns the current one which isn ' t thread - safe ( e . g . main thread could use it at the same time ) .
/ / Note : It ' s not worth caching the bitmap context for multiple frames ( "unique key " would be `width `, `height ` and `hasAlpha `) , it ' s ~ 50 % slower. Time spent in libRIP's `CGSBlendBGRA8888toARGB8888` suddenly shoots up -- not sure why.
CGContextRef bitmapContextRef = CGBitmapContextCreate ( data , width , height , bitsPerComponent , bytesPerRow , colorSpaceDeviceRGBRef , bitmapInfo ) ;
CGColorSpaceRelease ( colorSpaceDeviceRGBRef ) ;
/ / Early return on failure !
if ( !bitmapContextRef ) {
FLLogError ( @ "Failed to `CGBitmapContextCreate ` with color space %@ and parameters (width: %zu height: %zu bitsPerComponent: %zu bytesPerRow: %zu) for image %@", colorSpaceDeviceRGBRef, width, height, bitsPerComponent, bytesPerRow, imageToPredraw);
return imageToPredraw ;
}
/ / Draw image in bitmap context and create image by preserving receiver ' s properties .
CGContextDrawImage ( bitmapContextRef , CGRectMake ( 0.0 , 0.0 , imageToPredraw . size . width , imageToPredraw . size . height ) , imageToPredraw . CGImage ) ;
CGImageRef predrawnImageRef = CGBitmapContextCreateImage ( bitmapContextRef ) ;
UIImage * predrawnImage = [ UIImage imageWithCGImage : predrawnImageRef scale : imageToPredraw . scale orientation : imageToPredraw . imageOrientation ] ;
CGImageRelease ( predrawnImageRef ) ;
CGContextRelease ( bitmapContextRef ) ;
/ / Early return on failure !
if ( !predrawnImage ) {
FLLogError ( @ "Failed to `imageWithCGImage : scale : orientation : ` with image ref %@ created with color space %@ and bitmap context %@ and properties and properties (scale: %f orientation: %ld) for image %@", predrawnImageRef, colorSpaceDeviceRGBRef, bitmapContextRef, imageToPredraw.scale, (long)imageToPredraw.imageOrientation, imageToPredraw);
return imageToPredraw ;
}
return predrawnImage ;
}
#pragma mark - Description
- ( NSString * ) description
{
NSString * description = [ super description ] ;
description = [ description stringByAppendingFormat : @ " size = %@", NSStringFromCGSize(self.size)];
description = [ description stringByAppendingFormat : @ " frameCount = %lu", (unsigned long)self.frameCount];
return description ;
}
@ end
#pragma mark - FLWeakProxy
@ interface FLWeakProxy ( )
@ property ( nonatomic , weak ) id target ;
@ end
@ implementation FLWeakProxy
#pragma mark Life Cycle
/ / This is the designated creation method of an `FLWeakProxy ` and
/ / as a subclass of `NSProxy ` it doesn ' t respond to or need `- init `.
+ ( instancetype ) weakProxyForObject : ( id ) targetObject
{
FLWeakProxy * weakProxy = [ FLWeakProxy alloc ] ;
weakProxy . target = targetObject ;
return weakProxy ;
}
#pragma mark Forwarding Messages
- ( id ) forwardingTargetForSelector : ( SEL ) selector
{
/ / Keep it lightweight : access the ivar directly
return _target ;
}
#pragma mark - NSWeakProxy Method Overrides
#pragma mark Handling Unimplemented Methods
- ( void ) forwardInvocation : ( NSInvocation * ) invocation
{
/ / Fallback for when target is nil . Don ' t do anything , just return 0 / NULL / nil .
/ / The method signature we ' ve received to get here is just a dummy to keep `doesNotRecognizeSelector : ` from firing .
/ / We can ' t really handle struct return types here because we don ' t know the length .
void * nullPointer = NULL ;
[ invocation setReturnValue : & nullPointer ] ;
}
- ( NSMethodSignature * ) methodSignatureForSelector : ( SEL ) selector
{
/ / We only get here if `forwardingTargetForSelector : ` returns nil .
/ / In that case , our weak target has been reclaimed . Return a dummy method signature to keep `doesNotRecognizeSelector : ` from firing .
/ / We ' ll emulate the Obj - c messaging nil behavior by setting the return value to nil in `forwardInvocation : `, but we ' ll assume that the return value is `sizeof ( void * ) `.
/ / Other libraries handle this situation by making use of a global method signature cache , but that seems heavier than necessary and has issues as well .
/ / See https : / / www . mikeash . com / pyblog / friday - qa - 2010 - 02 - 26 - futures . html and https : / / github . com / steipete / PSTDelegateProxy / issues / 1 for examples of using a method signature cache .
return [ NSObject instanceMethodSignatureForSelector : @ selector ( init ) ] ;
}
@ end