44

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

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

  • 视图控制器 1
  • 启动一些功能 2
  • 其中增量 3
    • 创作出越来越复杂的艺术品和 4
    • 在每一步,你设置NeedsDisplay(错误!)5
  • 直到所有工作完成 6

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

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

4

10 回答 10

37

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

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

[[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];
}

这是一个展示这种技术的完整工作示例

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

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

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

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

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

一种选择可能是不那么频繁地调用 runloop。

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

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

于 2011-01-22T03:40:46.813 回答
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
    });
});

上面的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应用程序使用后台线程来处理新结构,并在此过程中更新状态栏。你可以下载源代码来看看我是如何实现的。

于 2011-01-19T19:53:02.197 回答
11

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

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

如果在循环之前已经存在隐式事务,则需要使用 [CATransaction commit] 提交它,然后才能工作。

于 2011-01-23T06:10:02.977 回答
9

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

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

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

新添加的答案:

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

[ CATransaction flush ];

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

请注意,当阻塞 UI 线程时,还需要刷新核心动画来更新不断变化的 CALayer 内容。因此,对于动画图形内容以显示进度,这些最终可能都是同一事物的形式。

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

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

于 2011-01-19T20:25:08.773 回答
4

无需质疑这样做的智慧(你应该这样做),你可以这样做:

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

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

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

于 2011-01-23T19:08:02.147 回答
1

您是否尝试过在辅助线程上进行繁重的处理并回调主线程以安排视图更新?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];

}
于 2011-01-19T19:47:36.400 回答
1

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

我同意其他发帖者的观点,即在理想情况下,所有繁重的工作都应该在后台线程中完成,但是有时这根本不可能,因为耗时的部分需要大量访问非线程安全的方法,例如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 的同时运行大量非线程安全代码。

于 2012-10-02T16:02:22.327 回答
1

我认为,最完整的答案来自 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 调用如下所示:

// 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 的回答中找到。

于 2014-09-28T23:44:15.873 回答
0

乔——如果你愿意设置它以便你的冗长处理都发生在 drawRect 内部,你可以让它工作。我刚刚写了一个测试项目。有用。请参阅下面的代码。

LongyComputationTestAppDelegate.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;
}

增量器.h:

#import <Foundation/Foundation.h>

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

@property (nonatomic) NSUInteger theInteger;

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

增量器.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
于 2011-01-19T20:56:35.370 回答
0

关于原问题:

总而言之,您可以(A)将大型绘画作为背景,并调用前台进行 UI 更新,或者(B) 有争议地建议有四种不使用后台进程的“立即”方法。对于有效的结果,运行演示程序。它对所有五种方法都有#defines。


交替每汤姆斯威夫特

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

[[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 在另一个特定问题中各自提出了令人震惊的解决方案,希望对您有所帮助。

于 2021-11-29T14:53:13.057 回答