有没有办法让drawRect现在正常工作?

Fat*_*tie 44 iphone cocoa quartz-graphics runloop ios

原来的问题...............................................

如果您是drawRect的高级用户,您将知道当然"drawRect"在"所有处理完成"之前不会实际运行.

setNeedsDisplay将视图标记为无效和操作系统,并且基本上等待所有处理完成.在您想要拥有的常见情况下,这可能会令人愤怒:

  • 视图控制器1
  • 开始一些功能2
  • 逐渐增加3
    • 创造了一个越来越复杂的艺术品和4
    • 在每一步,你setNeedsDisplay(错!)5
  • 直到所有工作完成6

当然,当你执行上面的1-6时,所有发生的事情是drawRect 在步骤6之后运行一次.

您的目标是在第5点刷新视图.怎么办?


解决原始问题............................................. .

总之,您可以(A)背景大画,并调用前景进行UI更新或(B)可争议地有四种"即时"方法建议不使用后台进程.为了起作用的结果,运行演示程序.它有所有五种方法的#defines.


Tom Swift介绍的真正令人震惊的替代解决方案..................

汤姆斯威夫特解释了非常简单地操纵运行循环的惊人想法.以下是触发运行循环的方法:

[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]];

这是一个真正令人惊叹的工程.当然,在操作运行循环时应​​该非常小心,并且许多人指出这种方法严格适用于专家.


引起的奇怪问题............................................. .

尽管许多方法都有效,但实际上并没有"工作",因为在演示中你会看到一个奇怪的渐进式减速神器.

滚动到我在下面粘贴的"答案",显示控制台输出 - 您可以看到它逐渐减慢的速度.

这是新的SO问题:
运行循环/ drawRect中的神秘"渐进式减速"问题

这是演示应用程序的V2 ...
http://www.fileswap.com/dl/p8lU3gAi/stepwiseDrawingV2.zip.html

你会看到它测试所有五种方法,

#ifdef TOMSWIFTMETHOD
 [self setNeedsDisplay];
 [[NSRunLoop currentRunLoop]
      runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]];
#endif
#ifdef HOTPAW
 [self setNeedsDisplay];
 [CATransaction flush];
#endif
#ifdef LLOYDMETHOD
 [CATransaction begin];
 [self setNeedsDisplay];
 [CATransaction commit];
#endif
#ifdef DDLONG
 [self setNeedsDisplay];
 [[self layer] displayIfNeeded];
#endif
#ifdef BACKGROUNDMETHOD
 // here, the painting is being done in the bg, we have been
 // called here in the foreground to inval
 [self setNeedsDisplay];
#endif
Run Code Online (Sandbox Code Playgroud)
  • 您可以自己查看哪些方法有效,哪些方法无效.

  • 你可以看到奇怪的"渐进式减速".为什么会这样?

  • 你可以看到有争议的TOMSWIFT方法,实际上对响应性没有任何问题.随时点击回复.(但仍然是奇怪的"渐进式减速"问题)

所以压倒性的是这种奇怪的"渐进式减速":在每次迭代中,由于未知原因,循环所用的时间减少了.请注意,这适用于"正确"(背景外观)或使用"立即"方法之一.


实用的解决方案........................

对于将来阅读的任何人来说,如果你因为"神秘的渐进式减速"而无法让它在生产代码中工作...... Felz和Void在其他具体问题中都提出了惊人的解决方案,希望它有所帮助.

Tom*_*ift 37

如果我正确理解你的问题,那么就有一个简单的解决方案.在长时间运行的例程中,您需要告诉当前的runloop在您自己的处理中的某些点处理一次迭代(或更多,runloop).例如,当您想要更新显示时.具有脏更新区域的任何视图将在运行runloop时调用其drawRect:方法.

告诉当前的runloop处理一次迭代(然后返回给你......):

[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];
Run Code Online (Sandbox Code Playgroud)

下面是一个(低效)长时间运行例程的示例,其中包含相应的drawRect - 每个例程都在自定义UIView的上下文中:

- (void) longRunningRoutine:(id)sender
{
    srand( time( NULL ) );

    CGFloat x = 0;
    CGFloat y = 0;

    [_path moveToPoint: CGPointMake(0, 0)];

    for ( int j = 0 ; j < 1000 ; j++ )
    {
        x = 0;
        y = (CGFloat)(rand() % (int)self.bounds.size.height);

        [_path addLineToPoint: CGPointMake( x, y)];

        y = 0;
        x = (CGFloat)(rand() % (int)self.bounds.size.width);

        [_path addLineToPoint: CGPointMake( x, y)];

        x = self.bounds.size.width;
        y = (CGFloat)(rand() % (int)self.bounds.size.height);

        [_path addLineToPoint: CGPointMake( x, y)];

        y = self.bounds.size.height;
        x = (CGFloat)(rand() % (int)self.bounds.size.width);

        [_path addLineToPoint: CGPointMake( x, y)];

        [self setNeedsDisplay];
        [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];
    }

    [_path removeAllPoints];
}

- (void) drawRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    CGContextSetFillColorWithColor( ctx, [UIColor blueColor].CGColor );

    CGContextFillRect( ctx,  rect);

    CGContextSetStrokeColorWithColor( ctx, [UIColor whiteColor].CGColor );

    [_path stroke];
}
Run Code Online (Sandbox Code Playgroud)

这是一个完整的工作样本,展示了这种技术.

通过一些调整,你可以调整它,以使其余的UI(即用户输入)响应.

更新(使用此技术的警告)

我只想说,我同意其他人的大部分反馈,说这个解决方案(调用runMode:强制调用drawRect :)不一定是个好主意.我已经回答了这个问题,我认为这是一个事实的"这里是如何"回答所陈述的问题,我并不打算将其作为"正确的"架构来推广.另外,我不是说可能没有其他(更好的?)方法来达到同样的效果 - 当然可能还有其他方法我不知道.

更新(回应Joe的示例代码和性能问题)

您所看到的性能下降是在绘图代码的每次迭代中运行runloop的开销,其中包括将图层渲染到屏幕以及runloop执行的所有其他处理(如输入收集和处理).

一种选择可能是不太频繁地调用runloop.

另一种选择可能是优化您的绘图代码.就目前而言(我不知道这是你的实际应用程序,还是只是你的样本...),你可以采取一些措施来加快速度.我要做的第一件事就是将所有UIGraphicsGet/Save/Restore代码移到循环之外.

但从架构的角度来看,我强烈建议考虑一下这里提到的其他方法.我没有理由不能将你的绘图结构化为后台线程(算法未更改),并使用计时器或其他机制来指示主线程在某个频率上更新它的UI,直到绘图完成.我想大多数参与讨论的人都会同意这是"正确的"方法.

  • @ hotpaw2我不推荐使用这个.我正在回答这个问题.我不认为值得投票.同意,有更好的方法来设计应用程序,放下手.问题是"它是否可能",是的,它是. (4认同)
  • 运行一次运行循环只提交通过更改视图和底层启动的当前隐式CATransaction,您可以直接使用显式CATransaction来完成相同的操作而不涉及运行循环. (4认同)

Bra*_*son 27

用户界面的更新发生在当前通过运行循环的末尾.这些更新是在主线程上执行的,因此在主线程中运行很长时间的任何事情(冗长的计算等)都会阻止启动接口更新.此外,在主线程上运行一段时间的任何内容也会导致您的触摸处理无响应.

这意味着无法从主线程上运行的进程中的某个其他点"强制"UI刷新. 汤姆的答案显示,之前的陈述并不完全正确.您可以允许运行循环在主线程上执行的操作过程中完成.但是,这仍然可能会降低应用程序的响应速度.

通常,建议您将需要一段时间才能执行的任何内容移动到后台线程,以便用户界面可以保持响应.但是,您希望对UI执行的任何更新都需要在主线程上完成.

也许在Snow Leopard和iOS 4.0+下最简单的方法是使用块,如下面的基本示例:

dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
    // Do some work
    dispatch_async(main_queue, ^{
        // Update the UI
    });
});
Run Code Online (Sandbox Code Playgroud)

Do some work上面的部分可能是一个冗长的计算,或循环多个值的操作.在此示例中,UI仅在操作结束时更新,但如果您希望在UI中进行持续进度跟踪,则可以将调度放置到需要执行UI更新的主队列中.

对于较旧的OS版本,您可以手动或通过NSOperation中断后台线程.对于手动背景线程,您可以使用

[NSThread detachNewThreadSelector:@selector(doWork) toTarget:self withObject:nil];
Run Code Online (Sandbox Code Playgroud)

要么

[self performSelectorInBackground:@selector(doWork) withObject:nil];
Run Code Online (Sandbox Code Playgroud)

然后更新您可以使用的UI

[self performSelectorOnMainThread:@selector(updateProgress) withObject:nil waitUntilDone:NO];
Run Code Online (Sandbox Code Playgroud)

请注意,我在上一个方法中找到了NO参数,以便在处理连续进度条时获得持续的UI更新.

我为我的类创建的示例应用程序演示了如何使用NSOperations和队列执行后台工作,然后在完成后更新UI.此外,我的Molecules应用程序使用后台线程来处理新结构,状态栏随着此进展而更新.您可以下载源代码以了解我是如何实现这一目标的.


Chr*_*oyd 10

你可以在一个循环中重复这样做,它可以正常工作,没有线程,没有与runloop搞乱等等.

[CATransaction begin];
// modify view or views
[view setNeedsDisplay];
[CATransaction commit];
Run Code Online (Sandbox Code Playgroud)

如果在循环之前已存在隐式事务,则需要在[CATransaction commit]之前提交该事务,然后才能生效.

  • 如果我在setNeedsDisplay之后执行CATransaction刷新,而不是begin + commit,它对我有效(调用视图的drawRect). (2认同)

hot*_*aw2 9

为了让drawRect被称为最快(不一定立即,因为操作系统可能仍会等到,例如,下一次硬件显示刷新等),应用程序应该尽快使用它的UI运行循环,通过退出UI线程中的任何和所有方法,并且非零时间.

您可以在主线程中执行此操作,方法是将任何超过动画帧时间的处理切换为较短的块并仅在短暂延迟后调度继续工作(因此drawRect可能在间隙中运行),或者通过在后台线程,定期调用performSelectorOnMainThread以一些合理的动画帧速率执行setNeedsDisplay.

用于立即更新显示的非OpenGL方法(这意味着在下一次硬件显示刷新或刷新三次)是通过使用您绘制的图像或CGBitmap交换可见的CALayer内容.一个应用程序几乎可以随时将Quartz绘制成Core Graphics位图.

新增答案:

请参阅下面的Brad Larson的评论和Christopher Lloyd对此处另一个答案的评论作为导致此解决方案的暗示.

[ CATransaction flush ];
Run Code Online (Sandbox Code Playgroud)

将导致在已经完成setNeedsDisplay请求的视图上调用drawRect,即使从阻止UI运行循环的方法内部完成刷新也是如此.

请注意,在阻止UI线程时,还需要Core Animation flush来更新更改的CALayer内容.因此,为了使图形内容动画化以显示进度,这些可能最终都是同一事物的形式.

上面新添加的答案的新增注释:

不要比drawRect或动画绘图更快地冲洗,因为这可能会使刷新排队,从而导致奇怪的动画效果.

  • 根据我的经验,如果在更改内容后执行"[CATransaction flush]",则仅在CALayer中交换内容时会立即更新.但是,这可能会导致界面中的其他部分出现伪影. (2认同)