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

时间:2011-01-19 19:32:48

标签: iphone ios cocoa quartz-graphics runloop

原始问题...................................... .........

如果您是drawRect的高级用户,您将知道当然“drawRect”在“所有处理完成后”才会实际运行。

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

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

当然,当您执行上述1-6时,所有发生的事情是drawRect在步骤6之后仅运行

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


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

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


汤姆斯威夫特引入的真正令人震惊的替代解决方案..................

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

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

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


出现的奇怪问题.................................... ..........

尽管许多方法都有效,但它们实际上并不“有效”,因为在演示中会有一个奇怪的渐进式减速神器。

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

这是新的SO问题:
Mysterious "progressive slowing" problem in run loop / 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在其他具体问题中都提出了惊人的解决方案,希望它帮助

9 个答案:

答案 0 :(得分:37)

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

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

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

以下是一个(低效)长时间运行例程的示例,其中包含相应的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];
}

And here is a fully working sample demonstrating this technique

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

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

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

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

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

一个选项可能是不那么频繁地调用runloop。

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

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

答案 1 :(得分:27)

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

这意味着无法从主线程上运行的进程中的某个其他点“强制”进行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更新的主队列中。

对于较旧的OS版本,您可以手动或通过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更新。

我为我的课程创建的

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

答案 2 :(得分:10)

你可以在一个循环中反复执行此操作,它可以正常工作,没有线程,也没有搞乱runloop等。

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

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

答案 3 :(得分:8)

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

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

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

新增答案:

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

[ CATransaction flush ];

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

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

上面新添加的答案的新添加说明:

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

答案 4 :(得分:4)

不要质疑这个(你应该做的)的智慧,你可以这样做:

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

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

但是,我会强调,您的问题表明可以使用一些重新工作的架构。在极其罕见的情况下,您应该永远不需要或想强制视图立即重绘。 UIKit并没有考虑到这个用例,如果有效,请认为自己很幸运。

答案 5 :(得分:1)

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


将NSURL数组作为输入并以异步方式全部下载它们的示例代码,通知主线程,因为每个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];

}

答案 6 :(得分:1)

我意识到这是一个老线程,但我想为给定的问题提供一个干净的解决方案。

我同意其他海报,在理想的情况下,所有繁重的工作应该在后台线程中完成,但有时候这是不可能的,因为耗时的部分需要大量访问非线程安全诸如UIKit提供的方法。在我的情况下,初始化我的UI非常耗时,而且我无法在后台运行,因此我最好的选择是在初始化期间更新进度条。

然而,一旦我们考虑理想的GCD方法,解决方案实际上很简单。我们在后台线程中完成所有工作,将其分成在主线程上同步调用的chuck。将为每个卡盘运行运行循环,更新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时运行大量非线程安全代码。

答案 7 :(得分:1)

我认为,最完整的答案来自Jeffrey Sambell的博客文章'Asynchronous Operations in iOS with Grand Central Dispatch',它对我有用! 它与Brad上面提出的解决方案基本相同,但在OSX / IOS并发模型方面进行了充分解释。

  

dispatch_get_current_queue函数将返回当前队列   从中发送块和dispatch_get_main_queue   function将返回运行UI的主队列。

     

dispatch_get_main_queue函数对于更新时非常有用   iOS应用程序的UI作为UIKit方法不是线程安全的(有一些   例外)所以你为更新UI元素所做的任何调用都必须始终如此   从主队列完成。

     

典型的GCD调用看起来像这样:

// 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.

这是我的工作示例(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);
            });
        }
    }
});

这个样本的第一部分可以在Damian的答案中找到here

答案 8 :(得分:0)

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