44 votes

Existe-t-il un moyen de faire fonctionner drawRect dès maintenant?

La question d'origine...............................................

Si vous êtes un utilisateur avancé de drawRect, vous savez que bien sûr drawRect ne sera pas réellement exécuter jusqu'à ce que "tout le traitement est terminé."

setNeedsDisplay drapeaux vue comme invalidé et les OS, et en gros, attend jusqu'à ce que tout le traitement est fait. Cela peut être exaspérant dans la situation où vous voulez avoir:

  • un view controller 1
  • commence une fonction 2
  • qui a progressivement 3
    • crée une plus compliquée et plus d'illustrations et de 4
    • à chaque étape, vous setNeedsDisplay (faux!) 5
  • jusqu'à ce que tout le travail est fait 6

Bien sûr, quand vous faites le au-dessus de 1-6, tout ce qui arrive est que drawRect est exécuté une seule fois après l'étape 6.

Votre but est de l'affichage pour être actualisé au point 5. Que faire?


La Solution de la question d'origine..............................................

En un mot, vous pouvez (A) de fond de la grande peinture, et d'appel à l'avant-plan de l'INTERFACE utilisateur des mises à jour ou (B), sans doute de façon controversée , il y a quatre "immédiat" des méthodes suggéré de ne pas utiliser un processus d'arrière-plan. Le résultat de ce qui fonctionne, exécutez le programme de démonstration. Il a #définit pour les cinq méthodes.


Époustouflant, une solution alternative introduite par Tom Swift..................

Tom Swift a expliqué l'incroyable idée de tout simplement la manipulation de l'exécution de la boucle. Voici comment déclencher l'exécution de la boucle:

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

C'est vraiment un étonnant morceau de l'ingénierie. Bien sûr, on doit être extrêmement prudent lors de la manipulation de l'exécution de la boucle et comme beaucoup l'ont souligné, cette approche est strictement pour les experts.


Le Problème Bizarre Qui Se Pose..............................................

Même si un certain nombre de méthodes de travail, ils n'ont pas fait le "travail" parce qu'il est un étrange progressive du ralentissement de l'artefact, vous verrez clairement dans la démo.

Faites défiler jusqu'à la "réponse", j'ai collé ci-dessous, montrant la sortie de la console - vous pouvez voir comment il ralentit progressivement.

Voici le nouveau DONC, la question:
Mystérieux "ralentissement progressif" problème dans l'exécution de la boucle / drawRect

Voici la V2 de la démo app...
http://www.fileswap.com/dl/p8lU3gAi/stepwiseDrawingV2.zip.html

Vous verrez qu'il teste tous 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 les méthodes qui fonctionnent et celles qui ne le sont pas.

  • vous pouvez voir l'étrange "progressif-slow down". pourquoi se produit-il?

  • vous pouvez voir avec le controversé TOMSWIFT méthode, il n'y a effectivement pas de problème de réactivité. appuyez sur pour la réponse en tout temps. (mais encore l'étrange "progressif-slow-down" (problème)

Si l'écrasante, c'est bizarre "progressif-slow-down": à chaque itération, pour des raisons inconnues, le temps pris pour une boucle de décès. Notez que cela s'applique à la fois à le faire "correctement" (fond d'oeil) ou à l'aide de l'un des "immédiat" des méthodes.


Des solutions pratiques ........................

Pour tous ceux qui lisent dans l'avenir, si vous êtes réellement incapable d'obtenir que cela fonctionne dans le code de production en raison de la "mystère ralentissement progressif" ... Felz et Vide, ont chacun présenté d'étonnantes solutions dans d'autres question spécifique, j'espère que ça aide.

37voto

TomSwift Points 22012

Si je comprends votre question correctement, il existe une solution simple à ce. Au cours de votre long-routine en cours d'exécution, vous devez dire à l'actuel runloop pour une seule itération (ou plus, de la runloop) à certains points dans votre propre traitement. e.g, lorsque vous voulez mettre à jour l'affichage. Tout point de vue avec sale de mise à jour de régions auront leur drawRect: les méthodes appelées lorsque vous exécutez le runloop.

Pour raconter l'actuel runloop pour une itération (et puis de retour pour vous...):

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

Voici un exemple d'un (inefficace) de long routine en cours d'exécution avec un correspondant drawRect - chacun dans le contexte d'une coutume 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];
}

Et ici, c'est un travail entièrement exemple illustrant cette technique.

Avec quelques réglages, vous pouvez probablement ajuster ce pour faire le reste de l'INTERFACE (c'est à dire de l'entrée utilisateur) répondre ainsi.

Mise à jour (mise en garde pour l'utilisation de cette technique)

Je veux juste dire que je suis d'accord avec beaucoup de commentaires des autres, ici en disant que cette solution (appel runMode: pour forcer un appel à drawRect:) n'est pas forcément une bonne idée. J'ai répondu à cette question avec ce que je ressens est une factuel "voici comment" la réponse à la question posée, et je ne suis pas l'intention de promouvoir ce que "correcte" de l'architecture. Aussi, je ne dis pas qu'il n'y a pas d'autres (mieux?) des moyens pour obtenir le même effet - certainement, il peut y avoir d'autres approches que je n'étais pas au courant.

Mise à jour (réponse à la Joe exemples de code et les performances de la question)

Le ralentissement des performances que vous voyez est la surcharge de l'exécution de la runloop à chaque itération de votre code de dessin, qui comprend le rendu de la couche à l'écran ainsi que tous les autres le traitement de la runloop n', telle que la collecte et le traitement.

Une option pourrait être d'invoquer la runloop moins fréquemment.

Une autre option pourrait être d'optimiser votre code de dessin. Comme il se trouve (et je ne sais pas si ceci est votre application, ou tout simplement votre échantillon...) il n'y a qu'une poignée de choses que vous pouvez faire pour le rendre plus rapide. La première chose que je voudrais faire est de déplacer toutes les UIGraphicsGet/Sauvegarder/Restaurer le code en dehors de la boucle.

D'un point de vue architectural cependant, je vous recommande fortement d'examiner quelques-uns des autres approches mentionnées ici. Je ne vois aucune raison pourquoi vous ne pouvez pas la structure de votre dessin pour arriver sur un thread d'arrière-plan (algorithme inchangé), et l'utilisation d'une minuterie ou d'un autre mécanisme pour signaler le thread principal pour mettre à jour l'INTERFACE utilisateur sur la fréquence jusqu'à ce que le dessin est terminé. Je pense que la plupart des gens qui ont participé à la discussion serait d'accord que ce serait la "bonne" approche.

27voto

Brad Larson Points 122629

Les mises à jour de l'interface utilisateur arriver à la fin de la passer le courant à travers l'exécution de la boucle. Ces mises à jour sont effectuées sur le thread principal, de sorte que tout ce qui fonctionne pour une longue période dans le thread principal (de longs calculs, etc.) permettra d'éviter les mises à jour d'interface de démarrage. En outre, tout ce qui fonctionne pour un certain temps sur le thread principal peut aussi causer votre manipulation tactile ne répond pas.

Cela signifie qu'il n'y a aucun moyen de "forcer" un de rafraîchissement de l'INTERFACE utilisateur de se produire un autre moment dans un processus en cours d'exécution sur le thread principal. La déclaration n'est pas tout à fait correct, comme Tom réponse de la montre. Vous pouvez autoriser l'exécution de la boucle de réalisation dans le milieu des opérations effectuées sur le thread principal. Toutefois, cela peut réduire la réactivité de votre application.

En général, il est recommandé que vous vous déplacez quelque chose qui prend un certain temps pour effectuer un thread d'arrière-plan de sorte que l'interface utilisateur peut rester sensible. Cependant, toutes les mises à jour que vous souhaitez effectuer à l'INTERFACE utilisateur doivent être faites de retour sur le thread principal.

Peut-être la façon la plus simple de le faire sous Snow Leopard et iOS 4.0+ est d'utiliser les blocs, comme dans l'rudimentaire de l'échantillon:

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

L' Do some work de la partie de la ci-dessus pourrait être un long calcul, ou une opération qui passe en boucle sur plusieurs valeurs. Dans cet exemple, l'INTERFACE utilisateur est mis à jour seulement à la fin de l'opération, mais si vous voulez en continu le suivi des progrès dans votre INTERFACE utilisateur, vous pouvez placer l'envoi à la file d'attente principale où jamais vous avez besoin d'une INTERFACE utilisateur mise à jour à effectuer.

Pour les anciennes versions de systèmes d'exploitation, vous pouvez casser un thread d'arrière-plan manuellement ou par le biais d'un NSOperation. Pour manuel de thread d'arrière-plan, vous pouvez utiliser

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

ou

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

et puis pour mettre à jour l'INTERFACE utilisateur que vous pouvez utiliser

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

Notez que j'ai trouvé sur le PAS d'argument dans la méthode précédente pour être nécessaire pour obtenir la constante de l'INTERFACE utilisateur des mises à jour tout en traitant avec un progrès continu de la barre.

Cet exemple d'application que j'ai créé pour ma classe montre comment utiliser les deux NSOperations et les files d'attente pour effectuer le travail de fond et de mise à jour de l'INTERFACE utilisateur lorsque vous avez terminé. Aussi, mon Molécules application utilise des threads d'arrière-plan pour le traitement des nouvelles structures, avec une barre d'état qui est mis à jour sur ce projet. Vous pouvez télécharger le code source pour voir comment j'ai atteint cet objectif.

11voto

Christopher Lloyd Points 536

Vous pouvez faire cela à plusieurs reprises dans une boucle et tout fonctionnera 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.

9voto

hotpaw2 Points 40796

Afin d'obtenir drawRect appelé le plus rapidement possible (ce qui n'est pas forcément immédiatement, comme le système d'exploitation peut encore attendre jusqu'à ce que, par exemple, le matériel suivant rafraîchissement d'affichage, etc.), une application doit ralenti c'est l'INTERFACE utilisateur d'exécuter la boucle dès que possible, en quittant tout et toutes les méthodes dans le thread de l'INTERFACE utilisateur, et pour un non-zéro d'un montant de temps.

Vous pouvez soit le faire dans le thread principal en coupant tout traitement qui prend plus d'une image d'animation de temps dans des morceaux plus courts et la planification de la poursuite des travaux qu'après un court délai (donc drawRect peut exécuter les lacunes), ou en faisant le traitement dans un thread d'arrière-plan, avec un appel périodique pour performSelectorOnMainThread faire un setNeedsDisplay à quelques raisonnable cadence d'images d'animation.

Un non-OpenGL méthode pour mettre à jour l'affichage à proximité immédiate (ce qui signifie à la très prochaine de matériel de rafraîchissement ou trois) est en échangeant visible CALayer contenu avec une image ou CGBitmap que vous avez dessiné en. Une application peut faire Quartz dessin dans une Base de Graphiques bitmap à peu près à tout moment.

Ajout d'une nouvelle réponse:

Veuillez voir Brad Larson commentaires ci-dessous et Christopher Lloyd commentaire sur une autre réponse ici comme un indice menant à cette solution.

[ CATransaction flush ];

va provoquer drawRect être appelée sur les points de vue sur un setNeedsDisplay demande qui a été fait, même si la chasse est effectuée à partir de l'intérieur d'une méthode de blocage de l'INTERFACE utilisateur d'exécuter la boucle.

Notez que, lors du blocage du thread d'INTERFACE utilisateur, un Core Animation de chasse d'eau est nécessaire pour mettre à jour en changeant CALayer contenu. Donc, pour l'animation de contenus graphiques pour montrer les progrès accomplis, ceux-ci peuvent à la fois être les formes de la même chose.

Ajout d'une nouvelle note à l'ajout d'une nouvelle réponse ci-dessus:

Ne pas vider plus vite que votre drawRect ou d'animation de dessin peut remplir, car cela pourrait file d'attente des bouffées de chaleur, provoquant d'étranges effets d'animation.

4voto

Dave DeLong Points 156978

Sans remettre en question la sagesse de ce (que vous devez faire), vous pouvez le faire:

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

-setNeedsDisplay marquera le point de vue en tant que besoin d'être redessiné. -displayIfNeeded va forcer l'affichage de la couche de support à redessiner, mais seulement s'il a été marqué comme devant être affiché.

Je vais souligner, cependant, que votre question est révélatrice d'une architecture qui pourrait utiliser un peu de re-travail. Dans tous les mais en de rares cas, vous ne devez ou voulez forcer une vue de redessiner immédiatement. UIKit avec pas construite avec que de cas d'utilisation à l'esprit, et si cela fonctionne, considérez-vous chanceux.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X