杨锴
2025-04-16 09a372bc45fde16fd42257ab6f78b8deeecf720b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
 
#import "SDAnimatedImagePlayer.h"
#import "NSImage+Compatibility.h"
#import "SDDisplayLink.h"
#import "SDDeviceHelper.h"
#import "SDImageFramePool.h"
#import "SDInternalMacros.h"
 
@interface SDAnimatedImagePlayer () {
    NSRunLoopMode _runLoopMode;
}
 
@property (nonatomic, strong) SDImageFramePool *framePool;
 
@property (nonatomic, strong, readwrite) UIImage *currentFrame;
@property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
@property (nonatomic, assign, readwrite) NSUInteger currentLoopCount;
@property (nonatomic, strong) id<SDAnimatedImageProvider> animatedProvider;
@property (nonatomic, assign) NSUInteger currentFrameBytes;
@property (nonatomic, assign) NSTimeInterval currentTime;
@property (nonatomic, assign) BOOL bufferMiss;
@property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable;
@property (nonatomic, assign) BOOL shouldReverse;
@property (nonatomic, strong) SDDisplayLink *displayLink;
 
@end
 
@implementation SDAnimatedImagePlayer
 
- (instancetype)initWithProvider:(id<SDAnimatedImageProvider>)provider {
    self = [super init];
    if (self) {
        NSUInteger animatedImageFrameCount = provider.animatedImageFrameCount;
        // Check the frame count
        if (animatedImageFrameCount <= 1) {
            return nil;
        }
        self.totalFrameCount = animatedImageFrameCount;
        // Get the current frame and loop count.
        self.totalLoopCount = provider.animatedImageLoopCount;
        self.animatedProvider = provider;
        self.playbackRate = 1.0;
        self.framePool = [SDImageFramePool registerProvider:provider];
    }
    return self;
}
 
+ (instancetype)playerWithProvider:(id<SDAnimatedImageProvider>)provider {
    SDAnimatedImagePlayer *player = [[SDAnimatedImagePlayer alloc] initWithProvider:provider];
    return player;
}
 
- (void)dealloc {
    // Dereference the frame pool, when zero the frame pool for provider will dealloc
    [SDImageFramePool unregisterProvider:self.animatedProvider];
}
 
#pragma mark - Private
 
- (SDDisplayLink *)displayLink {
    if (!_displayLink) {
        _displayLink = [SDDisplayLink displayLinkWithTarget:self selector:@selector(displayDidRefresh:)];
        [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];
        [_displayLink stop];
    }
    return _displayLink;
}
 
- (void)setRunLoopMode:(NSRunLoopMode)runLoopMode {
    if ([_runLoopMode isEqual:runLoopMode]) {
        return;
    }
    if (_displayLink) {
        if (_runLoopMode) {
            [_displayLink removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:_runLoopMode];
        }
        if (runLoopMode.length > 0) {
            [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:runLoopMode];
        }
    }
    _runLoopMode = [runLoopMode copy];
}
 
- (NSRunLoopMode)runLoopMode {
    if (!_runLoopMode) {
        _runLoopMode = [[self class] defaultRunLoopMode];
    }
    return _runLoopMode;
}
 
#pragma mark - State Control
 
- (void)setupCurrentFrame {
    if (self.currentFrameIndex != 0) {
        return;
    }
    if (self.playbackMode == SDAnimatedImagePlaybackModeReverse ||
               self.playbackMode == SDAnimatedImagePlaybackModeReversedBounce) {
        self.currentFrameIndex = self.totalFrameCount - 1;
    }
    
    if (!self.currentFrame && [self.animatedProvider isKindOfClass:[UIImage class]]) {
        UIImage *image = (UIImage *)self.animatedProvider;
        // Cache the poster image if available, but should not callback to avoid caller thread issues
        #if SD_MAC
        UIImage *posterFrame = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
        #else
        UIImage *posterFrame = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
        #endif
        if (posterFrame) {
            // Calculate max buffer size
            [self calculateMaxBufferCountWithFrame:posterFrame];
            // HACK: The first frame should not check duration and immediately display
            self.needsDisplayWhenImageBecomesAvailable = YES;
            [self.framePool setFrame:posterFrame atIndex:self.currentFrameIndex];
        }
    }
    
}
 
- (void)resetCurrentFrameStatus {
    // These should not trigger KVO, user don't need to receive an `index == 0, image == nil` callback.
    _currentFrame = nil;
    _currentFrameIndex = 0;
    _currentLoopCount = 0;
    _currentTime = 0;
    _bufferMiss = NO;
    _needsDisplayWhenImageBecomesAvailable = NO;
}
 
- (void)clearFrameBuffer {
    [self.framePool removeAllFrames];
}
 
#pragma mark - Animation Control
- (void)startPlaying {
    [self.displayLink start];
    // Setup frame
    [self setupCurrentFrame];
}
 
- (void)stopPlaying {
    // Using `_displayLink` here because when UIImageView dealloc, it may trigger `[self stopAnimating]`, we already release the display link in SDAnimatedImageView's dealloc method.
    [_displayLink stop];
    // We need to reset the frame status, but not trigger any handle. This can ensure next time's playing status correct.
    [self resetCurrentFrameStatus];
}
 
- (void)pausePlaying {
    [_displayLink stop];
}
 
- (BOOL)isPlaying {
    return _displayLink.isRunning;
}
 
- (void)seekToFrameAtIndex:(NSUInteger)index loopCount:(NSUInteger)loopCount {
    if (index >= self.totalFrameCount) {
        return;
    }
    self.currentFrameIndex = index;
    self.currentLoopCount = loopCount;
    self.currentFrame = [self.animatedProvider animatedImageFrameAtIndex:index];
    [self handleFrameChange];
}
 
#pragma mark - Core Render
- (void)displayDidRefresh:(SDDisplayLink *)displayLink {
    // If for some reason a wild call makes it through when we shouldn't be animating, bail.
    // Early return!
    if (!self.isPlaying) {
        return;
    }
    
    NSUInteger totalFrameCount = self.totalFrameCount;
    if (totalFrameCount <= 1) {
        // Total frame count less than 1, wrong configuration and stop animating
        [self stopPlaying];
        return;
    }
    
    NSTimeInterval playbackRate = self.playbackRate;
    if (playbackRate <= 0) {
        // Does not support <= 0 play rate
        [self stopPlaying];
        return;
    }
    
    // Calculate refresh duration
    NSTimeInterval duration = self.displayLink.duration;
    
    NSUInteger currentFrameIndex = self.currentFrameIndex;
    NSUInteger nextFrameIndex = (currentFrameIndex + 1) % totalFrameCount;
    
    if (self.playbackMode == SDAnimatedImagePlaybackModeReverse) {
        nextFrameIndex = currentFrameIndex == 0 ? (totalFrameCount - 1) : (currentFrameIndex - 1) % totalFrameCount;
        
    } else if (self.playbackMode == SDAnimatedImagePlaybackModeBounce ||
               self.playbackMode == SDAnimatedImagePlaybackModeReversedBounce) {
        if (currentFrameIndex == 0) {
            self.shouldReverse = NO;
        } else if (currentFrameIndex == totalFrameCount - 1) {
            self.shouldReverse = YES;
        }
        nextFrameIndex = self.shouldReverse ? (currentFrameIndex - 1) : (currentFrameIndex + 1);
        nextFrameIndex %= totalFrameCount;
    }
    
    
    // Check if we need to display new frame firstly
    if (self.needsDisplayWhenImageBecomesAvailable) {
        UIImage *currentFrame = [self.framePool frameAtIndex:currentFrameIndex];
        
        // Update the current frame
        if (currentFrame) {
            // Update the current frame immediately
            self.currentFrame = currentFrame;
            [self handleFrameChange];
            
            self.bufferMiss = NO;
            self.needsDisplayWhenImageBecomesAvailable = NO;
        }
        else {
            self.bufferMiss = YES;
        }
    }
    
    // Check if we have the frame buffer
    if (!self.bufferMiss) {
        // Then check if timestamp is reached
        self.currentTime += duration;
        NSTimeInterval currentDuration = [self.animatedProvider animatedImageDurationAtIndex:currentFrameIndex];
        currentDuration = currentDuration / playbackRate;
        if (self.currentTime < currentDuration) {
            // Current frame timestamp not reached, prefetch frame in advance.
            [self prefetchFrameAtIndex:currentFrameIndex
                             nextIndex:nextFrameIndex];
            return;
        }
        
        // Otherwise, we should be ready to display next frame
        self.needsDisplayWhenImageBecomesAvailable = YES;
        self.currentFrameIndex = nextFrameIndex;
        self.currentTime -= currentDuration;
        NSTimeInterval nextDuration = [self.animatedProvider animatedImageDurationAtIndex:nextFrameIndex];
        nextDuration = nextDuration / playbackRate;
        if (self.currentTime > nextDuration) {
            // Do not skip frame
            self.currentTime = nextDuration;
        }
        
        // Update the loop count when last frame rendered
        if (nextFrameIndex == 0) {
            // Update the loop count
            self.currentLoopCount++;
            [self handleLoopChange];
            
            // if reached the max loop count, stop animating, 0 means loop indefinitely
            NSUInteger maxLoopCount = self.totalLoopCount;
            if (maxLoopCount != 0 && (self.currentLoopCount >= maxLoopCount)) {
                [self stopPlaying];
                return;
            }
        }
    }
    
    // Since we support handler, check animating state again
    if (!self.isPlaying) {
        return;
    }
    
    [self prefetchFrameAtIndex:currentFrameIndex
                     nextIndex:nextFrameIndex];
}
 
// Check if we should prefetch next frame or current frame
// When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame
// Or, most cases, the decode speed is faster than render speed, we fetch next frame
- (void)prefetchFrameAtIndex:(NSUInteger)currentIndex
                   nextIndex:(NSUInteger)nextIndex {
    NSUInteger fetchFrameIndex = currentIndex;
    UIImage *fetchFrame = nil;
    if (!self.bufferMiss) {
        fetchFrameIndex = nextIndex;
        fetchFrame = [self.framePool frameAtIndex:nextIndex];
    }
    BOOL bufferFull = NO;
    if (self.framePool.currentFrameCount == self.totalFrameCount) {
        bufferFull = YES;
    }
    if (!fetchFrame && !bufferFull) {
        // Calculate max buffer size
        [self calculateMaxBufferCountWithFrame:self.currentFrame];
        // Prefetch next frame
        [self.framePool prefetchFrameAtIndex:fetchFrameIndex];
    }
}
 
- (void)handleFrameChange {
    if (self.animationFrameHandler) {
        self.animationFrameHandler(self.currentFrameIndex, self.currentFrame);
    }
}
 
- (void)handleLoopChange {
    if (self.animationLoopHandler) {
        self.animationLoopHandler(self.currentLoopCount);
    }
}
 
#pragma mark - Util
- (void)calculateMaxBufferCountWithFrame:(nonnull UIImage *)frame {
    NSUInteger bytes = self.currentFrameBytes;
    if (bytes == 0) {
        bytes = CGImageGetBytesPerRow(frame.CGImage) * CGImageGetHeight(frame.CGImage);
        if (bytes == 0) {
            bytes = 1024;
        } else {
            // Cache since most animated image each frame bytes is the same
            self.currentFrameBytes = bytes;
        }
    }
    
    NSUInteger max = 0;
    if (self.maxBufferSize > 0) {
        max = self.maxBufferSize;
    } else {
        // Calculate based on current memory, these factors are by experience
        NSUInteger total = [SDDeviceHelper totalMemory];
        NSUInteger free = [SDDeviceHelper freeMemory];
        max = MIN(total * 0.2, free * 0.6);
    }
    
    NSUInteger maxBufferCount = (double)max / (double)bytes;
    if (!maxBufferCount) {
        // At least 1 frame
        maxBufferCount = 1;
    }
    
    self.framePool.maxBufferCount = maxBufferCount;
}
 
+ (NSString *)defaultRunLoopMode {
    // Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations.
    return [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode;
}
 
@end