2015-09-01 04:51:39 +02:00
//
// 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 2D 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 ;
2016-07-09 01:39:28 +02:00
size_t width = ( size_t ) imageToPredraw . size . width ;
size_t height = ( size_t ) imageToPredraw . size . height ;
2015-09-01 04:51:39 +02:00
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