Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix the leak of WebImage with animation and NavigationLink. #163

Merged
merged 2 commits into from
Feb 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions SDWebImageSwiftUI/Classes/ImagePlayer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* This file is part of the SDWebImage package.
* (c) DreamPiggy <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import SwiftUI
import SDWebImage

/// 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.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public final class ImagePlayer : ObservableObject {
var player: SDAnimatedImagePlayer?

/// Max buffer size
public var maxBufferSize: UInt?

/// Custom loop count
public var customLoopCount: UInt?

/// Animation runloop mode
public var runLoopMode: RunLoop.Mode = .common

/// Animation playback rate
public var playbackRate: Double = 1.0

deinit {
player?.stopPlaying()
currentFrame = nil
}

/// Current playing frame image
@Published public var currentFrame: PlatformImage?

/// Start the animation
public func startPlaying() {
player?.startPlaying()
}

/// Pause the animation
public func pausePlaying() {
player?.pausePlaying()
}

/// Stop the animation
public func stopPlaying() {
player?.stopPlaying()
}

/// Clear the frame buffer
public func clearFrameBuffer() {
player?.clearFrameBuffer()
}


/// Setup the player using Animated Image
/// - Parameter image: animated image
public func setupPlayer(image: PlatformImage?) {
if player != nil {
return
}
if let animatedImage = image as? SDAnimatedImageProvider & PlatformImage {
if let imagePlayer = SDAnimatedImagePlayer(provider: animatedImage) {
imagePlayer.animationFrameHandler = { [weak self] (_, frame) in
self?.currentFrame = frame
}
// Setup configuration
if let maxBufferSize = maxBufferSize {
imagePlayer.maxBufferSize = maxBufferSize
}
if let customLoopCount = customLoopCount {
imagePlayer.totalLoopCount = UInt(customLoopCount)
}
imagePlayer.runLoopMode = runLoopMode
imagePlayer.playbackRate = playbackRate

self.player = imagePlayer

// Setup poster frame
if let cgImage = animatedImage.cgImage {
currentFrame = PlatformImage(cgImage: cgImage, scale: animatedImage.scale, orientation: .up)
} else {
currentFrame = .empty
}
}
}
}
}
72 changes: 19 additions & 53 deletions SDWebImageSwiftUI/Classes/WebImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,10 @@ public struct WebImage : View {
/// True to start animation, false to stop animation.
@Binding public var isAnimating: Bool

@State var currentFrame: PlatformImage? = nil
@State var imagePlayer: SDAnimatedImagePlayer? = nil
@ObservedObject var imagePlayer: ImagePlayer

var maxBufferSize: UInt?
var customLoopCount: UInt?
var runLoopMode: RunLoop.Mode = .common
var pausable: Bool = true
var purgeable: Bool = false
var playbackRate: Double = 1.0

/// Create a web image with url, placeholder, custom options and context.
/// - Parameter url: The image url
Expand All @@ -57,6 +52,7 @@ public struct WebImage : View {
}
}
self.imageManager = ImageManager(url: url, options: options, context: context)
self.imagePlayer = ImagePlayer()
}

public var body: some View {
Expand All @@ -67,30 +63,30 @@ public struct WebImage : View {
return Group {
if imageManager.image != nil {
if isAnimating && !imageManager.isIncremental {
if currentFrame != nil {
configure(image: currentFrame!)
if imagePlayer.currentFrame != nil {
configure(image: imagePlayer.currentFrame!)
.onAppear {
self.imagePlayer?.startPlaying()
imagePlayer.startPlaying()
}
.onDisappear {
if self.pausable {
self.imagePlayer?.pausePlaying()
imagePlayer.pausePlaying()
} else {
self.imagePlayer?.stopPlaying()
imagePlayer.stopPlaying()
}
if self.purgeable {
self.imagePlayer?.clearFrameBuffer()
imagePlayer.clearFrameBuffer()
}
}
} else {
configure(image: imageManager.image!)
.onReceive(imageManager.$image) { image in
self.setupPlayer(image: image)
imagePlayer.setupPlayer(image: image)
}
}
} else {
if currentFrame != nil {
configure(image: currentFrame!)
if imagePlayer.currentFrame != nil {
configure(image: imagePlayer.currentFrame!)
} else {
configure(image: imageManager.image!)
}
Expand Down Expand Up @@ -185,32 +181,6 @@ public struct WebImage : View {
return AnyView(configure(image: .empty))
}
}

/// Animated Image Support
func setupPlayer(image: PlatformImage?) {
if imagePlayer != nil {
return
}
if let animatedImage = image as? SDAnimatedImageProvider {
if let imagePlayer = SDAnimatedImagePlayer(provider: animatedImage) {
imagePlayer.animationFrameHandler = { (_, frame) in
self.currentFrame = frame
Copy link
Collaborator Author

@dreampiggy dreampiggy Feb 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here.

From Swift, the struct will not cause retain cycle. However, SwiftUI @State will keep an StorageLocation with an global mapping, which bind each View struct with the @State reference type. (Yes, each View struct will keep an reference)

So, if we write here, the retain cycle is:

View StorageLocation -> SDAnimatedImagePlayer -> animationFrameHandler -> View StorageLocation

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By break off to use ImageManager a pure ObservableObject, this retain cycle can be avoided.

}
// Setup configuration
if let maxBufferSize = maxBufferSize {
imagePlayer.maxBufferSize = maxBufferSize
}
if let customLoopCount = customLoopCount {
imagePlayer.totalLoopCount = UInt(customLoopCount)
}
imagePlayer.runLoopMode = runLoopMode
imagePlayer.playbackRate = playbackRate

self.imagePlayer = imagePlayer
imagePlayer.startPlaying()
}
}
}
}

// Layout
Expand Down Expand Up @@ -372,9 +342,8 @@ extension WebImage {
/// - Note: Pass nil to disable customization, use the image itself loop count (`animatedImageLoopCount`) instead
/// - Parameter loopCount: The animation loop count
public func customLoopCount(_ loopCount: UInt?) -> WebImage {
var result = self
result.customLoopCount = loopCount
return result
self.imagePlayer.customLoopCount = loopCount
return self
}

/// 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.
Expand All @@ -384,19 +353,17 @@ extension WebImage {
/// `UInt.max` means cache all the buffer. (Lowest CPU and Highest Memory)
/// - Parameter bufferSize: The max buffer size
public func maxBufferSize(_ bufferSize: UInt?) -> WebImage {
var result = self
result.maxBufferSize = bufferSize
return result
self.imagePlayer.maxBufferSize = bufferSize
return self
}

/// The runLoopMode when animation is playing on. Defaults is `.common`
/// You can specify a runloop mode to let it rendering.
/// - 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)
/// - Parameter runLoopMode: The runLoopMode for animation
public func runLoopMode(_ runLoopMode: RunLoop.Mode) -> WebImage {
var result = self
result.runLoopMode = runLoopMode
return result
self.imagePlayer.runLoopMode = runLoopMode
return self
}

/// 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.
Expand Down Expand Up @@ -425,9 +392,8 @@ extension WebImage {
/// `< 0.0` is not supported currently and stop animation. (may support reverse playback in the future)
/// - Parameter playbackRate: The animation playback rate.
public func playbackRate(_ playbackRate: Double) -> WebImage {
var result = self
result.playbackRate = playbackRate
return result
self.imagePlayer.playbackRate = playbackRate
return self
}
}

Expand Down