SDDisplayLink.m 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. /*
  2. * This file is part of the SDWebImage package.
  3. * (c) Olivier Poitrey <rs@dailymotion.com>
  4. *
  5. * For the full copyright and license information, please view the LICENSE
  6. * file that was distributed with this source code.
  7. */
  8. #import "SDDisplayLink.h"
  9. #import "SDWeakProxy.h"
  10. #if SD_MAC
  11. #import <CoreVideo/CoreVideo.h>
  12. #elif SD_IOS || SD_TV
  13. #import <QuartzCore/QuartzCore.h>
  14. #endif
  15. #include <mach/mach_time.h>
  16. #if SD_MAC
  17. static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext);
  18. #endif
  19. #define kSDDisplayLinkInterval 1.0 / 60
  20. @interface SDDisplayLink ()
  21. @property (nonatomic, assign) NSTimeInterval previousFireTime;
  22. @property (nonatomic, assign) NSTimeInterval nextFireTime;
  23. #if SD_MAC
  24. @property (nonatomic, assign) CVDisplayLinkRef displayLink;
  25. @property (nonatomic, assign) CVTimeStamp outputTime;
  26. @property (nonatomic, copy) NSRunLoopMode runloopMode;
  27. #elif SD_IOS || SD_TV
  28. @property (nonatomic, strong) CADisplayLink *displayLink;
  29. #else
  30. @property (nonatomic, strong) NSTimer *displayLink;
  31. @property (nonatomic, strong) NSRunLoop *runloop;
  32. @property (nonatomic, copy) NSRunLoopMode runloopMode;
  33. #endif
  34. @end
  35. @implementation SDDisplayLink
  36. - (void)dealloc {
  37. #if SD_MAC
  38. if (_displayLink) {
  39. CVDisplayLinkStop(_displayLink);
  40. CVDisplayLinkRelease(_displayLink);
  41. _displayLink = NULL;
  42. }
  43. #elif SD_IOS || SD_TV
  44. [_displayLink invalidate];
  45. _displayLink = nil;
  46. #else
  47. [_displayLink invalidate];
  48. _displayLink = nil;
  49. #endif
  50. }
  51. - (instancetype)initWithTarget:(id)target selector:(SEL)sel {
  52. self = [super init];
  53. if (self) {
  54. _target = target;
  55. _selector = sel;
  56. // CA/CV/NSTimer will retain to the target, we need to break this using weak proxy
  57. SDWeakProxy *weakProxy = [SDWeakProxy proxyWithTarget:self];
  58. #if SD_MAC
  59. CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink);
  60. // Simulate retain for target, the target is weak proxy to self
  61. CVDisplayLinkSetOutputCallback(_displayLink, DisplayLinkCallback, (__bridge_retained void *)weakProxy);
  62. #elif SD_IOS || SD_TV
  63. _displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayLinkDidRefresh:)];
  64. #else
  65. _displayLink = [NSTimer timerWithTimeInterval:kSDDisplayLinkInterval target:weakProxy selector:@selector(displayLinkDidRefresh:) userInfo:nil repeats:YES];
  66. #endif
  67. }
  68. return self;
  69. }
  70. + (instancetype)displayLinkWithTarget:(id)target selector:(SEL)sel {
  71. SDDisplayLink *displayLink = [[SDDisplayLink alloc] initWithTarget:target selector:sel];
  72. return displayLink;
  73. }
  74. - (NSTimeInterval)duration {
  75. NSTimeInterval duration = 0;
  76. #if SD_MAC
  77. CVTimeStamp outputTime = self.outputTime;
  78. double periodPerSecond = (double)outputTime.videoTimeScale * outputTime.rateScalar;
  79. if (periodPerSecond > 0) {
  80. duration = (double)outputTime.videoRefreshPeriod / periodPerSecond;
  81. }
  82. #elif SD_UIKIT
  83. // iOS 10+/watchOS use `nextTime`
  84. if (@available(iOS 10.0, tvOS 10.0, *)) {
  85. duration = self.nextFireTime - CACurrentMediaTime();
  86. } else {
  87. // iOS 9 use `previousTime`
  88. duration = CACurrentMediaTime() - self.previousFireTime;
  89. }
  90. #else
  91. if (self.nextFireTime != 0) {
  92. // `CFRunLoopTimerGetNextFireDate`: This time could be a date in the past if a run loop has not been able to process the timer since the firing time arrived.
  93. // Don't rely on this, always calculate based on elapsed time
  94. duration = CFRunLoopTimerGetNextFireDate((__bridge CFRunLoopTimerRef)self.displayLink) - self.nextFireTime;
  95. }
  96. #endif
  97. // When system sleep, the targetTimestamp will mass up, fallback refresh rate
  98. if (duration < 0) {
  99. #if SD_MAC
  100. // Supports Pro display 120Hz
  101. CGDirectDisplayID display = CVDisplayLinkGetCurrentCGDisplay(_displayLink);
  102. CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display);
  103. if (mode) {
  104. double refreshRate = CGDisplayModeGetRefreshRate(mode);
  105. if (refreshRate > 0) {
  106. duration = 1.0 / refreshRate;
  107. } else {
  108. duration = kSDDisplayLinkInterval;
  109. }
  110. CGDisplayModeRelease(mode);
  111. } else {
  112. duration = kSDDisplayLinkInterval;
  113. }
  114. #elif SD_IOS || SD_TV
  115. // Fallback
  116. duration = self.displayLink.duration;
  117. #else
  118. // Watch always 60Hz
  119. duration = kSDDisplayLinkInterval;
  120. #endif
  121. }
  122. return duration;
  123. }
  124. - (BOOL)isRunning {
  125. #if SD_MAC
  126. return CVDisplayLinkIsRunning(self.displayLink);
  127. #elif SD_IOS || SD_TV
  128. return !self.displayLink.isPaused;
  129. #else
  130. return self.displayLink.isValid;
  131. #endif
  132. }
  133. - (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode {
  134. if (!runloop || !mode) {
  135. return;
  136. }
  137. #if SD_MAC
  138. self.runloopMode = mode;
  139. #elif SD_IOS || SD_TV
  140. [self.displayLink addToRunLoop:runloop forMode:mode];
  141. #else
  142. self.runloop = runloop;
  143. self.runloopMode = mode;
  144. CFRunLoopMode cfMode;
  145. if ([mode isEqualToString:NSDefaultRunLoopMode]) {
  146. cfMode = kCFRunLoopDefaultMode;
  147. } else if ([mode isEqualToString:NSRunLoopCommonModes]) {
  148. cfMode = kCFRunLoopCommonModes;
  149. } else {
  150. cfMode = (__bridge CFStringRef)mode;
  151. }
  152. CFRunLoopAddTimer(runloop.getCFRunLoop, (__bridge CFRunLoopTimerRef)self.displayLink, cfMode);
  153. #endif
  154. }
  155. - (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode {
  156. if (!runloop || !mode) {
  157. return;
  158. }
  159. #if SD_MAC
  160. self.runloopMode = nil;
  161. #elif SD_IOS || SD_TV
  162. [self.displayLink removeFromRunLoop:runloop forMode:mode];
  163. #else
  164. self.runloop = nil;
  165. self.runloopMode = nil;
  166. CFRunLoopMode cfMode;
  167. if ([mode isEqualToString:NSDefaultRunLoopMode]) {
  168. cfMode = kCFRunLoopDefaultMode;
  169. } else if ([mode isEqualToString:NSRunLoopCommonModes]) {
  170. cfMode = kCFRunLoopCommonModes;
  171. } else {
  172. cfMode = (__bridge CFStringRef)mode;
  173. }
  174. CFRunLoopRemoveTimer(runloop.getCFRunLoop, (__bridge CFRunLoopTimerRef)self.displayLink, cfMode);
  175. #endif
  176. }
  177. - (void)start {
  178. #if SD_MAC
  179. CVDisplayLinkStart(self.displayLink);
  180. #elif SD_IOS || SD_TV
  181. self.displayLink.paused = NO;
  182. #else
  183. if (self.displayLink.isValid) {
  184. [self.displayLink fire];
  185. } else {
  186. SDWeakProxy *weakProxy = [SDWeakProxy proxyWithTarget:self];
  187. self.displayLink = [NSTimer timerWithTimeInterval:kSDDisplayLinkInterval target:weakProxy selector:@selector(displayLinkDidRefresh:) userInfo:nil repeats:YES];
  188. [self addToRunLoop:self.runloop forMode:self.runloopMode];
  189. }
  190. #endif
  191. }
  192. - (void)stop {
  193. #if SD_MAC
  194. CVDisplayLinkStop(self.displayLink);
  195. #elif SD_IOS || SD_TV
  196. self.displayLink.paused = YES;
  197. #else
  198. [self.displayLink invalidate];
  199. #endif
  200. self.previousFireTime = 0;
  201. self.nextFireTime = 0;
  202. }
  203. - (void)displayLinkDidRefresh:(id)displayLink {
  204. #if SD_IOS || SD_TV
  205. if (@available(iOS 10.0, tvOS 10.0, *)) {
  206. self.nextFireTime = self.displayLink.targetTimestamp;
  207. } else {
  208. self.previousFireTime = self.displayLink.timestamp;
  209. }
  210. #endif
  211. #pragma clang diagnostic push
  212. #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  213. [_target performSelector:_selector withObject:self];
  214. #pragma clang diagnostic pop
  215. #if SD_WATCH
  216. self.nextFireTime = CFRunLoopTimerGetNextFireDate((__bridge CFRunLoopTimerRef)self.displayLink);
  217. #endif
  218. }
  219. @end
  220. #if SD_MAC
  221. static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) {
  222. // CVDisplayLink callback is not on main queue
  223. // Actually `SDWeakProxy` but not `SDDisplayLink`
  224. SDDisplayLink *object = (__bridge SDDisplayLink *)displayLinkContext;
  225. if (!object) return kCVReturnSuccess;
  226. // CVDisplayLink does not use runloop, but we can provide similar behavior for modes
  227. // May use `default` runloop to avoid extra callback when in `eventTracking` (mouse drag, scroll) or `modalPanel` (modal panel)
  228. NSString *runloopMode = object.runloopMode;
  229. if (![runloopMode isEqualToString:NSRunLoopCommonModes] && ![runloopMode isEqualToString:NSRunLoop.mainRunLoop.currentMode]) {
  230. return kCVReturnSuccess;
  231. }
  232. CVTimeStamp outputTime = inOutputTime ? *inOutputTime : *inNow;
  233. __weak SDDisplayLink *weakObject = object;
  234. dispatch_async(dispatch_get_main_queue(), ^{
  235. weakObject.outputTime = outputTime;
  236. [weakObject displayLinkDidRefresh:(__bridge id)(displayLink)];
  237. });
  238. return kCVReturnSuccess;
  239. }
  240. #endif