Skip to content

Commit 8b1dfc0

Browse files
committed
Fix the leak of WebImage with animation and NavigationLink. The leak is because of @State which may cause reference cycle
1 parent 4c7f169 commit 8b1dfc0

File tree

2 files changed

+105
-53
lines changed

2 files changed

+105
-53
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* This file is part of the SDWebImage package.
3+
* (c) DreamPiggy <[email protected]>
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 SwiftUI
10+
import SDWebImage
11+
12+
/// A Image observable object for handle aniamted image playback. This is used to avoid `@State` update may capture the View struct type and cause memory leak.
13+
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
14+
public final class ImagePlayer : ObservableObject {
15+
var player: SDAnimatedImagePlayer?
16+
17+
/// Max buffer size
18+
public var maxBufferSize: UInt?
19+
20+
/// Custom loop count
21+
public var customLoopCount: UInt?
22+
23+
/// Animation runloop mode
24+
public var runLoopMode: RunLoop.Mode = .common
25+
26+
/// Animation playback rate
27+
public var playbackRate: Double = 1.0
28+
29+
deinit {
30+
player?.stopPlaying()
31+
currentFrame = nil
32+
}
33+
34+
/// Current playing frame image
35+
@Published public var currentFrame: PlatformImage?
36+
37+
/// Start the animation
38+
public func startPlaying() {
39+
player?.startPlaying()
40+
}
41+
42+
/// Pause the animation
43+
public func pausePlaying() {
44+
player?.pausePlaying()
45+
}
46+
47+
/// Stop the animation
48+
public func stopPlaying() {
49+
player?.stopPlaying()
50+
}
51+
52+
/// Clear the frame buffer
53+
public func clearFrameBuffer() {
54+
player?.clearFrameBuffer()
55+
}
56+
57+
58+
/// Setup the player using Animated Image
59+
/// - Parameter image: animated image
60+
public func setupPlayer(image: PlatformImage?) {
61+
if player != nil {
62+
return
63+
}
64+
if let animatedImage = image as? SDAnimatedImageProvider & PlatformImage {
65+
if let imagePlayer = SDAnimatedImagePlayer(provider: animatedImage) {
66+
imagePlayer.animationFrameHandler = { [weak self] (_, frame) in
67+
self?.currentFrame = frame
68+
}
69+
// Setup configuration
70+
if let maxBufferSize = maxBufferSize {
71+
imagePlayer.maxBufferSize = maxBufferSize
72+
}
73+
if let customLoopCount = customLoopCount {
74+
imagePlayer.totalLoopCount = UInt(customLoopCount)
75+
}
76+
imagePlayer.runLoopMode = runLoopMode
77+
imagePlayer.playbackRate = playbackRate
78+
79+
self.player = imagePlayer
80+
81+
let posterFrame = PlatformImage(cgImage: animatedImage.cgImage!, scale: animatedImage.scale, orientation: .up)
82+
currentFrame = posterFrame
83+
}
84+
}
85+
}
86+
}

SDWebImageSwiftUI/Classes/WebImage.swift

Lines changed: 19 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,10 @@ public struct WebImage : View {
2424
/// True to start animation, false to stop animation.
2525
@Binding public var isAnimating: Bool
2626

27-
@State var currentFrame: PlatformImage? = nil
28-
@State var imagePlayer: SDAnimatedImagePlayer? = nil
27+
@ObservedObject var imagePlayer: ImagePlayer
2928

30-
var maxBufferSize: UInt?
31-
var customLoopCount: UInt?
32-
var runLoopMode: RunLoop.Mode = .common
3329
var pausable: Bool = true
3430
var purgeable: Bool = false
35-
var playbackRate: Double = 1.0
3631

3732
/// Create a web image with url, placeholder, custom options and context.
3833
/// - Parameter url: The image url
@@ -57,6 +52,7 @@ public struct WebImage : View {
5752
}
5853
}
5954
self.imageManager = ImageManager(url: url, options: options, context: context)
55+
self.imagePlayer = ImagePlayer()
6056
}
6157

6258
public var body: some View {
@@ -67,30 +63,30 @@ public struct WebImage : View {
6763
return Group {
6864
if imageManager.image != nil {
6965
if isAnimating && !imageManager.isIncremental {
70-
if currentFrame != nil {
71-
configure(image: currentFrame!)
66+
if imagePlayer.currentFrame != nil {
67+
configure(image: imagePlayer.currentFrame!)
7268
.onAppear {
73-
self.imagePlayer?.startPlaying()
69+
imagePlayer.startPlaying()
7470
}
7571
.onDisappear {
7672
if self.pausable {
77-
self.imagePlayer?.pausePlaying()
73+
imagePlayer.pausePlaying()
7874
} else {
79-
self.imagePlayer?.stopPlaying()
75+
imagePlayer.stopPlaying()
8076
}
8177
if self.purgeable {
82-
self.imagePlayer?.clearFrameBuffer()
78+
imagePlayer.clearFrameBuffer()
8379
}
8480
}
8581
} else {
8682
configure(image: imageManager.image!)
8783
.onReceive(imageManager.$image) { image in
88-
self.setupPlayer(image: image)
84+
imagePlayer.setupPlayer(image: image)
8985
}
9086
}
9187
} else {
92-
if currentFrame != nil {
93-
configure(image: currentFrame!)
88+
if imagePlayer.currentFrame != nil {
89+
configure(image: imagePlayer.currentFrame!)
9490
} else {
9591
configure(image: imageManager.image!)
9692
}
@@ -185,32 +181,6 @@ public struct WebImage : View {
185181
return AnyView(configure(image: .empty))
186182
}
187183
}
188-
189-
/// Animated Image Support
190-
func setupPlayer(image: PlatformImage?) {
191-
if imagePlayer != nil {
192-
return
193-
}
194-
if let animatedImage = image as? SDAnimatedImageProvider {
195-
if let imagePlayer = SDAnimatedImagePlayer(provider: animatedImage) {
196-
imagePlayer.animationFrameHandler = { (_, frame) in
197-
self.currentFrame = frame
198-
}
199-
// Setup configuration
200-
if let maxBufferSize = maxBufferSize {
201-
imagePlayer.maxBufferSize = maxBufferSize
202-
}
203-
if let customLoopCount = customLoopCount {
204-
imagePlayer.totalLoopCount = UInt(customLoopCount)
205-
}
206-
imagePlayer.runLoopMode = runLoopMode
207-
imagePlayer.playbackRate = playbackRate
208-
209-
self.imagePlayer = imagePlayer
210-
imagePlayer.startPlaying()
211-
}
212-
}
213-
}
214184
}
215185

216186
// Layout
@@ -372,9 +342,8 @@ extension WebImage {
372342
/// - Note: Pass nil to disable customization, use the image itself loop count (`animatedImageLoopCount`) instead
373343
/// - Parameter loopCount: The animation loop count
374344
public func customLoopCount(_ loopCount: UInt?) -> WebImage {
375-
var result = self
376-
result.customLoopCount = loopCount
377-
return result
345+
self.imagePlayer.customLoopCount = loopCount
346+
return self
378347
}
379348

380349
/// Provide a max buffer size by bytes. This is used to adjust frame buffer count and can be useful when the decoding cost is expensive (such as Animated WebP software decoding). Default is nil.
@@ -384,19 +353,17 @@ extension WebImage {
384353
/// `UInt.max` means cache all the buffer. (Lowest CPU and Highest Memory)
385354
/// - Parameter bufferSize: The max buffer size
386355
public func maxBufferSize(_ bufferSize: UInt?) -> WebImage {
387-
var result = self
388-
result.maxBufferSize = bufferSize
389-
return result
356+
self.imagePlayer.maxBufferSize = bufferSize
357+
return self
390358
}
391359

392360
/// The runLoopMode when animation is playing on. Defaults is `.common`
393361
/// You can specify a runloop mode to let it rendering.
394362
/// - Note: This is useful for some cases, for example, always specify NSDefaultRunLoopMode, if you want to pause the animation when user scroll (for Mac user, drag the mouse or touchpad)
395363
/// - Parameter runLoopMode: The runLoopMode for animation
396364
public func runLoopMode(_ runLoopMode: RunLoop.Mode) -> WebImage {
397-
var result = self
398-
result.runLoopMode = runLoopMode
399-
return result
365+
self.imagePlayer.runLoopMode = runLoopMode
366+
return self
400367
}
401368

402369
/// Whether or not to pause the animation (keep current frame), instead of stop the animation (frame index reset to 0). When `isAnimating` binding value changed to false. Defaults is true.
@@ -425,9 +392,8 @@ extension WebImage {
425392
/// `< 0.0` is not supported currently and stop animation. (may support reverse playback in the future)
426393
/// - Parameter playbackRate: The animation playback rate.
427394
public func playbackRate(_ playbackRate: Double) -> WebImage {
428-
var result = self
429-
result.playbackRate = playbackRate
430-
return result
395+
self.imagePlayer.playbackRate = playbackRate
396+
return self
431397
}
432398
}
433399

0 commit comments

Comments
 (0)