有没有办法让drawRect立即工作?

发布于 2024-10-13 00:53:26 字数 405 浏览 5 评论 0原文

如果您是drawRect的高级用户,您就会知道drawRect当然不会真正运行,直到“所有处理完成”。

setNeedsDisplay 将视图和操作系统标记为无效,并基本上等待所有处理完成。在您希望拥有的常见情况下,这可能会令人恼火:

  • 视图控制器 1
  • 启动某个功能 2
  • ,该功能逐渐增加 3
    • 创作出越来越复杂的艺术作品并且 4
    • 在每一步中,您都设置了NeedsDisplay(错误!)5
  • 直到完成所有工作 6

当然,当您执行上面的 1-6 时,所发生的只是运行了drawRect 第 6 步后仅一次

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

If you are an advanced user of drawRect, you will know that of course drawRect will not actually run until "all processing is finished."

setNeedsDisplay flags a view as invalidated and the OS, and basically waits until all processing is done. This can be infuriating in the common situation where you want to have:

  • a view controller 1
  • starts some function 2
  • which incrementally 3
    • creates a more and more complicated artwork and 4
    • at each step, you setNeedsDisplay (wrong!) 5
  • until all the work is done 6

Of course, when you do the above 1-6, all that happens is that drawRect is run once only after step 6.

Your goal is for the view to be refreshed at point 5. What to do?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

始终不够爱げ你 2024-10-20 00:53:26

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

告诉当前的运行循环处理一次迭代(然后返回给您...):

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

这是一个(低效)长时间运行例程的示例,具有相应的绘制矩形 - 每个例程都在自定义 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];
}

这里是演示此技术的完整工作示例

通过一些调整,您可能可以调整它以使 UI 的其余部分(即用户输入)也能响应。

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

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

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

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

一种选择可能是降低调用运行循环的频率。

另一种选择可能是优化您的绘图代码。就目前情况而言(我不知道这是否是您的实际应用程序,或者只是您的示例......)您可以采取一些措施来使其更快。我要做的第一件事是将所有 UIGraphicsGet/Save/Restore 代码移到循环之外。

然而,从架构的角度来看,我强烈建议考虑这里提到的一些其他方法。我认为没有理由不能将绘图构造为在后台线程上进行(算法不变),并使用计时器或其他机制向主线程发出信号,以某种频率更新其 UI,直到绘图完成。我认为大多数参与讨论的人都会同意这将是“正确”的方法。

If I understand your question correctly, there is a simple solution to this. During your long-running routine you need to tell the current runloop to process for a single iteration (or more, of the runloop) at certain points in your own processing. e.g, when you want to update the display. Any views with dirty update regions will have their drawRect: methods called when you run the runloop.

To tell the current runloop to process for one iteration (and then return to you...):

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

Here's an example of an (inefficient) long running routine with a corresponding drawRect - each in the context of a custom 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];
}

And here is a fully working sample demonstrating this technique.

With some tweaking you can probably adjust this to make the rest of the UI (i.e. user-input) responsive as well.

Update (caveat for using this technique)

I just want to say that I agree with much of the feedback from others here saying this solution (calling runMode: to force a call to drawRect:) isn't necessarily a great idea. I've answered this question with what I feel is a factual "here's how" answer to the stated question, and I am not intending to promote this as "correct" architecture. Also, I'm not saying there might not be other (better?) ways to achieve the same effect - certainly there may be other approaches that I wasn't aware of.

Update (response to the Joe's sample code and performance question)

The performance slowdown you're seeing is the overhead of running the runloop on each iteration of your drawing code, which includes rendering the layer to the screen as well as all of the other processing the runloop does such as input gathering and processing.

One option might be to invoke the runloop less frequently.

Another option might be to optimize your drawing code. As it stands (and I don't know if this is your actual app, or just your sample...) there are a handful of things you could do to make it faster. The first thing I would do is move all the UIGraphicsGet/Save/Restore code outside the loop.

From an architectural standpoint however, I would highly recommend considering some of the other approaches mentioned here. I see no reason why you can't structure your drawing to happen on a background thread (algorithm unchanged), and use a timer or other mechanism to signal the main thread to update it's UI on some frequency until the drawing is complete. I think most of the folks who've participated in the discussion would agree that this would be the "correct" approach.

故人如初 2024-10-20 00:53:26

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

这意味着无法从主线程上运行的进程中的其他点“强制”进行 UI 刷新。 正如 Tom 的回答所示,前面的陈述并不完全正确。您可以允许运行循环在主线程上执行的操作中间完成。但是,这仍然可能会降低应用程序的响应能力。

一般来说,建议您将需要一段时间才能执行的任何操作移至后台线程,以便用户界面可以保持响应。但是,您希望对 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
    });
});

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

对于较旧的操作系统版本,您可以手动或通过 NSOperation 中断后台线程。对于手动后台线程,您可以使用

[NSThread detachNewThreadSelector:@selector(doWork) toTarget:self withObject:nil];

[self performSelectorInBackground:@selector(doWork) withObject:nil];

然后更新您可以使用的 UI

[self performSelectorOnMainThread:@selector(updateProgress) withObject:nil waitUntilDone:NO];

请注意,我发现在处理连续进度条时需要在上一个方法中使用 NO 参数来获取不断的 UI 更新。

这个示例应用程序我为我的班级创建了如何使用 NSOperations 和用于执行后台工作并在完成后更新 UI 的队列。此外,我的 Molecules 应用程序使用后台线程来处理新结构,状态栏会随着进展而更新。你可以下载源代码来看看我是如何实现这一点的。

Updates to the user interface happen at the end of the current pass through the run loop. These updates are performed on the main thread, so anything that runs for a long time in the main thread (lengthy calculations, etc.) will prevent the interface updates from being started. Additionally, anything that runs for a while on the main thread will also cause your touch handling to be unresponsive.

This means that there is no way to "force" a UI refresh to occur from some other point in a process running on the main thread. The previous statement is not entirely correct, as Tom's answer shows. You can allow the run loop to come to completion in the middle of operations performed on the main thread. However, this still may reduce the responsiveness of your application.

In general, it is recommended that you move anything that takes a while to perform to a background thread so that the user interface can remain responsive. However, any updates you wish to perform to the UI need to be done back on the main thread.

Perhaps the easiest way to do this under Snow Leopard and iOS 4.0+ is to use blocks, like in the following rudimentary sample:

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

The Do some work part of the above could be a lengthy calculation, or an operation that loops over multiple values. In this example, the UI is only updated at the end of the operation, but if you wanted continuous progress tracking in your UI, you could place the dispatch to the main queue where ever you needed a UI update to be performed.

For older OS versions, you can break off a background thread manually or through an NSOperation. For manual background threading, you can use

[NSThread detachNewThreadSelector:@selector(doWork) toTarget:self withObject:nil];

or

[self performSelectorInBackground:@selector(doWork) withObject:nil];

and then to update the UI you can use

[self performSelectorOnMainThread:@selector(updateProgress) withObject:nil waitUntilDone:NO];

Note that I've found the NO argument in the previous method to be needed to get constant UI updates while dealing with a continuous progress bar.

This sample application I created for my class illustrates how to use both NSOperations and queues for performing background work and then updating the UI when done. Also, my Molecules application uses background threads for processing new structures, with a status bar that is updated as this progresses. You can download the source code to see how I achieved this.

入画浅相思 2024-10-20 00:53:26

您可以在循环中重复执行此操作,它会正常工作,没有线程,不会扰乱运行循环等。

[CATransaction begin];
// modify view or views
[view setNeedsDisplay];
[CATransaction commit];

如果在循环之前已经存在隐式事务,则需要在之前使用 [CATransaction commit] 提交该事务这会起作用。

You can do this repeatedly in a loop and it'll work fine, no threads, no messing with the runloop, etc.

[CATransaction begin];
// modify view or views
[view setNeedsDisplay];
[CATransaction commit];

If there is an implicit transaction already in place prior to the loop you need to commit that with [CATransaction commit] before this will work.

ˇ宁静的妩媚 2024-10-20 00:53:26

为了让drawRect尽快被调用(这不一定是立即的,因为操作系统可能仍然要等到下一次硬件显示刷新等),应用程序应该尽快空闲它的UI运行循环,通过退出 UI 线程中的任何和所有方法,并且持续非零时间。

您可以在主线程中执行此操作,方法是将任何花费超过动画帧时间的处理切成更短的块,并仅在短暂的延迟后安排继续工作(因此drawRect可能会在间隙中运行),或者通过在后台线程,定期调用 PerformSelectorOnMainThread 以某个合理的动画帧速率执行 setNeedsDisplay。

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

新添加的答案:

请参阅下面 Brad Larson 的评论和 Christopher Lloyd 对此处另一个答案的评论,作为实现此解决方案的提示。

[ CATransaction flush ];

会导致在已完成 setNeedsDisplay 请求的视图上调用 drawRect,即使刷新是从阻塞 UI 运行循环的方法内部完成的。

请注意,当阻塞 UI 线程时,还需要刷新 Core Animation 来更新更改的 CALayer 内容。因此,为了用动画图形内容来显示进度,这些最终可能都是同一事物的形式。

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

刷新速度不要超过您的drawRect或动画绘制可以完成的速度,因为这可能会使刷新排队,导致奇怪的动画效果。

In order to get drawRect called the soonest (which is not necessarily immediately, as the OS may still wait until, for instance, the next hardware display refresh, etc.), an app should idle it's UI run loop as soon as possible, by exiting any and all methods in the UI thread, and for a non-zero amount of time.

You can either do this in the main thread by chopping any processing that takes more than an animation frame time into shorter chunks and scheduling continuing work only after a short delay (so drawRect might run in the gaps), or by doing the processing in a background thread, with a periodic call to performSelectorOnMainThread to do a setNeedsDisplay at some reasonable animation frame rate.

A non-OpenGL method to update the display near immediately (which means at the very next hardware display refresh or three) is by swapping visible CALayer contents with an image or CGBitmap that you have drawn into. An app can do Quartz drawing into a Core Graphics bitmap at pretty much at any time.

New added answer:

Please see Brad Larson's comments below and Christopher Lloyd's comment on another answer here as the hint leading towards this solution.

[ CATransaction flush ];

will cause drawRect to be called on views on which a setNeedsDisplay request has been done, even if the flush is done from inside a method that is blocking the UI run loop.

Note that, when blocking the UI thread, a Core Animation flush is required to update changing CALayer contents as well. So, for animating graphic content to show progress, these may both end up being forms of the same thing.

New added note to new added answer above:

Do not flush faster than your drawRect or animation drawing can complete, as this might queue up flushes, causing weird animation effects.

一枫情书 2024-10-20 00:53:26

无需质疑这样做是否明智(您应该这样做),您可以这样做:

[myView setNeedsDisplay];
[[myView layer] displayIfNeeded];

-setNeedsDisplay 将把视图标记为需要重绘。
-displayIfNeeded 将强制视图的支持层重绘,但前提是它已被标记为需要显示。

然而,我要强调的是,您的问题表明了可以进行一些重新设计的架构。除了极少数情况外,您应该永远不需要或不想强制视图立即重绘。 UIKit 并没有考虑到这个用例,如果它有效,那你就很幸运了。

Without questioning the wisdom of this (which you ought to do), you can do:

[myView setNeedsDisplay];
[[myView layer] displayIfNeeded];

-setNeedsDisplay will mark the view as needing to be redrawn.
-displayIfNeeded will force the view's backing layer to redraw, but only if it has been marked as needing to be displayed.

I will emphasize, however, that your question is indicative of an architecture that could use some re-working. In all but exceptionally rare cases, you should never need to or want to force a view to redraw immediately. UIKit with not built with that use-case in mind, and if it works, consider yourself lucky.

暗地喜欢 2024-10-20 00:53:26

您是否尝试过在辅助线程上进行繁重的处理并回调主线程来安排视图更新? NSOperationQueue 让这种事情变得非常简单。


示例代码采用 NSURL 数组作为输入并异步下载所有这些内容,并在每个内容完成并保存时通知主线程。

- (void)fetchImageWithURLs:(NSArray *)urlArray {
    [self.retriveAvatarQueue cancelAllOperations];
    self.retriveAvatarQueue = nil;

    NSOperationQueue *opQueue = [[NSOperationQueue alloc] init];

    for (NSUInteger i=0; i<[urlArray count]; i++) {
        NSURL *url = [urlArray objectAtIndex:i];

        NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:@selector(cacheImageWithIndex:andURL:)]];
        [inv setTarget:self];
        [inv setSelector:@selector(cacheImageWithIndex:andURL:)];
        [inv setArgument:&i atIndex:2];
        [inv setArgument:&url atIndex:3];

        NSInvocationOperation *invOp = [[NSInvocationOperation alloc] initWithInvocation:inv];
        [opQueue addOperation:invOp];
        [invOp release];
    }

    self.retriveAvatarQueue = opQueue;
    [opQueue release];
}

- (void)cacheImageWithIndex:(NSUInteger)index andURL:(NSURL *)url {
    NSData *imageData = [NSData dataWithContentsOfURL:url];

    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *filePath = PATH_FOR_IMG_AT_INDEX(index);
    NSError *error = nil;

    // Save the file      
    if (![fileManager createFileAtPath:filePath contents:imageData attributes:nil]) {
        DLog(@"Error saving file at %@", filePath);
    }

    // Notifiy the main thread that our file is saved.
    [self performSelectorOnMainThread:@selector(imageLoadedAtPath:) withObject:filePath waitUntilDone:NO];

}

Have you tried doing the heavy processing on a secondary thread and calling back to the main thread to schedule view updates? NSOperationQueue makes this sort of thing pretty easy.


Sample code that takes an array of NSURLs as input and asynchronously downloads them all, notifying the main thread as each of them is finished and saved.

- (void)fetchImageWithURLs:(NSArray *)urlArray {
    [self.retriveAvatarQueue cancelAllOperations];
    self.retriveAvatarQueue = nil;

    NSOperationQueue *opQueue = [[NSOperationQueue alloc] init];

    for (NSUInteger i=0; i<[urlArray count]; i++) {
        NSURL *url = [urlArray objectAtIndex:i];

        NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:@selector(cacheImageWithIndex:andURL:)]];
        [inv setTarget:self];
        [inv setSelector:@selector(cacheImageWithIndex:andURL:)];
        [inv setArgument:&i atIndex:2];
        [inv setArgument:&url atIndex:3];

        NSInvocationOperation *invOp = [[NSInvocationOperation alloc] initWithInvocation:inv];
        [opQueue addOperation:invOp];
        [invOp release];
    }

    self.retriveAvatarQueue = opQueue;
    [opQueue release];
}

- (void)cacheImageWithIndex:(NSUInteger)index andURL:(NSURL *)url {
    NSData *imageData = [NSData dataWithContentsOfURL:url];

    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *filePath = PATH_FOR_IMG_AT_INDEX(index);
    NSError *error = nil;

    // Save the file      
    if (![fileManager createFileAtPath:filePath contents:imageData attributes:nil]) {
        DLog(@"Error saving file at %@", filePath);
    }

    // Notifiy the main thread that our file is saved.
    [self performSelectorOnMainThread:@selector(imageLoadedAtPath:) withObject:filePath waitUntilDone:NO];

}
っ〆星空下的拥抱 2024-10-20 00:53:26

我认为,最完整的答案来自 Jeffrey Sambell 的博客文章 'iOS 中的异步操作与 Grand Central Dispatch' 它对我有用!
它与上述 Brad 提出的解决方案基本相同,但根据 OSX/IOS 并发模型进行了充分解释。

dispatch_get_current_queue函数将返回当前队列
从中调度块和dispatch_get_main_queue
函数将返回您的 UI 运行的主队列。

dispatch_get_main_queue函数对于更新
iOS 应用程序的 UI 作为 UIKit 方法不是线程安全的(有一些
例外),因此您为更新 UI 元素而进行的任何调用都必须始终是
从主队列完成。

典型的 GCD 调用如下所示:

// 在主线程上做一些事情
dispatch_queue_t myQueue =dispatch_queue_create("我的队列",NULL);
dispatch_async(myQueue, ^{

// 执行长时间运行的进程   
dispatch_async(dispatch_get_main_queue(), ^{
    // 更新用户界面   
    }); 
}); 

// 继续做其他事情  
// 进程运行时的主线程。

这是我的工作示例(iOS 6+)。它使用 AVAssetReader 类显示存储视频的帧:

//...prepare the AVAssetReader* asset_reader earlier and start reading frames now:
[asset_reader startReading];

dispatch_queue_t readerQueue = dispatch_queue_create("Reader Queue", NULL);
dispatch_async(readerQueue, ^{
    CMSampleBufferRef buffer;
    while ( [asset_reader status]==AVAssetReaderStatusReading )
    {
        buffer = [asset_reader_output copyNextSampleBuffer];
        if (buffer!=nil)
        {
            //The point is here: to use the main queue for actual UI operations
            dispatch_async(dispatch_get_main_queue(), ^{
                // Update the UI using the AVCaptureVideoDataOutputSampleBufferDelegate style function
                [self captureOutput:nil didOutputSampleBuffer:buffer fromConnection:nil];
                CFRelease (buffer);
            });
        }
    }
});

此示例的第一部分可以在 此处在达米安的回答中。

I think, the most complete answer comes from the Jeffrey Sambell's blog post 'Asynchronous Operations in iOS with Grand Central Dispatch' and it worked for me!
It's basically the same solution as proposed by Brad above but fully explained in terms of OSX/IOS concurrency model.

The dispatch_get_current_queue function will return the current queue
from which the block is dispatched and the dispatch_get_main_queue
function will return the main queue where your UI is running.

The dispatch_get_main_queue function is very useful for updating the
iOS app’s UI as UIKit methods are not thread safe (with a few
exceptions) so any calls you make to update UI elements must always be
done from the main queue.

A typical GCD call would look something like this:

// Doing something on the main thread
dispatch_queue_t myQueue = dispatch_queue_create("My Queue",NULL);
dispatch_async(myQueue, ^{

// Perform long running process   
dispatch_async(dispatch_get_main_queue(), ^{
    // Update the UI   
    }); 
}); 

// Continue doing other stuff on the  
// main thread while process is running.

And here goes my working example (iOS 6+). It displays frames of a stored video using the AVAssetReader class:

//...prepare the AVAssetReader* asset_reader earlier and start reading frames now:
[asset_reader startReading];

dispatch_queue_t readerQueue = dispatch_queue_create("Reader Queue", NULL);
dispatch_async(readerQueue, ^{
    CMSampleBufferRef buffer;
    while ( [asset_reader status]==AVAssetReaderStatusReading )
    {
        buffer = [asset_reader_output copyNextSampleBuffer];
        if (buffer!=nil)
        {
            //The point is here: to use the main queue for actual UI operations
            dispatch_async(dispatch_get_main_queue(), ^{
                // Update the UI using the AVCaptureVideoDataOutputSampleBufferDelegate style function
                [self captureOutput:nil didOutputSampleBuffer:buffer fromConnection:nil];
                CFRelease (buffer);
            });
        }
    }
});

The first part of this sample may be found here in Damian's answer.

も星光 2024-10-20 00:53:26

我想为给定的问题提供一个干净的解决方案。

我同意其他发帖者的观点,在理想情况下,所有繁重的工作都应该在后台线程中完成,但是有时这根本不可能,因为耗时的部分需要大量访问非线程安全方法,例如UIKit 提供的那些。就我而言,初始化 UI 非常耗时,而且我无法在后台运行任何内容,因此我最好的选择是在初始化期间更新进度栏。

然而,一旦我们考虑理想的 GCD 方法,解决方案实际上很简单。我们在后台线程中完成所有工作,将其划分为在主线程上同步调用的卡盘。运行循环将为每个卡盘运行,更新 UI 和任何进度条等。

- (void)myInit
{
    // Start the work in a background thread.
    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        // Back to the main thread for a chunk of code
        dispatch_sync(dispatch_get_main_queue(), ^{
            ...

            // Update progress bar
            self.progressIndicator.progress = ...: 
        });

        // Next chunk
        dispatch_sync(dispatch_get_main_queue(), ^{
            ...

            // Update progress bar
            self.progressIndicator.progress = ...: 
        });

        ...
    });
}

当然,这本质上与 Brad 的技术相同,但他的答案并没有完全解决当前的问题 - 运行大量定期更新 UI 时的非线程安全代码。

I'd like to offer a clean solution to the given problem.

I agree with other posters that in an ideal situation all the heavy lifting should be done in a background thread, however there are times when this simply isn't possible because the time consuming part requires lots of accessing to non thread-safe methods such as those offered by UIKit. In my case, initialising my UI is time consuming and there's nothing I can run in the background, so my best option is to update a progress bar during the init.

However, once we think in terms of the ideal GCD approach, the solution is actually a simple. We do all the work in a background thread, dividing it into chucks that are called synchronously on the main thread. The run loop will be run for each chuck, updating the UI and any progress bars etc.

- (void)myInit
{
    // Start the work in a background thread.
    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        // Back to the main thread for a chunk of code
        dispatch_sync(dispatch_get_main_queue(), ^{
            ...

            // Update progress bar
            self.progressIndicator.progress = ...: 
        });

        // Next chunk
        dispatch_sync(dispatch_get_main_queue(), ^{
            ...

            // Update progress bar
            self.progressIndicator.progress = ...: 
        });

        ...
    });
}

Of course, this is essentially the same as Brad's technique, but his answer doesn't quite address the issue at hand - that of running a lot of non thread safe code while updating the UI periodically.

宣告ˉ结束 2024-10-20 00:53:26

Joe——如果您愿意对其进行设置,以便您的冗长处理全部发生在drawRect内部,您就可以使其工作。我刚刚写了一个测试项目。有用。请参阅下面的代码。

LengthyComputationTestAppDelegate.h:

#import <UIKit/UIKit.h>
@interface LengthyComputationTestAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;

@end

LengthComputationTestAppDelegate.m:

#import "LengthyComputationTestAppDelegate.h"
#import "Incrementer.h"
#import "IncrementerProgressView.h"

@implementation LengthyComputationTestAppDelegate

@synthesize window;


#pragma mark -
#pragma mark Application lifecycle

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    

    // Override point for customization after application launch.
    IncrementerProgressView *ipv = [[IncrementerProgressView alloc]initWithFrame:self.window.bounds];
    [self.window addSubview:ipv];
    [ipv release];
    [self.window makeKeyAndVisible];
    return YES;
}

Incrementer.h:

#import <Foundation/Foundation.h>

//singleton object
@interface Incrementer : NSObject {
    NSUInteger theInteger_;
}

@property (nonatomic) NSUInteger theInteger;

+(Incrementer *) sharedIncrementer;
-(NSUInteger) incrementForTimeInterval: (NSTimeInterval) timeInterval;
-(BOOL) finishedIncrementing;

incrementer.m:

#import "Incrementer.h"

@implementation Incrementer

@synthesize theInteger = theInteger_;

static Incrementer *inc = nil;

-(void) increment {
    theInteger_++;
}

-(BOOL) finishedIncrementing {
    return (theInteger_>=100000000);
}

-(NSUInteger) incrementForTimeInterval: (NSTimeInterval) timeInterval {
    NSTimeInterval negativeTimeInterval = -1*timeInterval;
    NSDate *startDate = [NSDate date];
    while (!([self finishedIncrementing]) && [startDate timeIntervalSinceNow] > negativeTimeInterval)
        [self increment];
    return self.theInteger;
}

-(id) init {
    if (self = [super init]) {
        self.theInteger = 0;
    }
    return self;
}

#pragma mark --
#pragma mark singleton object methods

+ (Incrementer *) sharedIncrementer { 
    @synchronized(self) {
        if (inc == nil) {
            inc = [[Incrementer alloc]init];        
        }
    }
    return inc;
}

+ (id)allocWithZone:(NSZone *)zone {
    @synchronized(self) {
        if (inc == nil) {
            inc = [super allocWithZone:zone];
            return inc;  // assignment and return on first allocation
        }
    }
    return nil; // on subsequent allocation attempts return nil
}

- (id)copyWithZone:(NSZone *)zone
{
    return self;
}

- (id)retain {
    return self;
}

- (unsigned)retainCount {
    return UINT_MAX;  // denotes an object that cannot be released
}

- (void)release {
    //do nothing
}

- (id)autorelease {
    return self;
}

@end

IncrementerProgressView.m:

#import "IncrementerProgressView.h"


@implementation IncrementerProgressView
@synthesize progressLabel = progressLabel_;
@synthesize nextUpdateTimer = nextUpdateTimer_;

-(id) initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame: frame]) {
        progressLabel_ = [[UILabel alloc]initWithFrame:CGRectMake(20, 40, 300, 30)];
        progressLabel_.font = [UIFont systemFontOfSize:26];
        progressLabel_.adjustsFontSizeToFitWidth = YES;
        progressLabel_.textColor = [UIColor blackColor];
        [self addSubview:progressLabel_];
    }
    return self;
}

-(void) drawRect:(CGRect)rect {
    [self.nextUpdateTimer invalidate];
    Incrementer *shared = [Incrementer sharedIncrementer];
    NSUInteger progress = [shared incrementForTimeInterval: 0.1];
    self.progressLabel.text = [NSString stringWithFormat:@"Increments performed: %d", progress];
    if (![shared finishedIncrementing])
        self.nextUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:0. target:self selector:(@selector(setNeedsDisplay)) userInfo:nil repeats:NO];
}

- (void)dealloc {
    [super dealloc];
}

@end

Joe -- if you are willing to set it up so that your lengthy processing all happens inside of drawRect, you can make it work. I just wrote a test project. It works. See code below.

LengthyComputationTestAppDelegate.h:

#import <UIKit/UIKit.h>
@interface LengthyComputationTestAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;

@end

LengthComputationTestAppDelegate.m:

#import "LengthyComputationTestAppDelegate.h"
#import "Incrementer.h"
#import "IncrementerProgressView.h"

@implementation LengthyComputationTestAppDelegate

@synthesize window;


#pragma mark -
#pragma mark Application lifecycle

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    

    // Override point for customization after application launch.
    IncrementerProgressView *ipv = [[IncrementerProgressView alloc]initWithFrame:self.window.bounds];
    [self.window addSubview:ipv];
    [ipv release];
    [self.window makeKeyAndVisible];
    return YES;
}

Incrementer.h:

#import <Foundation/Foundation.h>

//singleton object
@interface Incrementer : NSObject {
    NSUInteger theInteger_;
}

@property (nonatomic) NSUInteger theInteger;

+(Incrementer *) sharedIncrementer;
-(NSUInteger) incrementForTimeInterval: (NSTimeInterval) timeInterval;
-(BOOL) finishedIncrementing;

incrementer.m:

#import "Incrementer.h"

@implementation Incrementer

@synthesize theInteger = theInteger_;

static Incrementer *inc = nil;

-(void) increment {
    theInteger_++;
}

-(BOOL) finishedIncrementing {
    return (theInteger_>=100000000);
}

-(NSUInteger) incrementForTimeInterval: (NSTimeInterval) timeInterval {
    NSTimeInterval negativeTimeInterval = -1*timeInterval;
    NSDate *startDate = [NSDate date];
    while (!([self finishedIncrementing]) && [startDate timeIntervalSinceNow] > negativeTimeInterval)
        [self increment];
    return self.theInteger;
}

-(id) init {
    if (self = [super init]) {
        self.theInteger = 0;
    }
    return self;
}

#pragma mark --
#pragma mark singleton object methods

+ (Incrementer *) sharedIncrementer { 
    @synchronized(self) {
        if (inc == nil) {
            inc = [[Incrementer alloc]init];        
        }
    }
    return inc;
}

+ (id)allocWithZone:(NSZone *)zone {
    @synchronized(self) {
        if (inc == nil) {
            inc = [super allocWithZone:zone];
            return inc;  // assignment and return on first allocation
        }
    }
    return nil; // on subsequent allocation attempts return nil
}

- (id)copyWithZone:(NSZone *)zone
{
    return self;
}

- (id)retain {
    return self;
}

- (unsigned)retainCount {
    return UINT_MAX;  // denotes an object that cannot be released
}

- (void)release {
    //do nothing
}

- (id)autorelease {
    return self;
}

@end

IncrementerProgressView.m:

#import "IncrementerProgressView.h"


@implementation IncrementerProgressView
@synthesize progressLabel = progressLabel_;
@synthesize nextUpdateTimer = nextUpdateTimer_;

-(id) initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame: frame]) {
        progressLabel_ = [[UILabel alloc]initWithFrame:CGRectMake(20, 40, 300, 30)];
        progressLabel_.font = [UIFont systemFontOfSize:26];
        progressLabel_.adjustsFontSizeToFitWidth = YES;
        progressLabel_.textColor = [UIColor blackColor];
        [self addSubview:progressLabel_];
    }
    return self;
}

-(void) drawRect:(CGRect)rect {
    [self.nextUpdateTimer invalidate];
    Incrementer *shared = [Incrementer sharedIncrementer];
    NSUInteger progress = [shared incrementForTimeInterval: 0.1];
    self.progressLabel.text = [NSString stringWithFormat:@"Increments performed: %d", progress];
    if (![shared finishedIncrementing])
        self.nextUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:0. target:self selector:(@selector(setNeedsDisplay)) userInfo:nil repeats:NO];
}

- (void)dealloc {
    [super dealloc];
}

@end
人疚 2024-10-20 00:53:26

关于最初的问题:

总之,你可以(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
  • 您可以自己查看哪些方法有效,哪些无效。

  • 你可以看到奇怪的“渐进式减速”。为什么会发生这种情况?

  • 可以看到,用备受争议的TOMSWIFT方法,其实响应能力完全没有问题。随时点击响应(但仍然是奇怪的“渐进减速”问题)

所以压倒性的事情是这个奇怪的“渐进减速”:在每次迭代中,由于未知的原因,循环所花费的时间减少。请注意,这适用于“正确”执行(背景外观)或使用“立即”方法之一。


实用的解决方案?

对于未来阅读的人来说,如果由于“神秘的渐进式减速”而实际上无法使其在生产代码中工作,Felz 和 Void 都在另一个具体问题中提出了令人震惊的解决方案。

Regarding the original issue:

In a word, you can (A) background the large painting, and call to the foreground for UI updates or (B) arguably controversially there are four 'immediate' methods suggested that do not use a background process. For the result of what works, run the demo program. It has #defines for all five methods.


Alternately per Tom Swift

Tom Swift has explained the amazing idea of quite simply manipulating the run loop. Here's how you trigger the run loop:

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

This is a truly amazing piece of engineering. Of course one should be extremely careful when manipulating the run loop and as many pointed out this approach is strictly for experts.


However, a bizarre problem arises ...

Even though a number of the methods work, they don't actually "work" because there is a bizarre progressive-slow-down artifact you will see clearly in the demo.

Scroll to the 'answer' I pasted in below, showing the console output - you can see how it progressively slows.

Here's the new SO question:
Mysterious "progressive slowing" problem in run loop / drawRect

Here is V2 of the demo app...
http://www.fileswap.com/dl/p8lU3gAi/stepwiseDrawingV2.zip.html

You will see it tests all five methods,

#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
  • You can see for yourself which methods work and which do not.

  • you can see the bizarre "progressive-slow-down". Why does it happen?

  • you can see with the controversial TOMSWIFT method, there is actually no problem at all with responsiveness. tap for response at any time (but still the bizarre "progressive-slow-down" problem)

So the overwhelming thing is this weird "progressive-slow-down": on each iteration, for unknown reasons, the time taken for a loop decreases. Note that this applies to both doing it "properly" (background look) or using one of the 'immediate' methods.


Practical solutions?

For anyone reading in the future, if you are actually unable to get this to work in production code because of the "mystery progressive slowdown", Felz and Void have each presented astounding solutions in the other specific question.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文