如何执行等待动画完成的锁?

时间:2013-08-24 20:05:16

标签: ios multithreading nslock animatewithduration

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

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

我想在健康栏上执行锁定,以便一次只能有一个线程更改它。问题是我陷入了僵局。

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

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

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

(嗯,想到它只有一个[UIView AnimateWithDuration]一次运行同一个UIView。第二个调用将中断第一个,导致完成处理程序立即运行第一个。也许第二个锁在第一个有机会解锁之前运行。在这种情况下处理锁定的最佳方法是什么?也许我应该重新审视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 ..

tryTheLock beforeLock .. 免费。

tryTheLock afterLock .. 锁定。 BEGIN animationBlock - val:250 END animationBlock - val:250 - 完成:是

tryTheLock beforeUnlock .. 锁定。

tryTheLock afterUnlock .. 免费。

结论:这可以按预期工作。

-

输出:

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

touchThreadButton1 ..

tryTheLock beforeLock .. 免费。

tryTheLock afterLock .. 锁定。 BEGIN animationBlock - val:250 touchThreadButton1 ..

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

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

1 个答案:

答案 0 :(得分:10)

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

虽然我会将其保留用于历史目的,但我可能想要提出一种完全不同的方法。具体来说,如果你点击一个应该导致动画的按钮,但是前一个动画仍在进行中,我建议你删除旧动画并立即启动新动画,但这样做是为了新动画从当前离开的地方开始。

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

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

    • 抓取动画视图的presentationLayer(这是CALayer的{​​{1}}的当前状态...如果您查看UIView当动画正在进行中时,您将看到最终值,我们需要获取当前状态);

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

    • 删除动画;

    • 将动画属性重置为&#34;当前&#34;值(因此在开始下一个动画之前它似乎没有跳到前一个动画的结尾);

    • 将动画发送到&#34; new&#34;值;

    因此,例如,如果您要动画可能正在进行动画的presentationLayer的更改动画,则可能会执行以下操作:

    frame

    这完全消除了排队&#34; next&#34;的所有尴尬。动画在&#34;当前&#34;之后运行动画(和其他排队的动画)完成。您最终会得到响应更快的用户界面(例如,您不必在用户所需的动画开始之前等待之前的动画完成)。

  2. 在iOS 8中,这个过程非常容易,如果您启动新动画,它通常会从动画属性的当前值开始动画,但也会识别速度此当前动画属性正在改变,导致旧动画和新动画之间的无缝转换。

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

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


    原始回答:

    我不建议在CALayer *presentationLayer = animatedView.layer.presentationLayer; CGRect currentFrame = presentationLayer.frame; [animatedView.layer removeAllAnimations]; animatedView.frame = currentFrame; [UIView animateWithDuration:1.0 animations:^{ animatedView.frame = newFrame; }]; (或信号量或任何其他类似机制)中包装动画,因为这会导致阻塞主线程。你永远不想阻止主线程。我认为你对使用串行队列调整操作大小的直觉很有希望。你可能想要一个&#34;调整大小&#34;操作:

    • 在主队列上启动NSLock动画(所有UI更新必须在主队列上发生);以及

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

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

    SizeOperation.h:

    UIView

    SizingOperation.m:

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

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

    #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更改的动画是不可取的。除了几毫秒之外,你不想要任何可以阻止主队列的东西。动画块太长,无法考虑阻止主队列。因此,使用串行操作队列(如果您有多个需要启动更改的线程,他们只需将操作添加到同一个共享操作队列,从而自动协调从各种不同线程发起的更改)。 / p>