¿Hay alguna manera de que drawRect funcione ahora?

La pregunta original ………………………………………..

Si usted es un usuario avanzado de drawRect, sabrá que, por supuesto, drawRect no se ejecutará hasta que “finalice todo el proceso”.

setNeedsDisplay marca una vista como invalidada y el sistema operativo, y básicamente espera hasta que se complete todo el procesamiento. Esto puede ser exasperante en la situación común en la que desea tener:

  • un controlador de vista 1
  • comienza alguna función 2
  • que incrementalmente 3
    • crea una obra de arte cada vez más complicada y 4
    • en cada paso, estableceNeedsDisplay (¡incorrecto!) 5
  • hasta que todo el trabajo esté hecho 6

Por supuesto, cuando haces los de arriba 1-6, todo lo que ocurre es que drawRect se ejecuta solo una vez después del paso 6.

Su objective es que la vista se actualice en el punto 5. ¿Qué hacer?


Solución a la pregunta original ……………………………………… .

En una palabra, puede (A) hacer un fondo de la pintura grande, y llamar al primer plano para actualizaciones de la interfaz de usuario o (B) discutiblemente hay cuatro métodos ‘inmediatos’ sugeridos que no usan un proceso en segundo plano. Para el resultado de lo que funciona, ejecute el progtwig de demostración. Tiene #defines para los cinco métodos.


Una solución alternativa verdaderamente asombrosa presentada por Tom Swift ………………

Tom Swift ha explicado la increíble idea de simplemente manipular el ciclo de ejecución . Así es como activa el ciclo de ejecución:

[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [fecha de NSDate]];

Esta es una pieza de ingeniería realmente asombrosa. Por supuesto, uno debe ser extremadamente cuidadoso al manipular el ciclo de ejecución y, como muchos señalaron, este enfoque es estrictamente para expertos.


El extraño problema que surge ……………………………………… .

A pesar de que varios de los métodos funcionan, en realidad no “funcionan” porque hay un extraño artefacto de disminución progresiva que verá claramente en la demostración.

Desplácese hasta la “respuesta” que pegué a continuación, que muestra la salida de la consola: puede ver cómo se ralentiza progresivamente.

Aquí está la nueva pregunta SO:
Misterioso problema de “ralentización progresiva” en el ciclo de ejecución / drawRect

Aquí está la versión 2 de la aplicación de demostración …
http://www.fileswap.com/dl/p8lU3gAi/stepwiseDrawingV2.zip.html

Verás que prueba los cinco métodos,

#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 
  • Puedes ver por ti mismo qué métodos funcionan y cuáles no.

  • puedes ver el extraño “progresivo-desaceleración”. ¿Por que sucede?

  • se puede ver con el controvertido método TOMSWIFT, en realidad no hay ningún problema con la capacidad de respuesta. toque para responder en cualquier momento. (pero aún así el extraño problema de “progresiva desaceleración”)

Así que lo abrumador es esta extraña “desaceleración progresiva”: en cada iteración, por razones desconocidas, el tiempo necesario para un ciclo desaparece. Tenga en cuenta que esto se aplica tanto para hacerlo “correctamente” (apariencia de fondo) o utilizando uno de los métodos “inmediatos”.


Soluciones prácticas ……………………

Para cualquiera que lea en el futuro, si realmente no puede lograr que esto funcione en el código de producción debido a la “desaceleración progresiva misteriosa” … Felz y Void han presentado soluciones asombrosas en la otra pregunta específica, espero que ayude.

Las actualizaciones de la interfaz de usuario ocurren al final de la pasada actual a través del ciclo de ejecución. Estas actualizaciones se realizan en el hilo principal, de modo que cualquier cosa que se ejecute durante un tiempo prolongado en el hilo principal (cálculos largos, etc.) evitará que se inicien las actualizaciones de la interfaz. Además, cualquier cosa que se ejecute durante un tiempo en el hilo principal también hará que su manejo táctil no responda.

Esto significa que no hay forma de “forzar” que ocurra una actualización de UI desde algún otro punto en un proceso que se ejecuta en el hilo principal. La statement anterior no es del todo correcta, como muestra la respuesta de Tom. Puede permitir que el ciclo de ejecución se complete en el medio de las operaciones realizadas en el hilo principal. Sin embargo, esto aún puede reducir la capacidad de respuesta de su aplicación.

En general, se recomienda que mueva todo lo que tarde un poco a un hilo de fondo para que la interfaz de usuario pueda seguir siendo receptiva. Sin embargo, cualquier actualización que desee realizar en la interfaz de usuario debe realizarse de nuevo en el hilo principal.

Quizás la forma más fácil de hacerlo en Snow Leopard y iOS 4.0+ es usar bloques, como en la siguiente muestra rudimentaria:

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

La parte Do some work de lo anterior podría ser un cálculo largo, o una operación que recorre múltiples valores. En este ejemplo, la UI solo se actualiza al final de la operación, pero si desea un seguimiento continuo del progreso en su UI, puede colocar el envío en la cola principal donde sea que necesite que se realice una actualización de UI.

Para versiones anteriores del sistema operativo, puede interrumpir un hilo de fondo manualmente o a través de una NSOperation. Para el enhebrado manual de fondo, puede usar

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

o

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

y luego para actualizar la UI que puedes usar

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

Tenga en cuenta que he encontrado que el argumento NO en el método anterior es necesario para obtener actualizaciones constantes de UI al tratar con una barra de progreso continuo.

Esta aplicación de muestra que creé para mi clase ilustra cómo usar NSOperations y colas para realizar el trabajo en segundo plano y luego actualizar la UI cuando haya terminado. Además, mi aplicación Moléculas utiliza subprocesos de fondo para procesar nuevas estructuras, con una barra de estado que se actualiza a medida que esto avanza. Puede descargar el código fuente para ver cómo lo logré.

Si entiendo tu pregunta correctamente, hay una solución simple para esto. Durante su rutina de larga ejecución necesita decirle al runloop actual que procese para una única iteración (o más, del runloop) en ciertos puntos de su propio procesamiento. por ejemplo, cuando quiere actualizar la pantalla. Cualquier vista con regiones sucias de actualización tendrá sus métodos drawRect: llamados cuando ejecuta el runloop.

Para decirle al runloop actual que procese para una iteración (y luego regrese a usted …):

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

Aquí hay un ejemplo de una rutina de ejecución larga (ineficaz) con un drawRect correspondiente, cada uno en el contexto de una UIView personalizada:

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

Y aquí hay una muestra completamente funcional que demuestra esta técnica .

Con algunos ajustes, probablemente pueda ajustar esto para hacer que el rest de la IU (es decir, la entrada del usuario) responda también.

Actualización (advertencia para usar esta técnica)

Solo quiero decir que estoy de acuerdo con gran parte de los comentarios de otros aquí que dicen que esta solución (llamar a runMode: forzar una llamada a drawRect 🙂 no es necesariamente una gran idea. He respondido esta pregunta con lo que creo que es una respuesta fáctica de "así es como" a la pregunta planteada, y no pretendo promover esto como una architecture "correcta". Además, no estoy diciendo que no haya otras formas (¿mejores?) De lograr el mismo efecto, sin duda puede haber otros enfoques de los que no tenía conocimiento.

Actualización (respuesta al código de muestra y la pregunta de rendimiento de Joe)

La ralentización del rendimiento que está viendo es la sobrecarga de ejecutar el runloop en cada iteración del código de dibujo, que incluye renderizar la capa en la pantalla, así como todo el otro procesamiento que hace el runloop, como recostackción y procesamiento de entrada.

Una opción podría ser invocar el runloop con menos frecuencia.

Otra opción podría ser optimizar su código de dibujo. Tal como está (y no sé si esta es su aplicación real, o solo su muestra ...) hay un puñado de cosas que podría hacer para hacerlo más rápido. Lo primero que haría es mover todo el código UIGraphicsGet / Save / Restore fuera del ciclo.

Desde un punto de vista arquitectónico, sin embargo, recomendaría considerar algunos de los otros enfoques mencionados aquí. No veo ninguna razón por la cual no puedas estructurar tu dibujo para que ocurra en una cadena de fondo (algoritmo sin modificar) y utilices un temporizador u otro mecanismo para señalar el hilo principal y actualizar su IU en alguna frecuencia hasta que se complete el dibujo. Creo que la mayoría de las personas que participaron en la discusión estarían de acuerdo en que este sería el enfoque "correcto".

Puede hacer esto repetidamente en un bucle y funcionará bien, sin hilos, sin problemas con el runloop, etc.

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

Si ya hay una transacción implícita antes del bucle, debe confirmarlo con [CATransaction commit] antes de que esto funcione.

Para poder llamar a drawRect lo antes posible (lo cual no es necesariamente inmediato, ya que el sistema operativo aún puede esperar hasta, por ejemplo, la próxima actualización de la pantalla del hardware, etc.), una aplicación debe inactivar su UI run lo antes posible, salir de todos los métodos en el subproceso de la interfaz de usuario, y por un período de tiempo distinto de cero.

Puede hacer esto en el hilo principal cortando cualquier procesamiento que requiera más tiempo que un marco de animación en trozos más cortos y progtwigr el trabajo continuo solo después de un breve retraso (para que drawRect pueda ejecutarse en los espacios), o haciendo el procesamiento en un hilo de fondo, con una llamada periódica para realizarSelectorOnMainThread para hacer un setNeedsDisplay a una velocidad de cuadro de animación razonable.

Un método que no sea de OpenGL para actualizar la pantalla de forma inmediata (lo que significa que en la próxima actualización de la pantalla de hardware o tres) es intercambiando los contenidos visibles de CALayer con una imagen o CGBitmap que haya dibujado. Una aplicación puede hacer un dibujo de Cuarzo en un bitmap de Core Graphics prácticamente en cualquier momento.

Nueva respuesta agregada:

Consulte los comentarios de Brad Larson a continuación y el comentario de Christopher Lloyd sobre otra respuesta aquí como la pista que conduce a esta solución.

 [ CATransaction flush ]; 

hará que se llame a drawRect en las vistas en las que se ha realizado una solicitud setNeedsDisplay, incluso si el borrado se realiza desde un método que está bloqueando el bucle de ejecución de la IU.

Tenga en cuenta que, al bloquear el hilo de UI, se requiere un enjuague de Core Animation para actualizar también los contenidos cambiantes de CALayer. Entonces, para animar el contenido gráfico a mostrar el progreso, ambos pueden terminar siendo formas de la misma cosa.

Nueva nota agregada a la nueva respuesta agregada arriba:

No descargue más rápido de lo que su drawRect o dibujo de animación puede completar, ya que esto podría colapsar los colores, causando efectos de animación extraños.

Sin cuestionar la sabiduría de esto (que deberías hacer), puedes hacer:

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

-setNeedsDisplay marcará la vista como que necesita ser redibujada. -displayIfNeeded obligará a la capa de respaldo de la vista a volver a dibujarse, pero solo si se ha marcado como necesario que se muestre.

Enfatizaré, sin embargo, que su pregunta es indicativa de una architecture que podría usar alguna nueva operación. En todos los casos, salvo en casos excepcionales, nunca debería necesitar o querer obligar a una vista a volver a dibujar inmediatamente . UIKit no está construido con ese caso de uso en mente, y si funciona, considérate afortunado.

¿Has intentado hacer el procesamiento pesado en un hilo secundario y volver a llamar al hilo principal para progtwigr actualizaciones de vista? NSOperationQueue hace que este tipo de cosas sea bastante fácil.


Código de ejemplo que toma una matriz de NSURL como entrada y los descarga de forma asincrónica, notificando el hilo principal a medida que cada uno de ellos finaliza y se guarda.

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

Me doy cuenta de que este es un hilo viejo, pero me gustaría ofrecer una solución limpia para el problema dado.

Estoy de acuerdo con otros carteles que en una situación ideal todo el trabajo pesado debe hacerse en una conversación de fondo, sin embargo, hay ocasiones en las que esto simplemente no es posible porque la parte que consume tiempo requiere mucho acceso a métodos que no son seguros para subprocesos, como los ofrecidos por UIKit. En mi caso, inicializar mi UI consume mucho tiempo y no hay nada que pueda ejecutar en segundo plano, así que mi mejor opción es actualizar una barra de progreso durante el inicio.

Sin embargo, una vez que pensamos en términos del enfoque GCD ideal, la solución es realmente simple. Hacemos todo el trabajo en un hilo de fondo, dividiéndolo en mandriles que se llaman sincrónicamente en el hilo principal. El ciclo de ejecución se ejecutará para cada plato, actualizando la interfaz de usuario y cualquier barra de progreso, 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 = ...: }); ... }); } 

Por supuesto, esto es esencialmente lo mismo que la técnica de Brad, pero su respuesta no resuelve el problema en cuestión: la de ejecutar una gran cantidad de código no seguro para subprocesos mientras se actualiza la interfaz de usuario periódicamente.

Creo que la respuesta más completa proviene de la publicación de blog de Jeffrey Sambell ‘Operaciones asincrónicas en iOS con Grand Central Dispatch’ y ¡funcionó para mí! Básicamente es la misma solución propuesta por Brad anteriormente pero completamente explicada en términos del modelo de concurrencia OSX / IOS.

La función dispatch_get_current_queue devolverá la cola actual desde la que se distribuye el bloque y la función dispatch_get_main_queue devolverá la cola principal donde se ejecuta su UI.

La función dispatch_get_main_queue es muy útil para actualizar la interfaz de usuario de la aplicación iOS ya que los métodos UIKit no son seguros para subprocesos (con algunas excepciones) por lo que cualquier llamada que realice para actualizar los elementos de la interfaz siempre se debe hacer desde la cola principal.

Una llamada GCD típica se vería así:

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

Y aquí va mi ejemplo de trabajo (iOS 6+). Muestra marcos de un video almacenado usando la clase 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); }); } } }); 

La primera parte de esta muestra se puede encontrar aquí en la respuesta de Damian.

Joe: si estás dispuesto a configurarlo para que todo tu procesamiento dentro de drawRect sea largo, puedes hacerlo funcionar. Acabo de escribir un proyecto de prueba. Funciona. Vea el código a continuación.

LengthyComputationTestAppDelegate.h:

 #import  @interface LengthyComputationTestAppDelegate : NSObject  { 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; } 

Incrementador.h:

 #import  //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