为什么不在动画块中使用弱模式? [英] Why not using weak pattern in animation blocks?

查看:31
本文介绍了为什么不在动画块中使用弱模式?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我知道已经有几个关于 weak 在动画上下文中被问到的问题,比如 是否有必要在 UIView.animateWithDuration(...) 的闭包中使用 unowned self? 虽然在后一种情况下很明显,但你可以省略 weak,我还是有困难,看看原因,为什么我不应该在 Robs 回答 关于旋转视图.我不想打扰那里的评论,所以我在这里提问.

有问题的代码是

private func createAnimation() {animator = UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 4, delay: 0, options: .curveLinear) { [self] inUIView.animateKeyframes(withDuration: 4, delay: 0) {UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1.0/3.0) {animationView.transform = .init(rotationAngle: .pi * 2 * 1/3)}...}} 完成:{ [弱自我] _ inself?.createAnimation()}}

Rob 在 completion-闭包中使用了 [weak self],但在 animations-闭包中没有使用,他实际上将 self 进入捕获列表,使他的意图显而易见.我怎么知道 UIViewPropertyAnimator.runningPropertyAnimator 永远不会将(转义)animations-closure 放入创建的 animator-instance-variable 中?

我不认为,UIViewPropertyAnimator.runningPropertyAnimator 实际上捕获了 animations-closure,但只要我不知道 UIViewPropertyAnimator.runningPropertyAnimator 已实施,或将在未来实施,我如何确定?

也许这个伪实现可以帮助解释我的意思:

import Foundation类 UIView {var 变换 = CGFloat.zerostatic func animateKeyFrames(animations: () -> Void) {}static func addKeyframe(animations: () -> Void) {}}类 UIViewPropertyAnimator {var 动画:() ->无效 = {}var 完成:(() -> Void)?= {}static func runningPropertyAnimator(animations: @escaping () -> Void,完成:(() -> Void)?) ->UIViewPropertyAnimator {让动画师 = UIViewPropertyAnimator()animator.animations = 动画animator.completion = 完成返回动画师}}类视图控制器{var 动画师:UIViewPropertyAnimator?让动画视图 = UIView()func viewDidLoad() {创建动画()}功能创建动画(){animator = UIViewPropertyAnimator.runningPropertyAnimator(animations: { [weak self] inUIView.animateKeyFrames(动画:{UIView.addKeyframe(动画:{self?.animatedView.transform = .zero})})},完成:{[弱自我]中self?.animatedView.transform = .zero})}去初始化{打印(deinit")}}func createAndRelease() {让 viewController = ViewController()viewController.viewDidLoad()}创建和释放()

animationscompletion-closure 中删除 [weak self] 显然会导致我的伪代码和 deinit 永远不会被调用.

解决方案

基于闭包的动画 API 有两个闭包,动画闭包(在动画开始然后释放时调用)和可选的完成处理程序闭包(在动画完成时调用).

考虑:

class ViewController: UIViewController {var 动画师:UIViewPropertyAnimator?覆盖 func viewDidAppear(_ 动画:布尔){super.viewDidAppear(动画)let subview = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))subview.backgroundColor = .redview.addSubview(子视图)logger.debug(创建动画师")动画师 = .runningPropertyAnimator(withDuration: 1, delay: 1) {logger.debug(动画闭包调用")subview.frame = CGRect(x: 100, y: 200, width: 100, height: 100)}完成:{位置在logger.debug(完成关闭调用")}}}

这将输出:

<前>2021-03-31 16:08:00.384558-0700 MyApp[3837:8759577] [ViewController] 创建动画师2021-03-31 16:08:00.384899-0700 MyApp[3837:8759577] [ViewController] 动画闭包调用2021-03-31 16:08:02.386253-0700 MyApp[3837:8759577] [ViewController] 完成关闭调用

注意上面调试日志中的时间戳:animations 闭包在创建动画师之后立即被调用,并且在 completion 闭包被调用之前.

在您的伪代码示例中,您似乎假设动画师存储并保留对 animations 闭包的引用.它不这样做(也不应该这样做).它运行 animation 闭包来找出正在动画的内容,然后释放它.在动画开始后,它不会保留对该闭包的强引用.

一个更好的伪代码示例可能是:

class UIViewPropertyAnimatorMockup {var 动画:(() -> Void)?var 完成:(() -> Void)?static func runningPropertyAnimator(animations: @escaping () -> Void, completion: (() -> Void)? = nil) ->UIViewPropertyAnimatorMockup {让动画师 = UIViewPropertyAnimatorMockup()animator.animations = 动画animator.completion = 完成动画师运行()返回动画师}功能运行(){开始事务()动画?()结束交易()animations = nil//既然我们已经完成了`animations` 引用,就释放它}func didFinishAnimations() {完成?()completion = nil//释放那个 `completion` 引用,现在我们已经完成了它}}

这显然不是 UIViewPropertyAnimator 正在做的,但要理解的想法是,一旦它调用闭包并完成它,它就会释放它的强引用.这适用于 animations 闭包和 completion 闭包(后者显然稍后发布).


顺便说一句,这是一个很好的模式,可以在自己的基于闭包的代码中遵循.如果您在某些属性中保存了一个闭包,请确保 (a) 使其成为可选;(b) 关闭后立即nil该属性.

这是一种防御性编程模式,如果应用程序开发人员不小心引入了他们不应该拥有的强引用,则可以减轻损害.

I know that there are already several questions asked about weak in the context of animations like Is it necessary to use unowned self in closures of UIView.animateWithDuration(…)? While it is obvious in the latter case, that you can omit weak, I have still difficulties, to see the reason, why I should not use the weak pattern in Robs answer about a rotating view. I do not want to disturb the comments there, so I ask the question here.

The code in question is

private func createAnimation() {
     animator = UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 4, delay: 0, options: .curveLinear) { [self] in
         UIView.animateKeyframes(withDuration: 4, delay: 0) {
             UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1.0 / 3.0) {
                 animatedView.transform = .init(rotationAngle: .pi * 2 * 1 / 3)
             }
             ...
         }
     } completion: { [weak self] _ in
         self?.createAnimation()
     }
 }

Rob used the [weak self] in the completion-closure but not in the animations-closure, where he actually put self into the capture list to make his intention obvious. How can I know, that UIViewPropertyAnimator.runningPropertyAnimator will never put the (escaping) animations-closure into the created animator-instance-variable?

I don't think, that UIViewPropertyAnimator.runningPropertyAnimator actually captures the animations-closure, but as long as I have no idea, how UIViewPropertyAnimator.runningPropertyAnimator is implemented, or will be implemented in the future, how can I be sure?

Maybe this pseudo-implementation could help to explain, what I mean:

import Foundation

class UIView {
    var transform = CGFloat.zero
    static func animateKeyFrames(animations: () -> Void) {}
    static func addKeyframe(animations: () -> Void) {}
}

class UIViewPropertyAnimator {
    
    var animations: () -> Void = {}
    var completion: (() -> Void)? = {}
    
    static func runningPropertyAnimator(animations: @escaping () -> Void,
                                        completion: (() -> Void)?) -> UIViewPropertyAnimator {
        let animator = UIViewPropertyAnimator()
        animator.animations = animations
        animator.completion = completion
        
        return animator
    }
}

class ViewController {
    var animator: UIViewPropertyAnimator?
    let animatedView = UIView()
    
    func viewDidLoad() {
        createAnimation()
    }
    
    func createAnimation() {
        animator = UIViewPropertyAnimator.runningPropertyAnimator(animations: { [weak self] in
            UIView.animateKeyFrames(animations: {
                UIView.addKeyframe(animations: {
                    self?.animatedView.transform = .zero
                })
            })
        }, completion: { [weak self] in
            self?.animatedView.transform = .zero
        })
    }
    
    deinit {
        print("deinit")
    }
}

func createAndRelease() {
    let viewController = ViewController()
    viewController.viewDidLoad()
}

createAndRelease()

Removing [weak self] from the animations or completion-closure would obviously cause a retain-cycle in my pseudo-code and deinit would never be called.

解决方案

Closure-based animation API have two closures, the animation closure (which is called when the animation starts and is then released) and, optionally, the completion handler closure (which is called when the animation is done).

Consider:

class ViewController: UIViewController {

    var animator: UIViewPropertyAnimator?

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        let subview = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        subview.backgroundColor = .red
        view.addSubview(subview)

        logger.debug("creating animator")
        animator = .runningPropertyAnimator(withDuration: 1, delay: 1) {
            logger.debug("animate closure called")
            subview.frame = CGRect(x: 100, y: 200, width: 100, height: 100)
        } completion: { position in
            logger.debug("completion closure called")
        }
    }
}

That will output:

2021-03-31 16:08:00.384558-0700 MyApp[3837:8759577] [ViewController] creating animator
2021-03-31 16:08:00.384899-0700 MyApp[3837:8759577] [ViewController] animate closure called
2021-03-31 16:08:02.386253-0700 MyApp[3837:8759577] [ViewController] completion closure called

Note the timestamps in the above debugging log: The animations closure is called immediately after the animator is created, and well before the completion closure is called.

In your pseudocode example, you seem to be assuming that the animator stores and retains a reference to the animations closure. It does not do that (and nor should it). It runs the animation closure to figure out what is being animated and then releases it. It does not keep a strong reference to that closure after the animation begins.

A better pseudocode example might be:

class UIViewPropertyAnimatorMockup {
    var animations: (() -> Void)?
    var completion: (() -> Void)?

    static func runningPropertyAnimator(animations: @escaping () -> Void, completion: (() -> Void)? = nil) -> UIViewPropertyAnimatorMockup {
        let animator = UIViewPropertyAnimatorMockup()
        animator.animations = animations
        animator.completion = completion
        animator.run()
        return animator
    }

    func run() {
        beginTransaction()
        animations?()
        endTransaction()

        animations = nil         // release that `animations` reference now that we're done with it
    }

    func didFinishAnimations() {
        completion?()
        completion = nil         // release that `completion` reference now that we're done with it
    }
}

This obviously is not precisely what UIViewPropertyAnimator is doing, but the idea to understand is that as soon as it calls the closure and is done with it, it releases its strong reference. This applies to both the animations closure and the completion closure (the latter obviously being released a bit later).


As an aside, this is a good pattern to follow in one’s own closure-based code. If you save a closure in some property, make sure to (a) make it optional; and (b) nil that property as soon as you are done with the closure.

This is a defensive programming pattern, mitigating damage if an application developer accidentally introduces a strong reference that they should not have.

这篇关于为什么不在动画块中使用弱模式?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆