Add support for imageNamed: in SDAnimatedImage with bundle files

This commit is contained in:
DreamPiggy 2018-02-22 19:28:07 +08:00
parent 7e83d78ca3
commit 5e09c6bf19
6 changed files with 246 additions and 15 deletions

View File

@ -66,7 +66,7 @@
@note Normally we use `initWithData:scale:` to create custom animated image class. So you can implement your custom class without our built-in coder.
@param animatedCoder An animated coder which conform `SDWebImageAnimatedCoder` protocol
@param scale The scale factor to assume when interpreting the image data. Applying a scale factor of 1.0 results in an image whose size matches the pixel-based dimensions of the image. Applying a different scale factor changes the size of the image as reported by the `size` property. (For `NSImage`, `scale` property can be calculated from `size`)
@param scale The scale factor to assume when interpreting the image data. Applying a scale factor of 1.0 results in an image whose size matches the pixel-based dimensions of the image. Applying a different scale factor changes the size of the image as reported by the `size` property.
@return An initialized object
*/
- (nullable instancetype)initWithAnimatedCoder:(nonnull id<SDWebImageAnimatedCoder>)animatedCoder scale:(CGFloat)scale;
@ -76,7 +76,11 @@
@interface SDAnimatedImage : UIImage <SDAnimatedImage>
// This class override these methods from UIImage(NSImage), and it supports NSSecureCoding.
// You should use these methods to create a new animated image. Use other methods will just call super instead.
// You should use these methods to create a new animated image. Use other methods just call super instead.
+ (nullable instancetype)imageNamed:(nonnull NSString *)name; // Cache in memory, no Asset Catalog support
#if __has_include(<UIKit/UITraitCollection.h>)
+ (nullable instancetype)imageNamed:(nonnull NSString *)name inBundle:(nullable NSBundle *)bundle compatibleWithTraitCollection:(nullable UITraitCollection *)traitCollection; // Cache in memory, no Asset Catalog support
#endif
+ (nullable instancetype)imageWithContentsOfFile:(nonnull NSString *)path;
+ (nullable instancetype)imageWithData:(nonnull NSData *)data;
+ (nullable instancetype)imageWithData:(nonnull NSData *)data scale:(CGFloat)scale;
@ -94,12 +98,13 @@
*/
@property (nonatomic, copy, readonly, nullable) NSData *animatedImageData;
#if SD_MAC
/**
For AppKit, `NSImage` can contains multiple image representations with different scales. However, this class does not do that from the design. We processs the scale like UIKit and store it as a extra information for correctlly rendering in `SDAnimatedImageView`.
The scale factor of the image.
@note For UIKit, this just call super instead.
@note For AppKit, `NSImage` can contains multiple image representations with different scales. However, this class does not do that from the design. We processs the scale like UIKit and store it as a extra information for correctlly rendering in `SDAnimatedImageView`.
*/
@property (nonatomic, readonly) CGFloat scale;
#endif
/**
Preload all frame image to memory. Then later request can directly return the frame for index without decoding.

View File

@ -13,6 +13,10 @@
#import "SDWebImageCodersManager.h"
#import "SDWebImageFrame.h"
#define LOCK(...) dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER); \
__VA_ARGS__; \
dispatch_semaphore_signal(self->_lock);
static CGFloat SDImageScaleFromPath(NSString *string) {
if (string.length == 0 || [string hasSuffix:@"/"]) return 1;
NSString *name = string.stringByDeletingPathExtension;
@ -28,6 +32,169 @@ static CGFloat SDImageScaleFromPath(NSString *string) {
return scale;
}
static NSArray *SDBundlePreferredScales() {
static NSArray *scales;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
#if SD_WATCH
CGFloat screenScale = [WKInterfaceDevice currentDevice].screenScale;
#elif SD_UIKIT
CGFloat screenScale = [UIScreen mainScreen].scale;
#elif SD_MAC
CGFloat screenScale = [NSScreen mainScreen].backingScaleFactor;
#endif
if (screenScale <= 1) {
scales = @[@1,@2,@3];
} else if (screenScale <= 2) {
scales = @[@2,@3,@1];
} else {
scales = @[@3,@2,@1];
}
});
return scales;
}
#pragma mark - UIImage cache for bundle
// Apple parse the Asset Catalog compiled file(`Assets.car`) by CoreUI.framework, however it's a private framework and there are no other ways to directly get the data. So we just process the normal bundle files :)
@interface SDImageAssetManager : NSObject {
dispatch_semaphore_t _lock;
}
@property (nonatomic, strong) NSMapTable<NSString *, UIImage *> *imageTable;
+ (instancetype)sharedAssetManager;
- (nullable NSString *)getPathForName:(nonnull NSString *)name bundle:(nonnull NSBundle *)bundle preferredScale:(CGFloat *)scale;
- (nullable UIImage *)imageForName:(nonnull NSString *)name;
- (void)storeImage:(nonnull UIImage *)image forName:(nonnull NSString *)name;
@end
@implementation SDImageAssetManager
+ (instancetype)sharedAssetManager {
static dispatch_once_t onceToken;
static SDImageAssetManager *assetManager;
dispatch_once(&onceToken, ^{
assetManager = [[SDImageAssetManager alloc] init];
});
return assetManager;
}
- (instancetype)init {
self = [super init];
if (self) {
NSPointerFunctionsOptions valueOptions;
#if SD_MAC
// Apple says that NSImage use a weak reference to value
valueOptions = NSPointerFunctionsWeakMemory;
#else
// Apple says that UIImage use a strong reference to value
valueOptions = NSPointerFunctionsStrongMemory;
#endif
_imageTable = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsCopyIn valueOptions:valueOptions];
_lock = dispatch_semaphore_create(1);
#if SD_UIKIT
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}
return self;
}
- (void)dealloc {
#if SD_UIKIT
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}
- (void)didReceiveMemoryWarning:(NSNotification *)notification {
LOCK({
[self.imageTable removeAllObjects];
});
}
- (NSString *)getPathForName:(NSString *)name bundle:(NSBundle *)bundle preferredScale:(CGFloat *)scale {
NSParameterAssert(name);
NSParameterAssert(bundle);
NSString *path;
if (name.length == 0) {
return path;
}
if ([name hasSuffix:@"/"]) {
return path;
}
NSString *extension = name.pathExtension;
if (extension.length == 0) {
// If no extension, follow Apple's doc, check PNG format
extension = @"png";
}
name = [name stringByDeletingPathExtension];
CGFloat providedScale = *scale;
NSArray *scales = SDBundlePreferredScales();
// Check if file name contains scale
for (size_t i = 0; i < scales.count; i++) {
NSNumber *scaleValue = scales[i];
if ([name hasSuffix:[NSString stringWithFormat:@"@%@x", scaleValue]]) {
path = [bundle pathForResource:name ofType:extension];
if (path) {
*scale = scaleValue.doubleValue; // override
return path;
}
}
}
// Search with provided scale first
if (providedScale != 0) {
NSString *scaledName = [name stringByAppendingFormat:@"@%@x", @(providedScale)];
path = [bundle pathForResource:scaledName ofType:extension];
if (path) {
return path;
}
}
// Search with preferred scale
for (size_t i = 0; i < scales.count; i++) {
NSNumber *scaleValue = scales[i];
if (scaleValue.doubleValue == providedScale) {
// Ignore provided scale
continue;
}
NSString *scaledName = [name stringByAppendingFormat:@"@%@x", scaleValue];
path = [bundle pathForResource:scaledName ofType:extension];
if (path) {
*scale = scaleValue.doubleValue; // override
return path;
}
}
// Search without scale
path = [bundle pathForResource:name ofType:extension];
return path;
}
- (UIImage *)imageForName:(NSString *)name {
NSParameterAssert(name);
UIImage *image;
LOCK({
image = [self.imageTable objectForKey:name];
});
return image;
}
- (void)storeImage:(UIImage *)image forName:(NSString *)name {
NSParameterAssert(image);
NSParameterAssert(name);
LOCK({
[self.imageTable setObject:image forKey:name];
});
}
@end
@interface SDAnimatedImage ()
@property (nonatomic, strong) id<SDWebImageAnimatedCoder> coder;
@ -38,6 +205,9 @@ static CGFloat SDImageScaleFromPath(NSString *string) {
@end
@implementation SDAnimatedImage
#if SD_UIKIT || SD_WATCH
@dynamic scale; // call super
#endif
#pragma mark - Dealloc & Memory warning
@ -55,6 +225,53 @@ static CGFloat SDImageScaleFromPath(NSString *string) {
}
#pragma mark - UIImage override method
+ (instancetype)imageNamed:(NSString *)name {
#if __has_include(<UIKit/UITraitCollection.h>)
return [self imageNamed:name inBundle:nil compatibleWithTraitCollection:nil];
#else
return [self imageNamed:name inBundle:nil scale:0];
#endif
}
#if __has_include(<UIKit/UITraitCollection.h>)
+ (instancetype)imageNamed:(NSString *)name inBundle:(NSBundle *)bundle compatibleWithTraitCollection:(UITraitCollection *)traitCollection {
if (!traitCollection) {
traitCollection = UIScreen.mainScreen.traitCollection;
}
CGFloat scale = traitCollection.displayScale;
return [self imageNamed:name inBundle:bundle scale:scale];
}
#endif
// 0 scale means automatically check
+ (instancetype)imageNamed:(NSString *)name inBundle:(NSBundle *)bundle scale:(CGFloat)scale {
if (!name) {
return nil;
}
if (!bundle) {
bundle = [NSBundle mainBundle];
}
SDImageAssetManager *assetManager = [SDImageAssetManager sharedAssetManager];
SDAnimatedImage *image = (SDAnimatedImage *)[assetManager imageForName:name];
if ([image isKindOfClass:[SDAnimatedImage class]]) {
return image;
}
NSString *path = [assetManager getPathForName:name bundle:bundle preferredScale:&scale];
if (!path) {
return image;
}
NSData *data = [NSData dataWithContentsOfFile:path];
if (!data) {
return image;
}
image = [[self alloc] initWithData:data scale:scale];
if (image) {
[assetManager storeImage:image forName:name];
}
return image;
}
+ (instancetype)imageWithContentsOfFile:(NSString *)path {
return [[self alloc] initWithContentsOfFile:path];
}

View File

@ -10,6 +10,8 @@
#if SD_UIKIT || SD_MAC
#import "SDAnimatedImage.h"
/**
A drop-in replacement for UIImageView/NSImageView, you can use this for animated image rendering.
Call `setImage:` with a `UIImage<SDAnimatedImage>` will start animated image rendering. Call with a UIImage(NSImage) will back to normal UIImageView(NSImageView) rendering

View File

@ -7,10 +7,11 @@
*/
#import "SDAnimatedImageView.h"
#if SD_UIKIT || SD_MAC
#import "UIImage+WebCache.h"
#import "NSImage+Additions.h"
#if SD_UIKIT || SD_MAC
#import "SDAnimatedImage.h"
#import <mach/mach.h>
#if SD_MAC

View File

@ -50,7 +50,7 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCustom
FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCustomTransformer;
/**
A Class object which the instance is a `UIImage/NSImage` subclass and adopt `SDAnimatedImage` protocol. And call `initWithData:scale:` to create the instance. If the instance create failed, fallback to normal `UIImage/NSImage`.
A Class object which the instance is a `UIImage/NSImage` subclass and adopt `SDAnimatedImage` protocol. We will call `initWithData:scale:` to create the instance (or `initWithAnimatedCoder:sclae` when using progressive download) . If the instance create failed, fallback to normal `UIImage/NSImage`.
This can be used to improve animated images rendering performance (especially memory usage on big animated images) with `SDAnimatedImageView` (Class).
*/
FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextAnimatedImageClass;

View File

@ -61,7 +61,13 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun
// enough, other can be test with InitWithData
}
- (void)test04AnimatedImagePreloadFrames {
- (void)test04AnimatedImageImageNamed {
SDAnimatedImage *image = [SDAnimatedImage imageNamed:@"TestImage.gif" inBundle:[NSBundle bundleForClass:[self class]] compatibleWithTraitCollection:nil];
expect(image).notTo.beNil();
expect([image.animatedImageData isEqualToData:[self testGIFData]]).beTruthy();
}
- (void)test05AnimatedImagePreloadFrames {
NSData *validData = [self testGIFData];
SDAnimatedImage *image = [SDAnimatedImage imageWithData:validData];
@ -76,7 +82,7 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun
expect(frame).notTo.beNil();
}
- (void)test05AnimatedImageViewSetImage {
- (void)test06AnimatedImageViewSetImage {
SDAnimatedImageView *imageView = [SDAnimatedImageView new];
UIImage *image = [UIImage imageWithData:[self testJPEGData]];
imageView.image = image;
@ -84,7 +90,7 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun
expect(imageView.currentFrame).beNil(); // current frame
}
- (void)test06AnimatedImageViewSetAnimatedImage {
- (void)test07AnimatedImageViewSetAnimatedImage {
SDAnimatedImageView *imageView = [SDAnimatedImageView new];
SDAnimatedImage *image = [SDAnimatedImage imageWithData:[self testAnimatedWebPData]];
imageView.image = image;
@ -92,7 +98,7 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun
expect(imageView.currentFrame).notTo.beNil(); // current frame
}
- (void)test07AnimatedImageViewRendering {
- (void)test08AnimatedImageViewRendering {
XCTestExpectation *expectation = [self expectationWithDescription:@"test SDAnimatedImageView rendering"];
SDAnimatedImageView *imageView = [[SDAnimatedImageView alloc] init];
[self.window addSubview:imageView];
@ -126,7 +132,7 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun
[self waitForExpectationsWithCommonTimeout];
}
- (void)test08AnimatedImageViewSetProgressiveAnimatedImage {
- (void)test09AnimatedImageViewSetProgressiveAnimatedImage {
NSData *gifData = [self testGIFData];
SDWebImageGIFCoder *progressiveCoder = [[SDWebImageGIFCoder alloc] initIncremental];
// simulate progressive decode, pass partial data
@ -153,7 +159,7 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun
expect(isProgressive).equal(NO);
}
- (void)test09AnimatedImageViewCategory {
- (void)test10AnimatedImageViewCategory {
XCTestExpectation *expectation = [self expectationWithDescription:@"test SDAnimatedImageView view category"];
SDAnimatedImageView *imageView = [SDAnimatedImageView new];
NSURL *testURL = [NSURL URLWithString:kTestWebPURL];
@ -166,7 +172,7 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun
[self waitForExpectationsWithCommonTimeout];
}
- (void)test10AnimatedImageViewCategoryProgressive {
- (void)test11AnimatedImageViewCategoryProgressive {
XCTestExpectation *expectation = [self expectationWithDescription:@"test SDAnimatedImageView view category"];
SDAnimatedImageView *imageView = [SDAnimatedImageView new];
NSURL *testURL = [NSURL URLWithString:kTestGIFURL];