Fix the issue that SDAnimatedImageView will trigger an empty callback when animation stopped. This will cause some bad effect such as rendering a empty image or placeholder image (especially on iOS 14)
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 |
|
|
9 |
#import "SDAnimatedImagePlayer.h"
|
|
10 |
#import "NSImage+Compatibility.h"
|
|
11 |
#import "SDDisplayLink.h"
|
|
12 |
#import "SDDeviceHelper.h"
|
|
13 |
#import "SDInternalMacros.h"
|
|
14 |
|
|
15 |
@interface SDAnimatedImagePlayer () { |
|
16 |
NSRunLoopMode _runLoopMode; |
|
17 |
}
|
|
18 |
|
|
19 |
@property (nonatomic, strong, readwrite) UIImage *currentFrame; |
|
20 |
@property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex; |
|
21 |
@property (nonatomic, assign, readwrite) NSUInteger currentLoopCount; |
|
22 |
@property (nonatomic, strong) id<SDAnimatedImageProvider> animatedProvider; |
|
23 |
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, UIImage *> *frameBuffer; |
|
24 |
@property (nonatomic, assign) NSTimeInterval currentTime; |
|
25 |
@property (nonatomic, assign) BOOL bufferMiss; |
|
26 |
@property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable; |
|
27 |
@property (nonatomic, assign) NSUInteger maxBufferCount; |
|
28 |
@property (nonatomic, strong) NSOperationQueue *fetchQueue; |
|
29 |
@property (nonatomic, strong) dispatch_semaphore_t lock; |
|
30 |
@property (nonatomic, strong) SDDisplayLink *displayLink; |
|
31 |
|
|
32 |
@end
|
|
33 |
|
|
34 |
@implementation SDAnimatedImagePlayer |
|
35 |
|
|
36 | 2 |
- (instancetype)initWithProvider:(id<SDAnimatedImageProvider>)provider { |
37 | 2 |
self = [super init]; |
38 | 2 |
if (self) { |
39 | 2 |
NSUInteger animatedImageFrameCount = provider.animatedImageFrameCount; |
40 | 2 |
// Check the frame count
|
41 | 2 |
if (animatedImageFrameCount <= 1) { |
42 |
return nil; |
|
43 |
}
|
|
44 | 2 |
self.totalFrameCount = animatedImageFrameCount; |
45 | 2 |
// Get the current frame and loop count.
|
46 | 2 |
self.totalLoopCount = provider.animatedImageLoopCount; |
47 | 2 |
self.animatedProvider = provider; |
48 | 2 |
self.playbackRate = 1.0; |
49 | 1 |
#if SD_UIKIT
|
50 | 1 |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; |
51 | 1 |
#endif
|
52 |
}
|
|
53 | 2 |
return self; |
54 |
}
|
|
55 |
|
|
56 | 2 |
+ (instancetype)playerWithProvider:(id<SDAnimatedImageProvider>)provider { |
57 | 2 |
SDAnimatedImagePlayer *player = [[SDAnimatedImagePlayer alloc] initWithProvider:provider]; |
58 | 2 |
return player; |
59 |
}
|
|
60 |
|
|
61 |
#pragma mark - Life Cycle
|
|
62 |
|
|
63 | 2 |
- (void)dealloc { |
64 | 1 |
#if SD_UIKIT
|
65 | 1 |
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; |
66 | 1 |
#endif
|
67 |
}
|
|
68 |
|
|
69 | 1 |
- (void)didReceiveMemoryWarning:(NSNotification *)notification { |
70 | 1 |
[_fetchQueue cancelAllOperations]; |
71 | 1 |
[_fetchQueue addOperationWithBlock:^{ |
72 | 1 |
NSNumber *currentFrameIndex = @(self.currentFrameIndex); |
73 | 1 |
SD_LOCK(self.lock); |
74 | 1 |
NSArray *keys = self.frameBuffer.allKeys; |
75 | 1 |
// only keep the next frame for later rendering
|
76 | 1 |
for (NSNumber * key in keys) { |
77 | 1 |
if (![key isEqualToNumber:currentFrameIndex]) { |
78 | 1 |
[self.frameBuffer removeObjectForKey:key]; |
79 |
}
|
|
80 |
}
|
|
81 | 1 |
SD_UNLOCK(self.lock); |
82 | 1 |
}];
|
83 |
}
|
|
84 |
|
|
85 |
#pragma mark - Private
|
|
86 | 2 |
- (NSOperationQueue *)fetchQueue { |
87 | 2 |
if (!_fetchQueue) { |
88 | 2 |
_fetchQueue = [[NSOperationQueue alloc] init]; |
89 | 2 |
_fetchQueue.maxConcurrentOperationCount = 1; |
90 |
}
|
|
91 | 2 |
return _fetchQueue; |
92 |
}
|
|
93 |
|
|
94 | 2 |
- (NSMutableDictionary<NSNumber *,UIImage *> *)frameBuffer { |
95 | 2 |
if (!_frameBuffer) { |
96 | 2 |
_frameBuffer = [NSMutableDictionary dictionary]; |
97 |
}
|
|
98 | 2 |
return _frameBuffer; |
99 |
}
|
|
100 |
|
|
101 | 2 |
- (dispatch_semaphore_t)lock { |
102 | 2 |
if (!_lock) { |
103 | 2 |
_lock = dispatch_semaphore_create(1); |
104 |
}
|
|
105 | 2 |
return _lock; |
106 |
}
|
|
107 |
|
|
108 | 2 |
- (SDDisplayLink *)displayLink { |
109 | 2 |
if (!_displayLink) { |
110 | 2 |
_displayLink = [SDDisplayLink displayLinkWithTarget:self selector:@selector(displayDidRefresh:)]; |
111 | 2 |
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode]; |
112 | 2 |
[_displayLink stop]; |
113 |
}
|
|
114 | 2 |
return _displayLink; |
115 |
}
|
|
116 |
|
|
117 | 2 |
- (void)setRunLoopMode:(NSRunLoopMode)runLoopMode { |
118 | 2 |
if ([_runLoopMode isEqual:runLoopMode]) { |
119 | 2 |
return; |
120 |
}
|
|
121 | 2 |
if (_displayLink) { |
122 |
if (_runLoopMode) { |
|
123 |
[_displayLink removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:_runLoopMode]; |
|
124 |
}
|
|
125 |
if (runLoopMode.length > 0) { |
|
126 |
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:runLoopMode]; |
|
127 |
}
|
|
128 |
}
|
|
129 | 2 |
_runLoopMode = [runLoopMode copy]; |
130 |
}
|
|
131 |
|
|
132 | 2 |
- (NSRunLoopMode)runLoopMode { |
133 | 2 |
if (!_runLoopMode) { |
134 |
_runLoopMode = [[self class] defaultRunLoopMode]; |
|
135 |
}
|
|
136 | 2 |
return _runLoopMode; |
137 |
}
|
|
138 |
|
|
139 |
#pragma mark - State Control
|
|
140 |
|
|
141 | 2 |
- (void)setupCurrentFrame { |
142 | 2 |
if (self.currentFrameIndex != 0) { |
143 |
return; |
|
144 |
}
|
|
145 | 2 |
if ([self.animatedProvider isKindOfClass:[UIImage class]]) { |
146 | 2 |
UIImage *image = (UIImage *)self.animatedProvider; |
147 | 2 |
// Use the poster image if available
|
148 | 1 |
#if SD_MAC
|
149 | 1 |
UIImage *posterFrame = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp]; |
150 |
#else
|
|
151 |
UIImage *posterFrame = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation]; |
|
152 | 1 |
#endif
|
153 | 2 |
if (posterFrame) { |
154 | 2 |
self.currentFrame = posterFrame; |
155 | 2 |
SD_LOCK(self.lock); |
156 | 2 |
self.frameBuffer[@(self.currentFrameIndex)] = self.currentFrame; |
157 | 2 |
SD_UNLOCK(self.lock); |
158 | 2 |
[self handleFrameChange]; |
159 |
}
|
|
160 |
}
|
|
161 |
}
|
|
162 |
|
|
163 | 2 |
- (void)resetCurrentFrameStatus { |
164 | 2 |
// These should not trigger KVO, user don't need to receive an `index == 0, image == nil` callback.
|
165 | 2 |
_currentFrame = nil; |
166 | 2 |
_currentFrameIndex = 0; |
167 | 2 |
_currentLoopCount = 0; |
168 | 2 |
_currentTime = 0; |
169 | 2 |
_bufferMiss = NO; |
170 | 2 |
_needsDisplayWhenImageBecomesAvailable = NO; |
171 |
}
|
|
172 |
|
|
173 | 2 |
- (void)clearFrameBuffer { |
174 | 2 |
SD_LOCK(self.lock); |
175 | 2 |
[_frameBuffer removeAllObjects]; |
176 | 2 |
SD_UNLOCK(self.lock); |
177 |
}
|
|
178 |
|
|
179 |
#pragma mark - Animation Control
|
|
180 | 2 |
- (void)startPlaying { |
181 | 2 |
[self.displayLink start]; |
182 | 2 |
// Setup frame
|
183 | 2 |
if (self.currentFrameIndex == 0 && !self.currentFrame) { |
184 | 2 |
[self setupCurrentFrame]; |
185 |
}
|
|
186 | 2 |
// Calculate max buffer size
|
187 | 2 |
[self calculateMaxBufferCount]; |
188 |
}
|
|
189 |
|
|
190 | 2 |
- (void)stopPlaying { |
191 | 2 |
[_fetchQueue cancelAllOperations]; |
192 | 2 |
// Using `_displayLink` here because when UIImageView dealloc, it may trigger `[self stopAnimating]`, we already release the display link in SDAnimatedImageView's dealloc method.
|
193 | 2 |
[_displayLink stop]; |
194 | 2 |
// We need to reset the frame status, but not trigger any handle. This can ensure next time's playing status correct.
|
195 | 2 |
[self resetCurrentFrameStatus]; |
196 |
}
|
|
197 |
|
|
198 | 2 |
- (void)pausePlaying { |
199 | 2 |
[_fetchQueue cancelAllOperations]; |
200 | 2 |
[_displayLink stop]; |
201 |
}
|
|
202 |
|
|
203 | 2 |
- (BOOL)isPlaying { |
204 | 2 |
return _displayLink.isRunning; |
205 |
}
|
|
206 |
|
|
207 | 2 |
- (void)seekToFrameAtIndex:(NSUInteger)index loopCount:(NSUInteger)loopCount { |
208 | 2 |
if (index >= self.totalFrameCount) { |
209 |
return; |
|
210 |
}
|
|
211 | 2 |
self.currentFrameIndex = index; |
212 | 2 |
self.currentLoopCount = loopCount; |
213 | 2 |
self.currentFrame = [self.animatedProvider animatedImageFrameAtIndex:index]; |
214 | 2 |
[self handleFrameChange]; |
215 |
}
|
|
216 |
|
|
217 |
#pragma mark - Core Render
|
|
218 | 2 |
- (void)displayDidRefresh:(SDDisplayLink *)displayLink { |
219 | 2 |
// If for some reason a wild call makes it through when we shouldn't be animating, bail.
|
220 | 2 |
// Early return!
|
221 | 2 |
if (!self.isPlaying) { |
222 | 1 |
return; |
223 |
}
|
|
224 |
|
|
225 | 2 |
NSUInteger totalFrameCount = self.totalFrameCount; |
226 | 2 |
if (totalFrameCount <= 1) { |
227 |
// Total frame count less than 1, wrong configuration and stop animating
|
|
228 |
[self stopPlaying]; |
|
229 |
return; |
|
230 |
}
|
|
231 |
|
|
232 | 2 |
NSTimeInterval playbackRate = self.playbackRate; |
233 | 2 |
if (playbackRate <= 0) { |
234 |
// Does not support <= 0 play rate
|
|
235 |
[self stopPlaying]; |
|
236 |
return; |
|
237 |
}
|
|
238 |
|
|
239 | 2 |
// Calculate refresh duration
|
240 | 2 |
NSTimeInterval duration = self.displayLink.duration; |
241 |
|
|
242 | 2 |
NSUInteger currentFrameIndex = self.currentFrameIndex; |
243 | 2 |
NSUInteger nextFrameIndex = (currentFrameIndex + 1) % totalFrameCount; |
244 |
|
|
245 | 2 |
// Check if we need to display new frame firstly
|
246 | 2 |
BOOL bufferFull = NO; |
247 | 2 |
if (self.needsDisplayWhenImageBecomesAvailable) { |
248 | 2 |
UIImage *currentFrame; |
249 | 2 |
SD_LOCK(self.lock); |
250 | 2 |
currentFrame = self.frameBuffer[@(currentFrameIndex)]; |
251 | 2 |
SD_UNLOCK(self.lock); |
252 |
|
|
253 | 2 |
// Update the current frame
|
254 | 2 |
if (currentFrame) { |
255 | 2 |
SD_LOCK(self.lock); |
256 | 2 |
// Remove the frame buffer if need
|
257 | 2 |
if (self.frameBuffer.count > self.maxBufferCount) { |
258 |
self.frameBuffer[@(currentFrameIndex)] = nil; |
|
259 |
}
|
|
260 | 2 |
// Check whether we can stop fetch
|
261 | 2 |
if (self.frameBuffer.count == totalFrameCount) { |
262 | 2 |
bufferFull = YES; |
263 |
}
|
|
264 | 2 |
SD_UNLOCK(self.lock); |
265 |
|
|
266 | 2 |
// Update the current frame immediately
|
267 | 2 |
self.currentFrame = currentFrame; |
268 | 2 |
[self handleFrameChange]; |
269 |
|
|
270 | 2 |
self.bufferMiss = NO; |
271 | 2 |
self.needsDisplayWhenImageBecomesAvailable = NO; |
272 |
}
|
|
273 | 2 |
else { |
274 | 2 |
self.bufferMiss = YES; |
275 |
}
|
|
276 |
}
|
|
277 |
|
|
278 | 2 |
// Check if we have the frame buffer
|
279 | 2 |
if (!self.bufferMiss) { |
280 | 2 |
// Then check if timestamp is reached
|
281 | 2 |
self.currentTime += duration; |
282 | 2 |
NSTimeInterval currentDuration = [self.animatedProvider animatedImageDurationAtIndex:currentFrameIndex]; |
283 | 2 |
currentDuration = currentDuration / playbackRate; |
284 | 2 |
if (self.currentTime < currentDuration) { |
285 | 2 |
// Current frame timestamp not reached, return
|
286 | 2 |
return; |
287 |
}
|
|
288 |
|
|
289 | 2 |
// Otherwise, we should be ready to display next frame
|
290 | 2 |
self.needsDisplayWhenImageBecomesAvailable = YES; |
291 | 2 |
self.currentFrameIndex = nextFrameIndex; |
292 | 2 |
self.currentTime -= currentDuration; |
293 | 2 |
NSTimeInterval nextDuration = [self.animatedProvider animatedImageDurationAtIndex:nextFrameIndex]; |
294 | 2 |
nextDuration = nextDuration / playbackRate; |
295 | 2 |
if (self.currentTime > nextDuration) { |
296 |
// Do not skip frame
|
|
297 |
self.currentTime = nextDuration; |
|
298 |
}
|
|
299 |
|
|
300 | 2 |
// Update the loop count when last frame rendered
|
301 | 2 |
if (nextFrameIndex == 0) { |
302 | 2 |
// Update the loop count
|
303 | 2 |
self.currentLoopCount++; |
304 | 2 |
[self handleLoopChange]; |
305 |
|
|
306 | 2 |
// if reached the max loop count, stop animating, 0 means loop indefinitely
|
307 | 2 |
NSUInteger maxLoopCount = self.totalLoopCount; |
308 | 2 |
if (maxLoopCount != 0 && (self.currentLoopCount >= maxLoopCount)) { |
309 |
[self stopPlaying]; |
|
310 |
return; |
|
311 |
}
|
|
312 |
}
|
|
313 |
}
|
|
314 |
|
|
315 | 2 |
// Since we support handler, check animating state again
|
316 | 2 |
if (!self.isPlaying) { |
317 | 2 |
return; |
318 |
}
|
|
319 |
|
|
320 | 2 |
// Check if we should prefetch next frame or current frame
|
321 | 2 |
// When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame
|
322 | 2 |
// Or, most cases, the decode speed is faster than render speed, we fetch next frame
|
323 | 2 |
NSUInteger fetchFrameIndex = self.bufferMiss? currentFrameIndex : nextFrameIndex; |
324 | 2 |
UIImage *fetchFrame; |
325 | 2 |
SD_LOCK(self.lock); |
326 | 2 |
fetchFrame = self.bufferMiss? nil : self.frameBuffer[@(nextFrameIndex)]; |
327 | 2 |
SD_UNLOCK(self.lock); |
328 |
|
|
329 | 2 |
if (!fetchFrame && !bufferFull && self.fetchQueue.operationCount == 0) { |
330 | 2 |
// Prefetch next frame in background queue
|
331 | 2 |
id<SDAnimatedImageProvider> animatedProvider = self.animatedProvider; |
332 | 2 |
@weakify(self); |
333 | 2 |
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ |
334 | 2 |
@strongify(self); |
335 | 2 |
if (!self) { |
336 |
return; |
|
337 |
}
|
|
338 | 2 |
UIImage *frame = [animatedProvider animatedImageFrameAtIndex:fetchFrameIndex]; |
339 |
|
|
340 | 2 |
BOOL isAnimating = self.displayLink.isRunning; |
341 | 2 |
if (isAnimating) { |
342 | 2 |
SD_LOCK(self.lock); |
343 | 2 |
self.frameBuffer[@(fetchFrameIndex)] = frame; |
344 | 2 |
SD_UNLOCK(self.lock); |
345 |
}
|
|
346 | 2 |
}];
|
347 | 2 |
[self.fetchQueue addOperation:operation]; |
348 |
}
|
|
349 |
}
|
|
350 |
|
|
351 | 2 |
- (void)handleFrameChange { |
352 | 2 |
if (self.animationFrameHandler) { |
353 | 2 |
self.animationFrameHandler(self.currentFrameIndex, self.currentFrame); |
354 |
}
|
|
355 |
}
|
|
356 |
|
|
357 | 2 |
- (void)handleLoopChange { |
358 | 2 |
if (self.animationLoopHandler) { |
359 | 2 |
self.animationLoopHandler(self.currentLoopCount); |
360 |
}
|
|
361 |
}
|
|
362 |
|
|
363 |
#pragma mark - Util
|
|
364 | 2 |
- (void)calculateMaxBufferCount { |
365 | 2 |
NSUInteger bytes = CGImageGetBytesPerRow(self.currentFrame.CGImage) * CGImageGetHeight(self.currentFrame.CGImage); |
366 | 2 |
if (bytes == 0) bytes = 1024; |
367 |
|
|
368 | 2 |
NSUInteger max = 0; |
369 | 2 |
if (self.maxBufferSize > 0) { |
370 |
max = self.maxBufferSize; |
|
371 | 2 |
} else { |
372 | 2 |
// Calculate based on current memory, these factors are by experience
|
373 | 2 |
NSUInteger total = [SDDeviceHelper totalMemory]; |
374 | 2 |
NSUInteger free = [SDDeviceHelper freeMemory]; |
375 | 2 |
max = MIN(total * 0.2, free * 0.6); |
376 |
}
|
|
377 |
|
|
378 | 2 |
NSUInteger maxBufferCount = (double)max / (double)bytes; |
379 | 2 |
if (!maxBufferCount) { |
380 |
// At least 1 frame
|
|
381 |
maxBufferCount = 1; |
|
382 |
}
|
|
383 |
|
|
384 | 2 |
self.maxBufferCount = maxBufferCount; |
385 |
}
|
|
386 |
|
|
387 |
+ (NSString *)defaultRunLoopMode { |
|
388 |
// Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations.
|
|
389 |
return [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode; |
|
390 |
}
|
|
391 |
|
|
392 |
@end
|
Read our documentation on viewing source code .