diff --git a/Examples/SDWebImage Demo.xcodeproj/project.pbxproj b/Examples/SDWebImage Demo.xcodeproj/project.pbxproj index ddf8a658..8b6ceb18 100644 --- a/Examples/SDWebImage Demo.xcodeproj/project.pbxproj +++ b/Examples/SDWebImage Demo.xcodeproj/project.pbxproj @@ -3,12 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ 32892E311FAE898C00BE8320 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 32892E301FAE898C00BE8320 /* Assets.xcassets */; }; 32892E351FAE89FE00BE8320 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 32892E341FAE89FD00BE8320 /* LaunchScreen.storyboard */; }; + 328CA3AB29980A840063950F /* WindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 328CA3AA29980A840063950F /* WindowController.m */; }; 3E75A9861742DBE700DA412D /* CustomPathImages in Resources */ = {isa = PBXBuildFile; fileRef = 3E75A9851742DBE700DA412D /* CustomPathImages */; }; 4314D1AA1D0E1181004B36C9 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4314D1A91D0E1181004B36C9 /* main.m */; }; 4314D1AD1D0E1181004B36C9 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4314D1AC1D0E1181004B36C9 /* AppDelegate.m */; }; @@ -136,6 +137,8 @@ 327E1C604113A7CEC9AC02DB /* Pods-SDWebImage Watch Demo Extension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SDWebImage Watch Demo Extension.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-SDWebImage Watch Demo Extension/Pods-SDWebImage Watch Demo Extension.debug.xcconfig"; sourceTree = ""; }; 32892E301FAE898C00BE8320 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 32892E341FAE89FD00BE8320 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + 328CA3A929980A840063950F /* WindowController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WindowController.h; sourceTree = ""; }; + 328CA3AA29980A840063950F /* WindowController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WindowController.m; sourceTree = ""; }; 3E75A9851742DBE700DA412D /* CustomPathImages */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CustomPathImages; sourceTree = SOURCE_ROOT; }; 4314D1A61D0E1181004B36C9 /* SDWebImage TV Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SDWebImage TV Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 4314D1A91D0E1181004B36C9 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; @@ -275,6 +278,8 @@ children = ( 43A629D11D0DFD000089D7DD /* AppDelegate.h */, 43A629D21D0DFD000089D7DD /* AppDelegate.m */, + 328CA3A929980A840063950F /* WindowController.h */, + 328CA3AA29980A840063950F /* WindowController.m */, 43A629D71D0DFD000089D7DD /* ViewController.h */, 43A629D81D0DFD000089D7DD /* ViewController.m */, 43A629DA1D0DFD000089D7DD /* Assets.xcassets */, @@ -804,6 +809,7 @@ buildActionMask = 2147483647; files = ( 43A629D91D0DFD000089D7DD /* ViewController.m in Sources */, + 328CA3AB29980A840063950F /* WindowController.m in Sources */, 43A629D61D0DFD000089D7DD /* main.m in Sources */, 43A629D31D0DFD000089D7DD /* AppDelegate.m in Sources */, ); diff --git a/Examples/SDWebImage OSX Demo/AppDelegate.h b/Examples/SDWebImage OSX Demo/AppDelegate.h index a9a60529..99adbba0 100644 --- a/Examples/SDWebImage OSX Demo/AppDelegate.h +++ b/Examples/SDWebImage OSX Demo/AppDelegate.h @@ -10,6 +10,7 @@ @interface AppDelegate : NSObject +@property (strong, nonatomic) NSWindowController *windowController; @end diff --git a/Examples/SDWebImage OSX Demo/AppDelegate.m b/Examples/SDWebImage OSX Demo/AppDelegate.m index f1c7e02b..9a7294fa 100644 --- a/Examples/SDWebImage OSX Demo/AppDelegate.m +++ b/Examples/SDWebImage OSX Demo/AppDelegate.m @@ -30,6 +30,12 @@ // For HEIC animated image. Animated image is new introduced in iOS 13, but it contains performance issue for now. [[SDImageCodersManager sharedManager] addCoder:[SDImageHEICCoder sharedCoder]]; } + + NSStoryboard *mainStoryboard = [NSStoryboard storyboardWithName:@"Main" bundle:nil]; + NSWindowController *initialController = [mainStoryboard instantiateControllerWithIdentifier:@"MainWindowController"]; + self.windowController = initialController; + [initialController showWindow:self]; + [initialController.window makeKeyAndOrderFront:self]; } - (void)applicationWillTerminate:(NSNotification *)aNotification { diff --git a/Examples/SDWebImage OSX Demo/Base.lproj/Main.storyboard b/Examples/SDWebImage OSX Demo/Base.lproj/Main.storyboard index 8986ad91..0384fdaa 100644 --- a/Examples/SDWebImage OSX Demo/Base.lproj/Main.storyboard +++ b/Examples/SDWebImage OSX Demo/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -651,7 +651,7 @@ - + diff --git a/Examples/SDWebImage OSX Demo/WindowController.h b/Examples/SDWebImage OSX Demo/WindowController.h new file mode 100644 index 00000000..86e69bb9 --- /dev/null +++ b/Examples/SDWebImage OSX Demo/WindowController.h @@ -0,0 +1,17 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface WindowController : NSWindowController + +@end + +NS_ASSUME_NONNULL_END diff --git a/Examples/SDWebImage OSX Demo/WindowController.m b/Examples/SDWebImage OSX Demo/WindowController.m new file mode 100644 index 00000000..51c9b852 --- /dev/null +++ b/Examples/SDWebImage OSX Demo/WindowController.m @@ -0,0 +1,13 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#import "WindowController.h" + +@implementation WindowController + +@end diff --git a/SDWebImage/Core/SDWebImageDownloader.m b/SDWebImage/Core/SDWebImageDownloader.m index ed866cf7..6e9fe062 100644 --- a/SDWebImage/Core/SDWebImageDownloader.m +++ b/SDWebImage/Core/SDWebImageDownloader.m @@ -26,7 +26,7 @@ static void * SDWebImageDownloaderOperationKey = &SDWebImageDownloaderOperationK BOOL SDWebImageDownloaderOperationGetCompleted(id operation) { NSCParameterAssert(operation); NSNumber *value = objc_getAssociatedObject(operation, SDWebImageDownloaderOperationKey); - if (value) { + if (value != nil) { return value.boolValue; } else { return NO; diff --git a/SDWebImage/Private/SDDisplayLink.h b/SDWebImage/Private/SDDisplayLink.h index 3ee8c6fd..6582ccbf 100644 --- a/SDWebImage/Private/SDDisplayLink.h +++ b/SDWebImage/Private/SDDisplayLink.h @@ -15,7 +15,7 @@ @property (readonly, nonatomic, weak, nullable) id target; @property (readonly, nonatomic, assign, nonnull) SEL selector; -@property (readonly, nonatomic) CFTimeInterval duration; +@property (readonly, nonatomic) NSTimeInterval duration; // elapsed time in seconds of previous callback. (or it's first callback, use the time between `start` and callback). Always zero when display link not running @property (readonly, nonatomic) BOOL isRunning; + (nonnull instancetype)displayLinkWithTarget:(nonnull id)target selector:(nonnull SEL)sel; diff --git a/SDWebImage/Private/SDDisplayLink.m b/SDWebImage/Private/SDDisplayLink.m index 1dcfd99f..c88c0fbf 100644 --- a/SDWebImage/Private/SDDisplayLink.m +++ b/SDWebImage/Private/SDDisplayLink.m @@ -13,15 +13,31 @@ #elif SD_IOS || SD_TV #import #endif +#include #if SD_MAC static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext); #endif +#if SD_WATCH +static CFTimeInterval CACurrentMediaTime(void) +{ + mach_timebase_info_data_t timebase; + mach_timebase_info(&timebase); + + uint64_t time = mach_absolute_time(); + double seconds = (double)time * (double)timebase.numer / (double)timebase.denom / 1e9; + return seconds; +} +#endif + #define kSDDisplayLinkInterval 1.0 / 60 @interface SDDisplayLink () +@property (nonatomic, assign) NSTimeInterval previousFireTime; +@property (nonatomic, assign) NSTimeInterval nextFireTime; + #if SD_MAC @property (nonatomic, assign) CVDisplayLinkRef displayLink; @property (nonatomic, assign) CVTimeStamp outputTime; @@ -32,7 +48,6 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt @property (nonatomic, strong) NSTimer *displayLink; @property (nonatomic, strong) NSRunLoop *runloop; @property (nonatomic, copy) NSRunLoopMode runloopMode; -@property (nonatomic, assign) NSTimeInterval currentFireDate; #endif @end @@ -78,33 +93,47 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt return displayLink; } -- (CFTimeInterval)duration { +- (NSTimeInterval)duration { + NSTimeInterval duration = 0; #if SD_MAC CVTimeStamp outputTime = self.outputTime; - NSTimeInterval duration = 0; double periodPerSecond = (double)outputTime.videoTimeScale * outputTime.rateScalar; if (periodPerSecond > 0) { duration = (double)outputTime.videoRefreshPeriod / periodPerSecond; } -#elif SD_IOS || SD_TV -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - NSTimeInterval duration = 0; - if (@available(iOS 10.0, tvOS 10.0, *)) { - duration = self.displayLink.targetTimestamp - CACurrentMediaTime(); - } else { - duration = self.displayLink.duration * self.displayLink.frameInterval; - } -#pragma clang diagnostic pop #else - NSTimeInterval duration = 0; - if (self.displayLink.isValid && self.currentFireDate != 0) { - NSTimeInterval nextFireDate = CFRunLoopTimerGetNextFireDate((__bridge CFRunLoopTimerRef)self.displayLink); - duration = nextFireDate - self.currentFireDate; + // iOS 10+/watchOS use `nextTime` + if (@available(iOS 10.0, tvOS 10.0, watchOS 2.0, *)) { + duration = self.nextFireTime - CACurrentMediaTime(); + } else { + // iOS 9 use `previousTime` + duration = CACurrentMediaTime() - self.previousFireTime; } #endif - if (duration <= 0) { + // When system sleep, the targetTimestamp will mass up, fallback refresh rate + if (duration < 0) { +#if SD_MAC + // Supports Pro display 120Hz + CGDirectDisplayID display = CVDisplayLinkGetCurrentCGDisplay(_displayLink); + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display); + if (mode) { + double refreshRate = CGDisplayModeGetRefreshRate(mode); + if (refreshRate > 0) { + duration = 1.0 / refreshRate; + } else { + duration = kSDDisplayLinkInterval; + } + CGDisplayModeRelease(mode); + } else { + duration = kSDDisplayLinkInterval; + } +#elif SD_IOS || SD_TV + // Fallback + duration = self.displayLink.duration; +#else + // Watch always 60Hz duration = kSDDisplayLinkInterval; +#endif } return duration; } @@ -189,24 +218,25 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt #else [self.displayLink invalidate]; #endif + self.previousFireTime = 0; + self.nextFireTime = 0; } - (void)displayLinkDidRefresh:(id)displayLink { -#if SD_MAC - // CVDisplayLink does not use runloop, but we can provide similar behavior for modes - // May use `default` runloop to avoid extra callback when in `eventTracking` (mouse drag, scroll) or `modalPanel` (modal panel) - NSString *runloopMode = self.runloopMode; - if (![runloopMode isEqualToString:NSRunLoopCommonModes] && ![runloopMode isEqualToString:NSRunLoop.mainRunLoop.currentMode]) { - return; +#if SD_IOS || SD_TV + if (@available(iOS 10.0, tvOS 10.0, *)) { + self.nextFireTime = self.displayLink.targetTimestamp; + } else { + self.previousFireTime = self.displayLink.timestamp; } #endif +#if SD_WATCH + self.nextFireTime = CFRunLoopTimerGetNextFireDate((__bridge CFRunLoopTimerRef)self.displayLink); +#endif #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [_target performSelector:_selector withObject:self]; #pragma clang diagnostic pop -#if SD_WATCH - self.currentFireDate = CFRunLoopTimerGetNextFireDate((__bridge CFRunLoopTimerRef)self.displayLink); -#endif } @end @@ -215,11 +245,16 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) { // CVDisplayLink callback is not on main queue SDDisplayLink *object = (__bridge SDDisplayLink *)displayLinkContext; - if (inOutputTime) { - object.outputTime = *inOutputTime; + // CVDisplayLink does not use runloop, but we can provide similar behavior for modes + // May use `default` runloop to avoid extra callback when in `eventTracking` (mouse drag, scroll) or `modalPanel` (modal panel) + NSString *runloopMode = object.runloopMode; + if (![runloopMode isEqualToString:NSRunLoopCommonModes] && ![runloopMode isEqualToString:NSRunLoop.mainRunLoop.currentMode]) { + return kCVReturnSuccess; } + CVTimeStamp outputTime = inOutputTime ? *inOutputTime : *inNow; __weak SDDisplayLink *weakObject = object; dispatch_async(dispatch_get_main_queue(), ^{ + weakObject.outputTime = outputTime; [weakObject displayLinkDidRefresh:(__bridge id)(displayLink)]; }); return kCVReturnSuccess; diff --git a/Tests/Tests/SDUtilsTests.m b/Tests/Tests/SDUtilsTests.m index fc238904..9935ea82 100644 --- a/Tests/Tests/SDUtilsTests.m +++ b/Tests/Tests/SDUtilsTests.m @@ -48,8 +48,6 @@ XCTestExpectation *expectation1 = [self expectationWithDescription:@"Display Link Stop"]; XCTestExpectation *expectation2 = [self expectationWithDescription:@"Display Link Start"]; SDDisplayLink *displayLink = [SDDisplayLink displayLinkWithTarget:self selector:@selector(displayLinkDidRefresh:)]; - NSTimeInterval duration = displayLink.duration; // Initial value - expect(duration).equal(1.0 / 60); [displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes]; [displayLink start]; expect(displayLink.isRunning).beTruthy();