现在有没有办法使drawRect工作?
原来的问题………………………………………..
如果您是drawRect的高级用户,您将会知道当然在“所有处理完成”之前,drawRect才会真正运行。
setNeedsDisplay将视图标记为无效的操作系统,并基本上等待,直到所有的处理完成。 这可以在你想要的普通情况下生气:
- 视图控制器1
- 启动一些function2
- 增量式3
- 创造出越来越复杂的艺术作品
- 在每一步,你setNeedsDisplay(错!)5
- 直到所有的工作完成6
当然,当你做上述1-6时,所发生的只是在第6步之后,drawRect 才运行一次 。
您的目标是在第5点刷新视图。该怎么办?
解决原来的问题……………………………………… 。
总而言之,你可以(A)背景大型的绘画,并调用前景的UI更新或(B)争论有争议的有四个“立即”方法build议不要使用后台进程。 对于什么工作的结果,运行演示程序。 它有#定义所有五种方法。
汤姆·斯威夫特(Tom Swift)介绍的真正惊人的替代解决scheme………………
汤姆·斯威夫特(Tom Swift)解释了简单地操作运行循环的奇妙想法。 以下是您如何触发运行循环:
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]];
这是一个真正令人惊叹的工程。 当然,在操作运行循环时应该非常小心,许多人指出这种方法对于专家来说是严格的。
奇异的问题……………………………………… 。
尽pipe有许多方法可行,但实际上它们并不“起作用”,因为在演示中会看到一个奇怪的渐进式减速神器。
滚动到下面粘贴的“答案”,显示控制台输出 – 你可以看到它是如何逐渐减慢。
这是新的SO问题:
运行循环/ drawRect中的神秘的“渐进式减速”问题
这里是演示应用程序的V2 …
http://www.fileswap.com/dl/p8lU3gAi/stepwiseDrawingV2.zip.html
你会看到它testing所有五种方法,
#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方法,实际上没有任何问题的响应。 随时点击回复。 (但仍然是奇怪的“渐进式减速”问题)
所以压倒性的是这种奇怪的“渐进式减速”:在每次迭代中,由于不明原因,循环所花费的时间都会减less。 请注意,这适用于“正确”(背景外观)或使用其中一种“立即”方法。
实用的解决scheme……………………
对于将来阅读的人来说,如果实际上由于“神秘的渐进式放缓”而无法使用这些代码来生产代码…… Felz和Void在另一个具体问题上都提出了令人惊异的解决scheme,希望它能起到一些作用。
用户界面的更新发生在当前通过运行循环的末尾。 这些更新是在主线程上执行的,所以在主线程中长时间运行的任何东西(冗长的计算等)都将阻止启动接口更新。 此外,在主线程上运行一段时间的任何内容也会导致您的触摸处理无响应。
这意味着在主线程上运行的进程中没有办法“强制”UI刷新发生。 汤姆的回答显示,以前的说法并不完全正确。 您可以允许运行循环在主线程执行的操作中间完成。 但是,这仍然可能会降低您的应用程序的响应速度。
通常,build议您将需要一段时间才能执行的任何操作移到后台线程,以便用户界面可以保持响应。 但是,您希望对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];
然后更新您可以使用的用户界面
[self performSelectorOnMainThread:@selector(updateProgress) withObject:nil waitUntilDone:NO];
请注意,我发现前面的方法中的NO参数需要在处理连续的进度条时获得持续的UI更新。
我为我的类创build的示例应用程序说明了如何使用NSOperations和队列来执行后台工作,然后在完成时更新UI。 此外,我的分子应用程序使用后台线程来处理新的结构,与此进展状态栏更新。 你可以下载源代码,看看我是如何实现这一目标的。
如果我正确地理解你的问题,这是一个简单的解决scheme。 在你长时间运行的例程中,你需要告诉当前的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]; }
这里是一个充分的工作示例演示这种技术 。
随着一些调整,你可以调整,以使其余的用户界面(即用户input)也响应。
更新(警告使用这种技术)
我只想说,我同意大部分来自其他人的反馈,说这个解决scheme(调用runMode:强制调用drawRect :)不一定是个好主意。 我已经回答了这个问题,我认为这是一个事实性的“这是怎么回答”这个陈述的问题,我不打算把这个问题推广为“正确的”架构。 另外,我并不是说可能没有其他(更好的)方法来达到同样的效果 – 当然也可能有其他方法,我没有意识到。
更新(回应乔的示例代码和性能问题)
您看到的性能下降是在绘图代码的每次迭代中运行runloop的开销,其中包括将图层渲染到屏幕以及runloop执行的所有其他处理,如input采集和处理。
一种select可能是不经常调用runloop。
另一个select可能是优化您的绘图代码。 按照现状(我不知道这是你的实际应用程序,还是只是你的样本…),你可以做一些事情来加快速度。 我要做的第一件事就是把所有的UIGraphicsGet / Save / Restore代码移到循环之外。
然而,从架构的angular度来看,我强烈build议考虑这里提到的其他一些方法。 我没有看到为什么你不能在后台线程上构build你的绘图(algorithm不变),并使用一个定时器或其他机制来通知主线程在某个频率上更新它的UI,直到绘制完成。 我想大部分参与讨论的人都会同意这是“正确的”方法。
你可以在一个循环中重复执行这个操作,它可以正常工作,不会有线程,也不会妨碍runloop等等。
[CATransaction begin]; // modify view or views [view setNeedsDisplay]; [CATransaction commit];
如果在循环之前已经存在一个隐式事务,那么你需要在[CATransaction commit]之前提交这个隐式事务。
为了得到最快的drawRect(这不一定是马上,因为操作系统可能还要等到下一个硬件显示刷新等等),应用程序应该尽快地将它的UI运行循环,通过退出UI线程中的任何和所有方法,以及非零的时间。
您可以在主线程中执行此操作,方法是将超过animation帧时间的任何处理切分为较短的块,并且仅在短暂延迟之后才安排继续工作(所以drawRect可能会在间隙中运行),或者通过在后台线程,定期调用performSelectorOnMainThread以合理的animation帧速率执行setNeedsDisplay。
一个非OpenGL的方法来立即更新显示附近(这意味着在下一个硬件显示刷新或三个)是通过交换可见CALayer内容与您已经绘制的图像或CGBitmap。 一个应用程序可以随时将Quartz绘图到Core Graphics位图中。
新增加的答案:
请参阅下面的Brad Larson的评论和Christopher Lloyd对这里的另一个答案的评论,作为通向这个解决scheme的暗示。
[ CATransaction flush ];
将导致drawRect在setNeedsDisplay请求已经完成的视图上被调用,即使是在阻塞UI运行循环的方法内完成刷新。
请注意,在阻塞UI线程时,还需要Core Animation刷新来更新更改的CALayer内容。 因此,为了使graphics内容呈现出dynamic效果,这些可能最终都是同一事物的forms。
新添加的注释新增上面的答案:
不要冲刷得比drawRect更快,也不能完成animation绘制,因为这可能会排队冲洗,造成奇怪的animation效果。
不用质疑这个(你应该这么做)的智慧,你可以这样做:
[myView setNeedsDisplay]; [[myView layer] displayIfNeeded];
-setNeedsDisplay
会将视图标记为需要重绘。 -displayIfNeeded
将强制视图的背景图层重绘,但只有当它被标记为需要显示。
但是,我会强调,你的问题是一个可以使用一些重新工作的架构的指示。 除了非常罕见的情况外,您不应该或者不希望强制立即重新绘制视图 。 UIKit没有考虑到这个用例,如果它工作,认为自己是幸运的。
你有没有尝试在辅助线程上执行繁重的处理,并调用回主线程安排视图更新? NSOperationQueue
使这种事情很容易。
将NSURL数组作为input并将其asynchronous下载的示例代码,并在每个主线程完成并保存时通知主线程。
- (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]; }
我意识到这是一个古老的线程,但我想提供一个干净的解决scheme给出的问题。
我同意其他的海报,在理想的情况下,所有繁重的工作都应该在后台线程中完成,但有时候这是不可能的,因为耗时的部分需要大量的访问非线程安全的方法,比如那些由UIKit提供的。 在我的情况下,初始化我的用户界面非常耗时,并且我没有办法在后台运行,所以我最好的select是在init中更新进度条。
然而,一旦我们用理想的GCD方法来思考,解决scheme实际上是一个简单的方法。 我们在后台线程中完成所有的工作,将其分成在主线程中同步调用的卡盘。 运行循环将针对每个卡盘运行,更新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时运行大量非线程安全代码。
我认为,最完整的答案来自Jeffrey Sambell的博客文章“iOS与Grand Central Dispatch的asynchronous操作” ,它对我很有帮助! 它基本上是与上面Brad提出的解决scheme相同的解决scheme,但是完全用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
类显示存储video的帧:
//...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); }); } } });
这个样本的第一部分可以在这里findDamian的答案。
乔 – 如果你愿意设置它,以便你的冗长的处理都发生在drawRect里面,你可以使它工作。 我刚写了一个testing项目。 有用。 看下面的代码。
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