La question initiale ......................................... ......
Si vous êtes un utilisateur expérimenté de drawRect, vous saurez que, naturellement, drawRect ne sera en réalité pas exécuté tant que "tout le traitement n'est pas terminé".
setNeedsDisplay marque une vue comme invalidée et le système d'exploitation et attend fondamentalement que tout le traitement soit terminé. Cela peut être exaspérant dans la situation courante où vous voulez avoir:
Bien entendu, lorsque vous effectuez les opérations 1 à 6 ci-dessus, tout ce qui se produit est que drawRect est exécutéune seule foisaprès l'étape 6.
Votre objectif est que la vue soit actualisée au point 5. Que faire?
Solution à la question initiale ....................................... .......
Dans un mot, vous pouvez(arrière-plan)le grand tableau et appeler au premier plan les mises à jour de l'interface utilisateur ou(B) discutablement controversé _ Il existe quatre méthodes "immédiates" suggérées qui n'utilisent pas de processus d'arrière-plan. Pour le résultat de ce qui fonctionne, lancez le programme de démonstration. Il a #defines pour les cinq méthodes.
Solution alternative vraiment étonnante présentée par Tom Swift ..................
Tom Swift a expliqué l’idée étonnante de tout simplement manipuler la boucle d’exécution . Voici comment vous déclenchez la boucle d'exécution:
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];
C'est une pièce d'ingénierie vraiment incroyable. Bien sûr, il faut être extrêmement prudent lors de la manipulation de la boucle d'exécution et, comme beaucoup l'ont souligné, cette approche est strictement réservée aux experts.
Le problème bizarre qui se pose ....................................... .......
Même si un certain nombre de méthodes fonctionnent, elles ne "fonctionnent" pas, car il existe un artefact bizarre au ralentissement progressif que vous verrez clairement dans la démo.
Faites défiler jusqu'à la «réponse» que j'ai collée ci-dessous, montrant la sortie de la console - vous pouvez voir comment elle ralentit progressivement.
Voici la nouvelle question SO:
Mystérieux problème de "ralentissement progressif" dans la boucle d'exécution/drawRect
Voici la V2 de l'application de démonstration ...
http://www.fileswap.com/dl/p8lU3gAi/stepwiseDrawingV2.Zip.html
Vous verrez qu'il teste les cinq méthodes,
#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
Vous pouvez voir par vous-même quelles méthodes fonctionnent et lesquelles ne fonctionnent pas.
vous pouvez voir l'étrange "ralentissement progressif". pourquoi ça se passe
comme vous pouvez le constater avec la méthode TOMSWIFT, très controversée, il n’ya aucun problème de réactivité. appuyez sur pour obtenir une réponse à tout moment. (mais toujours le problème bizarre de "ralentissement progressif")
La chose la plus écrasante est donc cet étrange "ralentissement progressif": à chaque itération, pour des raisons inconnues, le temps pris pour une boucle diminue. Notez que cela s'applique à la fois de le faire "correctement" (aspect de fond) ou en utilisant l'une des méthodes "immédiates".
Solutions pratiques ........................
Pour ceux qui liront à l'avenir, si vous êtes réellement incapable de faire en sorte que cela fonctionne dans le code de production à cause du "ralentissement progressif mystérieux" ... Felz et Void ont chacun présenté des solutions étonnantes dans l'autre question spécifique. , J'espère que ça aide.
Les mises à jour de l'interface utilisateur ont lieu à la fin du passage en cours dans la boucle d'exécution. Ces mises à jour sont effectuées sur le thread principal. Par conséquent, tout ce qui dure longtemps dans le thread principal (calculs longs, etc.) empêche le démarrage des mises à jour de l'interface. En outre, tout ce qui est exécuté pendant un certain temps sur le thread principal empêchera également votre manipulation tactile de réagir.
Cela signifie qu'il n'y a aucun moyen de "forcer" une actualisation de l'interface utilisateur à partir d'un autre point du processus en cours d'exécution sur le thread principal. La déclaration précédente n'est pas tout à fait correcte, comme le montre la réponse de Tom. Vous pouvez permettre à la boucle d'exécution de se terminer au milieu d'opérations effectuées sur le thread principal. Toutefois, cela peut encore réduire la réactivité de votre application.
En règle générale, il est recommandé de déplacer tout élément nécessitant un certain temps d'exécution vers un thread en arrière-plan afin que l'interface utilisateur puisse rester réactif. Cependant, toutes les mises à jour que vous souhaitez effectuer sur l'interface utilisateur doivent être effectuées sur le thread principal.
La manière la plus simple de procéder sous Snow Leopard et iOS 4.0+ consiste à utiliser des blocs, comme dans l'exemple rudimentaire suivant:
dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
// Do some work
dispatch_async(main_queue, ^{
// Update the UI
});
});
La partie Do some work
de ce qui précède peut être un calcul long ou une opération en boucle sur plusieurs valeurs. Dans cet exemple, l'interface utilisateur n'est mise à jour qu'à la fin de l'opération. Toutefois, si vous souhaitez un suivi de la progression continue dans votre interface utilisateur, vous pouvez placer la répartition dans la file d'attente principale où vous avez besoin qu'une mise à jour de l'interface utilisateur soit effectuée.
Pour les anciennes versions de système d'exploitation, vous pouvez rompre un thread d'arrière-plan manuellement ou via une opération NSO. Pour un enfilage manuel en arrière-plan, vous pouvez utiliser
[NSThread detachNewThreadSelector:@selector(doWork) toTarget:self withObject:nil];
ou
[self performSelectorInBackground:@selector(doWork) withObject:nil];
puis pour mettre à jour l'interface utilisateur, vous pouvez utiliser
[self performSelectorOnMainThread:@selector(updateProgress) withObject:nil waitUntilDone:NO];
Notez que j'ai trouvé que l'argument NO de la méthode précédente était nécessaire pour obtenir des mises à jour constantes de l'interface utilisateur tout en gérant une barre de progression continue.
Cet exemple d'application J'ai créé pour ma classe montre comment utiliser à la fois NSOperations et les files d'attente pour effectuer un travail en arrière-plan, puis mettre à jour l'interface utilisateur une fois l'opération terminée. De plus, mon Molecules application utilise des threads d’arrière-plan pour le traitement de nouvelles structures, avec une barre d’état mise à jour à mesure de son avancement. Vous pouvez télécharger le code source pour voir comment j'ai réalisé cela.
Si je comprends bien votre question, il existe une solution simple à cela. Au cours de votre longue routine, vous devez indiquer au cycle en cours de traiter une seule itération (ou plus du cycle) à certains moments de votre propre traitement. par exemple, lorsque vous souhaitez mettre à jour l'affichage. Toutes les vues avec des régions de mise à jour modifiées auront leurs méthodes drawRect: appelées lors de l'exécution du runloop.
Pour indiquer au cycle d'exécution actuel de le traiter pour une itération (puis de vous le retourner ...):
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];
Voici un exemple de routine longue (inefficace) avec un drawRect correspondant, chacun dans le contexte d'un UIView personnalisé:
- (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];
}
Et voici un échantillon pleinement fonctionnel démontrant cette technique .
Avec quelques ajustements, vous pouvez probablement ajuster cela pour que le reste de l'interface utilisateur (c'est-à-dire l'entrée de l'utilisateur) soit également réactif.
Update (mise en garde concernant l'utilisation de cette technique)
Je veux juste dire que je suis d’accord avec la plupart des commentaires d’autres personnes qui disent ici que cette solution (appeler runMode: forcer un appel à drawRect :) n’est pas forcément une bonne idée. J'ai répondu à cette question avec ce que j'estime être une réponse factuelle "voici comment" à la question posée, et je n'ai pas l'intention de promouvoir cela en tant qu'architecture "correcte". En outre, je ne dis pas qu'il n'y a peut-être pas d'autres moyens (meilleurs?) Pour obtenir le même effet - il peut certainement y avoir d'autres approches dont je n'étais pas au courant.
Update (réponse à l'exemple de code de Joe et à sa question sur les performances)
Le ralentissement des performances que vous constatez résulte de l’exécution du bouclage à chaque itération de votre code de dessin, ce qui inclut le rendu du calque à l’écran ainsi que tous les autres traitements exécutés par le bouclage, tels que la collecte et le traitement des entrées.
Une option pourrait consister à invoquer moins souvent le cycle d'exécution.
Une autre option pourrait être d'optimiser votre code de dessin. Dans l'état actuel des choses (et je ne sais pas s'il s'agit de votre application réelle ou simplement de votre échantillon ...), vous pouvez effectuer un certain nombre de choses pour la rendre plus rapide. La première chose à faire est de déplacer tout le code UIGraphicsGet/Save/Restore en dehors de la boucle.
Cependant, d’un point de vue architectural, je recommanderais vivement d’examiner certaines des autres approches mentionnées ici. Je ne vois aucune raison pour laquelle vous ne pouvez pas structurer votre dessin de manière à ce qu'il se produise sur un thread d'arrière-plan (algorithme inchangé), et utilisez un minuteur ou un autre mécanisme pour signaler au thread principal de mettre à jour son interface utilisateur à une certaine fréquence jusqu'à la fin du dessin. Je pense que la plupart des gens qui ont participé à la discussion seraient d'accord pour dire que ce serait l'approche "correcte".
Vous pouvez faire cela à plusieurs reprises dans une boucle et tout ira bien, pas de threads, pas de déconner avec le runloop, etc.
[CATransaction begin];
// modify view or views
[view setNeedsDisplay];
[CATransaction commit];
Si une transaction implicite est déjà en place avant la boucle, vous devez la valider avec [Validation CATransaction] avant que cela fonctionne.
Pour que drawRect soit appelé le plus tôt possible (ce qui n’est pas nécessairement immédiat, car le système d’exploitation peut encore attendre, par exemple, la prochaine actualisation de l’affichage du matériel, etc.), une application doit utiliser sa boucle d’inactivité dès que possible, par quitter toutes les méthodes du thread d'interface utilisateur et pour une durée non nulle.
Vous pouvez le faire dans le thread principal en découpant en morceaux plus courts tout traitement prenant plus de temps qu'une image d'animation et en planifiant la poursuite du travail uniquement après un bref délai (de sorte que drawRect peut s'exécuter dans les espaces vides), ou en effectuant le traitement dans un fichier. thread d'arrière-plan, avec un appel périodique à performSelectorOnMainThread pour effectuer un setNeedsDisplay à un débit d'images raisonnable.
Une méthode non OpenGL pour mettre à jour l'affichage presque immédiatement (ce qui signifie qu'à la prochaine actualisation de l'affichage matériel ou à trois), consiste à permuter le contenu visible de CALayer avec une image ou une CGBitmap dans laquelle vous avez dessiné. Une application peut faire du dessin Quartz dans un bitmap Core Graphics à tout moment.
Nouvelle réponse ajoutée:
Veuillez vous reporter aux commentaires de Brad Larson ci-dessous et aux commentaires de Christopher Lloyd sur une autre réponse proposée ici comme indice de cette solution.
[ CATransaction flush ];
drawRect sera appelé sur les vues sur lesquelles une demande setNeedsDisplay a été effectuée, même si le vidage est effectué depuis une méthode bloquant la boucle d'exécution de l'interface utilisateur.
Notez que, lors du blocage du thread d'interface utilisateur, un vidage de Core Animation est également requis pour mettre à jour le contenu de CALayer modifié. Ainsi, pour animer un contenu graphique afin de montrer les progrès réalisés, il peut s’avérer que ces deux formes se ressemblent.
Nouvelle note ajoutée à la nouvelle réponse ajoutée ci-dessus:
Ne rincez pas plus rapidement que drawRect ou votre dessin d’animation ne peut se terminer, car cela pourrait entraîner des flous dans la file, ce qui provoquerait des effets d’animation étranges.
Sans remettre en question la sagesse de ce que vous devez faire, vous pouvez faire:
[myView setNeedsDisplay];
[[myView layer] displayIfNeeded];
-setNeedsDisplay
marquera la vue comme devant être redessinée .-displayIfNeeded
forcera la couche de sauvegarde de la vue à se redessiner, mais uniquement si elle est marquée comme devant être affichée.
J'insisterai cependant sur le fait que votre question est révélatrice d'une architecture qui pourrait nécessiter un remaniement. Dans tous les cas sauf exceptionnellement rares, vous devriez ne jamais avoir besoin ou vouloir forcer le redessin d'une vue immédiatement}. UIKit avec pas construit avec ce cas d'utilisation à l'esprit, et si cela fonctionne, considérez-vous chanceux.
Avez-vous essayé d'effectuer le traitement lourd sur un thread secondaire et de rappeler le thread principal pour planifier les mises à jour des vues? NSOperationQueue
rend ce genre de chose assez facile.
Exemple de code qui prend un tableau de NSURL en entrée et les télécharge de manière asynchrone, en notifiant le thread principal au fur et à mesure que chacun d'eux est terminé et enregistré.
- (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];
}
Je me rends compte que c’est un vieux fil conducteur, mais j’aimerais proposer une solution propre au problème posé.
Je conviens avec d’autres affiches que, dans une situation idéale, tout le travail de levage doit être effectué dans un fil d’arrière-plan. Cependant, il arrive parfois que cela ne soit tout simplement pas possible, car la partie qui prend du temps nécessite de nombreux accès à des méthodes non thread-safe ceux offerts par UIKit. Dans mon cas, l’initialisation de mon interface utilisateur prend beaucoup de temps et je ne peux rien exécuter en arrière-plan; ma meilleure option est donc de mettre à jour une barre de progression pendant l’init.
Cependant, une fois que nous pensons en termes d’approche idéale de GCD, la solution est simple. Nous effectuons tout le travail dans un fil d’arrière-plan, en le divisant en mandrins appelés de manière synchrone sur le fil principal. La boucle d'exécution sera exécutée pour chaque mandrin, mettant à jour l'interface utilisateur et les barres de progression, 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 = ...:
});
...
});
}
Bien sûr, il s’agit pour l’essentiel de la technique de Brad, mais sa réponse ne résout pas tout à fait le problème, à savoir exécuter un grand nombre de codes ne contenant pas de threads lors de la mise à jour périodique de l’UI.
Je pense que la réponse la plus complète provient de l'article de blog de Jeffrey Sambell "Opérations asynchrones sous iOS avec Grand Central Dispatch" et cela a fonctionné pour moi! en termes de modèle de concurrence OSX/IOS.
La fonction
dispatch_get_current_queue
renverra la file d'attente actuelle à partir duquel le bloc est envoyé et ledispatch_get_main_queue
fonction retournera la file d'attente principale où votre interface utilisateur est en cours d'exécution.La fonction
dispatch_get_main_queue
est très utile pour mettre à jour le fichier L’IU de l’application iOS en tant que méthodesUIKit
ne sont pas thread-safe (avec quelques exceptions ). Par conséquent, tout appel que vous effectuez pour mettre à jour des éléments de l’UI doit toujours être. fait à partir de la file d'attente principale.Un appel GCD typique ressemblerait à quelque chose comme ceci:
// 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.
Et voici mon exemple de travail (iOS 6+). Il affiche les images d'une vidéo stockée à l'aide de la classe 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 première partie de cet échantillon peut être trouvée ici dans la réponse de Damian.
Joe - si vous êtes prêt à le configurer de manière à ce que votre long traitement se déroule entièrement dans drawRect, vous pouvez le faire fonctionner. Je viens d'écrire un projet test. Ça marche. Voir le code ci-dessous.
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