如何执行一个锁,等待动画完成?

7
我正在实现一个健康条,通过用户输入进行动画。这些动画使其上下移动一定量(比如50个单位),并且是按钮按下的结果。有两个按钮,分别是增加和减少。
我想对健康条执行锁定,以便只有一个线程可以同时更改它。问题是我遇到了死锁。
我猜测是因为另一个线程运行了由另一个线程持有的锁。但是当动画完成时,该锁将让位。如何实现在 [UIView animateWithDuration] 完成时结束锁定?
我想知道是否可以使用 NSLocks 来避免不必要的复杂性,但我不确定是否应该使用 NSConditionLock。你有什么建议吗?
(最终,我希望在让用户输入继续的同时“排队”播放动画,但现在我只想让锁起作用,即使它会先阻止输入。)
(嗯,想想看,对于同一个 UIView,每次只有一个 [UIView animateWithDuration] 在运行。第二个调用将中断第一个,导致第一个立即运行完成处理程序。也许第二个锁在第一个解锁之前就运行了。在这种情况下,处理锁定的最佳方法是什么?也许我应该重新考虑 Grand Central Dispatch,但我想看看是否有更简单的方法。)
在 ViewController.h 中,我声明:
NSLock *_lock;

在ViewController.m中,我有:
在loadView中:
_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..

在锁定之前尝试锁定.. 空闲。

在锁定之后尝试锁定.. 已锁定。 开始动画块 - val: 250 结束动画块 - val: 250 - 完成: 是

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

在解锁之后尝试解锁.. 空闲。

结论: 这符合预期。

--

输出:

重现步骤: 快速点击两次“减少”按钮(中断初始动画)..

touchThreadButton1..

在锁定之前尝试锁定.. 空闲。

在锁定之后尝试锁定.. 已锁定。 开始动画块 - val: 250 touchThreadButton1..

在锁定之前尝试锁定.. 已锁定。 * -[NSLock lock]: 死锁 ( '(null)') * 在 _NSLockError() 上打断点以进行调试。

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

1个回答

11
在我的原始答案底部,我描述了一种实现所请求功能的方法(如果您在前一个动画仍在进行时启动动画,则将这个后续动画排队,直到当前动画完成后才开始)。
虽然我会保留它以供历史目的,但我可能会建议完全不同的方法。具体来说,如果您点击应该导致动画的按钮,但是先前的动画仍在进行中,我建议您删除旧动画并立即开始新动画,但是以这样的方式进行,使新动画从当前动画离开的地方继续。
  1. In iOS versions prior to iOS 8, the challenge is that if you start a new animation while another is in progress, the OS awkwardly immediately jumps to where the current animation would have ended and starts the new animation from there.

    The typical solution in iOS versions prior to 8 would be to:

    • grab the presentationLayer of the animated view (this is the current state of the CALayer of the UIView ... if you look at the UIView while the animation is in progress, you'll see the final value, and we need to grab the current state);

    • grab the current value of the animated property value from that presentationLayer;

    • remove the animations;

    • reset the animated property to the "current" value (so that it doesn't appear to jump to the end of the prior animation before starting the next animation);

    • initiate the animation to the "new" value;

    So, for example, if you are animating the changing of a frame which might be in progress of an animation, you might do something like:

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

    This completely eliminates all of the awkwardness associated with queuing up the "next" animation to run after the "current" animation (and other queued animations) completes. You end up with a far more responsive UI, too (e.g. you don't have to wait for prior animations to finish before the user's desired animation commences).

  2. In iOS 8, this process is infinitely easier, where it will, if you initiate a new animation, it will often start the animation from not only the current value of the animated property, but will also identify the speed at which that this currently animated property is changing, resulting in a seamless transition between the old animation and the new animation.

    For more information about this new iOS 8 feature, I'd suggest you refer to WWDC 2014 video Building Interruptible and Responsive Interactions.

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

原始答案:

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

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

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

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

SizeOperation.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更改的动画是不可取的。除了几毫秒之外,您不想阻塞主队列的任何事情。动画块太长了,难以想象会阻塞主队列。因此,请使用串行操作队列(如果您有多个需要启动更改的线程,则它们都只会向同一个共享操作队列添加操作,从而自动协调从各种不同线程发起的更改)。


1
谢谢您先生!我很欣赏您以清晰的方式演示NSOperationQueue。将大小操作封装在自己的类中相当优雅。它完美地运行。 - Black Orchid
@BlackOrchid,顺便说一下,我相信你早就解决了这个问题,但是我用一个极其简单的方法对它进行了更新。 - Rob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接