Fix the leak of WebImage with animation and NavigationLink. The leak is because of @State which may cause reference cycle

This commit is contained in:
DreamPiggy 2021-02-22 19:33:27 +08:00
parent 4c7f169e39
commit 8b1dfc01cf
2 changed files with 105 additions and 53 deletions

View File

@ -0,0 +1,86 @@
/*
* This file is part of the SDWebImage package.
* (c) DreamPiggy <lizhuoli1126@126.com>
*
* 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
let posterFrame = PlatformImage(cgImage: animatedImage.cgImage!, scale: animatedImage.scale, orientation: .up)
currentFrame = posterFrame
}
}
}
}

View File

@ -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
@ -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 {
@ -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!)
}
@ -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
}
// 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
@ -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.
@ -384,9 +353,8 @@ 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`
@ -394,9 +362,8 @@ extension WebImage {
/// - 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.
@ -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
}
}