【问题标题】:How to execute a lock that waits until an animation completes?如何执行等待动画完成的锁?
【发布时间】:2013-08-24 20:05:15
【问题描述】:

我正在实现一个通过用户输入动画的健康栏。

这些动画使其上升或下降一定量(例如 50 个单位),并且是按下按钮的结果。有两个按钮。增加和减少。

我想在健康栏上执行一个锁,这样一次只有一个线程可以更改它。问题是我遇到了死锁。

我猜是因为一个单独的线程运行一个由另一个线程持有的锁。但是当动画完成时,那个锁就会让位。如何实现一个在 [UIView AnimateWithDuration] 完成时结束的锁?

我想知道NSConditionLock 是否可行,但我想尽可能使用 NSLocks 以避免不必要的复杂性。你有什么推荐的?

(最终我想让动画“排队”,同时让用户输入继续,但现在我只想让锁工作,即使它首先阻止输入。)

(嗯,想想看,同一个 UIView 一次只有一个 [UIView AnimateWithDuration] 运行。第二次调用将中断第一次调用,导致完成处理程序立即为第一次运行。也许是第二次锁定在第一个有机会解锁之前运行。在这种情况下处理锁定的最佳方法是什么?也许我应该重新访问 Grand Central Dispatch,但我想看看是否有更简单的方法。)

在 ViewController.h 我声明:

NSLock *_lock;

在 ViewController.m 我有:

在加载视图中:

_lock = [[NSLock alloc] init];

ViewController.m 的其余部分(相关部分):

-(void)tryTheLockWithStr:(NSString *)str
{
    LLog(@"\n");
    LLog(@" tryTheLock %@..", str);

    if ([_lock tryLock] == NO)
    {
         NSLog(@"LOCKED.");
    }
          else
    {
          NSLog(@"free.");
          [_lock unlock];
    }
}

// TOUCH DECREASE BUTTON
-(void)touchThreadButton1
{
    LLog(@" touchThreadButton1..");

    [self tryTheLockWithStr:@"beforeLock"];
    [_lock lock];
    [self tryTheLockWithStr:@"afterLock"];

    int changeAmtInt = ((-1) * FILLBAR_CHANGE_AMT); 
    [self updateFillBar1Value:changeAmtInt];

    [UIView animateWithDuration:1.0
                      delay:0.0
                    options:(UIViewAnimationOptionTransitionNone|UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionAllowUserInteraction)
                 animations:
     ^{

         LLog(@" BEGIN animationBlock - val: %d", self.fillBar1Value)
         self.fillBar1.frame = CGRectMake(FILLBAR_1_X_ORIGIN,FILLBAR_1_Y_ORIGIN, self.fillBar1Value,30);
     }
     completion:^(BOOL finished)
     {
         LLog(@" END animationBlock - val: %d - finished: %@", self.fillBar1Value, (finished ? @"YES" : @"NO"));

         [self tryTheLockWithStr:@"beforeUnlock"];
         [_lock unlock];
         [self tryTheLockWithStr:@"afterUnlock"];         
     }
     ];
}

-(void)updateFillBar1Value:(int)changeAmt
{
    self.prevFillBar1Value = self.fillBar1Value;

    self.fillBar1Value += changeAmt;

    if (self.fillBar1Value < FILLBAR_MIN_VALUE)
    {
        self.fillBar1Value = FILLBAR_MIN_VALUE;
    }
    else if (self.fillBar1Value > FILLBAR_MAX_VALUE)
    {
        self.fillBar1Value = FILLBAR_MAX_VALUE;
    }
}

输出:

复制说明:点击“减少”一次

touchThreadButton1..

tryTheLock beforeLock.. 免费。

tryTheLock afterLock.. 锁定。 开始动画块 - val: 250 结束动画块 - val:250 - 完成:是

在解锁之前尝试TheLock.. 已锁定。

在解锁后尝试锁定.. 免费。

结论:这符合预期。

--

输出:

重现说明:快速点击“减少”两次(中断初始动画)..

touchThreadButton1..

tryTheLock beforeLock.. 免费。

tryTheLock afterLock.. 锁定。 开始动画块 - val: 250 touchThreadButton1..

tryTheLock beforeLock.. 锁定。 * -[NSLock lock]: 死锁 ('(null)') * 中断 _NSLockError() 以进行调试。

结论。死锁错误。用户输入被冻结。

【问题讨论】:

    标签: ios multithreading nslock animatewithduration


    【解决方案1】:

    在底部,在我的原始答案中,我描述了一种实现所请求功能的方法(如果您在前一个动画仍在进行中时启动一个动画,则将此后续动画排队等待仅在当前动画完成后启动)。

    虽然出于历史目的我会保留它,但我可能想提出一种完全不同的方法。具体来说,如果您点击应该产生动画的按钮,但之前的动画仍在进行中,我建议您删除旧动画并立即开始新动画,但这样做的方式是让新动画动画从当前动画停止的地方开始。

    1. 在 iOS 8 之前的 iOS 版本中,挑战在于,如果您在另一个动画正在进行时开始一个新动画,操作系统会尴尬地立即跳转到当前动画将结束的位置并从那里开始新动画。

      8 之前的 iOS 版本中的典型解决方案是:

      • 抓取动画视图的presentationLayer(这是UIViewCALayer的当前状态...如果您在动画进行时查看UIView,您'会看到最终值,我们需要抓取当前状态);

      • presentationLayer中获取动画属性值的当前值;

      • 移除动画;

      • 将动画属性重置为“当前”值(这样它就不会在开始下一个动画之前跳到上一个动画的结尾);

      • 将动画初始化为“新”值;

      因此,例如,如果您正在为可能正在进行的动画更改 frame 设置动画,您可能会执行以下操作:

      CALayer *presentationLayer = animatedView.layer.presentationLayer;
      CGRect currentFrame = presentationLayer.frame;
      [animatedView.layer removeAllAnimations];
      animatedView.frame = currentFrame;
      [UIView animateWithDuration:1.0 animations:^{
          animatedView.frame = newFrame;
      }];
      

      这完全消除了在“当前”动画(和其他排队的动画)完成后排队“下一个”动画以运行相关的所有尴尬。您最终也会获得响应速度更快的 UI(例如,您不必等待先前的动画完成,然后再开始用户所需的动画)。

    2. 在 iOS 8 中,这个过程非常简单,如果你启动一个新动画,它通常不仅会从动画属性的当前值开始动画,而且还会确定该动画的速度。此当前动画属性正在发生变化,从而导致旧动画和新动画之间的无缝过渡。

      有关此 iOS 8 新功能的更多信息,我建议您参考 WWDC 2014 视频Building Interruptible and Responsive Interactions

    为了完整起见,我将在下面保留我的原始答案,因为它试图精确解决问题中概述的功能(只是使用不同的机制来确保主队列不被阻塞)。但我真的建议考虑停止当前动画并以这样一种方式开始新动画,即它从任何正在进行的动画可能停止的地方开始。


    原答案:

    我不建议将动画包装在 NSLock(或信号量,或任何其他类似机制)中,因为这会导致阻塞主线程。你永远不想阻塞主线程。我认为您对使用串行队列调整大小操作的直觉很有希望。你可能想要一个“调整大小”的操作:

    • 在主队列上启动 UIView 动画(所有 UI 更新必须在主队列上进行);和

    • 在动画的完成块中,完成操作(我们在此之前不会完成操作,以确保其他排队的操作在完成之前不会启动)。

    我可能会建议调整大小操作:

    大小操作.h:

    @interface SizeOperation : NSOperation
    
    @property (nonatomic) CGFloat sizeChange;
    @property (nonatomic, weak) UIView *view;
    
    - (id)initWithSizeChange:(NSInteger)change view:(UIView *)view;
    
    @end
    

    SizingOperation.m:

    #import "SizeOperation.h"
    
    @interface SizeOperation ()
    
    @property (nonatomic, readwrite, getter = isFinished)  BOOL finished;
    @property (nonatomic, readwrite, getter = isExecuting) BOOL executing;
    
    @end
    
    @implementation SizeOperation
    
    @synthesize finished = _finished;
    @synthesize executing = _executing;
    
    - (id)initWithSizeChange:(NSInteger)change view:(UIView *)view
    {
        self = [super init];
        if (self) {
            _sizeChange = change;
            _view = view;
        }
        return self;
    }
    
    - (void)start
    {
        if ([self isCancelled] || self.view == nil) {
            self.finished = YES;
            return;
        }
    
        self.executing = YES;
    
        // note, UI updates *must* take place on the main queue, but in the completion
        // block, we'll terminate this particular operation
    
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            [UIView animateWithDuration:2.0 delay:0.0 options:kNilOptions animations:^{
                CGRect frame = self.view.frame;
                frame.size.width += self.sizeChange;
                self.view.frame = frame;
            } completion:^(BOOL finished) {
                self.finished = YES;
                self.executing = NO;
            }];
        }];
    }
    
    #pragma mark - NSOperation methods
    
    - (void)setExecuting:(BOOL)executing
    {
        [self willChangeValueForKey:@"isExecuting"];
        _executing = executing;
        [self didChangeValueForKey:@"isExecuting"];
    }
    
    - (void)setFinished:(BOOL)finished
    {
        [self willChangeValueForKey:@"isFinished"];
        _finished = finished;
        [self didChangeValueForKey:@"isFinished"];
    }
    
    @end
    

    然后为这些操作定义一个队列:

    @property (nonatomic, strong) NSOperationQueue *sizeQueue;
    

    确保实例化此队列(作为串行队列):

    self.sizeQueue = [[NSOperationQueue alloc] init];
    self.sizeQueue.maxConcurrentOperationCount = 1;
    

    然后,任何使相关视图增长的东西都会做:

    [self.sizeQueue addOperation:[[SizeOperation alloc] initWithSizeChange:+50.0 view:self.barView]];
    

    任何使相关视图缩小的事情都可以:

    [self.sizeQueue addOperation:[[SizeOperation alloc] initWithSizeChange:-50.0 view:self.barView]];
    

    希望这能说明这个想法。有各种可能的改进:

    • 我让动画变得非常慢,所以我可以轻松地排队一大堆,但你可能会使用更短的值;

    • 如果使用自动布局,您将调整宽度约束的constant 并在动画块中执行layoutIfNeeded) 而不是直接调整框架;和

    • 如果宽度达到某些最大值/最小值,您可能希望添加检查以不执行帧更改。

    但关键是不建议使用锁来控制 UI 更改的动画。除了几毫秒之外,您不想要任何可能阻塞主队列的东西。动画块太长,无法考虑阻塞主队列。所以使用串行操作队列(如果您有多个线程需要启动更改,它们都会将一个操作添加到同一个共享操作队列中,从而自动协调从各种不同线程启动的更改)。

    【讨论】:

    • 谢谢先生!我很欣赏您如何以清晰的方式演示 NSOperationQueue。将大小操作封装在自己的类中是相当优雅的。效果很好。
    • @BlackOrchid 仅供参考,我敢肯定你早​​就跳过了这个问题,但我用一种非常简单的方法对其进行了更新。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-22
    • 2013-07-20
    • 1970-01-01
    • 2012-08-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多