web-dev-qa-db-fra.com

drawViewHierarchyInRect: afterScreenUpdates: retarde les autres animations

Dans mon application, j'utilise drawViewHierarchyInRect:afterScreenUpdates: afin d'obtenir une image floue de ma vue (en utilisant la catégorie UIImage d'Apple IImageEffects ).

Mon code ressemble à ceci:

UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
[self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:YES];
UIImage *im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
/* Use im */

J'ai remarqué pendant le développement que beaucoup de mes animations ont été retardées après avoir utilisé mon application pendant un certain temps, c'est-à-dire que mes vues ont commencé leurs animations après une pause notable (mais moins d'une seconde environ) par rapport à un nouveau lancement de l'application.

Après quelques débogages, j'ai remarqué que le simple fait d'utiliser drawViewHierarchyInRect:afterScreenUpdates: avec des mises à jour d'écran définies sur YES a provoqué ce retard. Si ce message n'a jamais été envoyé lors d'une session d'utilisation, le retard n'est jamais apparu. L'utilisation de NO pour le paramètre de mise à jour de l'écran a également fait disparaître le retard.

Ce qui est étrange, c'est que ce code flou est complètement indépendant (pour autant que je sache) des animations retardées. Les animations en question n'utilisent pas drawViewHierarchyInRect:afterScreenUpdates:, ce sont des animations CAKeyframeAnimation. Le simple fait d'envoyer ce message (avec des mises à jour d'écran définies sur YES) semble avoir affecté globalement les animations dans mon application.

Que se passe-t-il?

(J'ai créé des vidéos illustrant l'effet: avec et sans un délai d'animation. Notez le délai d'apparition de la bulle de dialogue "Vérifier!" Dans la barre de navigation.)

METTRE À JOUR

J'ai créé un exemple de projet pour illustrer ce bogue potentiel. https://github.com/timarnold/AnimationBugExample

MISE À JOUR N ° 2

J'ai reçu une réponse de Apple vérifiant qu'il s'agit d'un bogue. Voir la réponse ci-dessous .

28
Tim Camber

J'ai utilisé l'un de mes Apple tickets d'assistance aux développeurs pour demander Apple à propos de mon problème.

Il s'avère que c'est un bug confirmé (numéro de radar 17851775). Leur hypothèse pour ce qui se passe est ci-dessous:

La méthode drawViewHierarchyInRect: afterScreenUpdates: effectue ses opérations sur le GPU autant que possible, et une grande partie de ce travail se fera probablement en dehors de l'espace d'adressage de votre application dans un autre processus. Passer YES en tant que paramètre afterScreenUpdates: pour drawViewHierarchyInRect: afterScreenUpdates: entraînera une animation de base à vider tous ses tampons dans votre tâche et dans la tâche de rendu. Comme vous pouvez l'imaginer, il y a aussi beaucoup d'autres choses internes qui se produisent dans ces cas. L'ingénierie théorise que cela pourrait très bien être un bug dans cette machine lié à l'effet que vous voyez.

En comparaison, la méthode renderInContext: effectue ses opérations à l'intérieur de l'espace d'adressage de votre application et n'utilise pas le processus basé sur GPU pour effectuer le travail. Pour la plupart, il s'agit d'un chemin de code différent et s'il fonctionne pour vous, c'est une solution de contournement appropriée. Cette route n'est pas aussi efficace qu'elle n'utilise pas la tâche basée sur le GPU. En outre, il n'est pas aussi précis pour les captures d'écran qu'il peut exclure les flous et autres fonctionnalités Core Animation gérées par la tâche GPU.

Et ils ont également fourni une solution de contournement. Ils ont suggéré qu'au lieu de:

UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
[self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:YES];
UIImage *im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
/* Use im */

Je devrais faire ça

UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
[self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
/* Use im */

J'espère que cela est utile pour quelqu'un!

68
Tim Camber

Avez-vous essayé d'exécuter votre code sur un thread d'arrière-plan? Voici un exemple utilisant gcd:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
    //background thread
    UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
    [self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:YES];
    UIImage *im = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    dispatch_sync(dispatch_get_main_queue(), ^(void) {
        //update ui on main thread
    });
});
1
Sam

Pourquoi avez-vous cette ligne (à partir de votre exemple d'application):

animation.beginTime = CACurrentMediaTime();

Retirez-le et tout sera comme vous le souhaitez.

En définissant explicitement le temps d'animation sur CACurrentMediaTime(), vous ignorez les éventuelles transformations temporelles qui peuvent être présentes dans l'arborescence des couches. Soit ne le définissez pas du tout (par défaut, les animations démarreront now), soit utilisez la méthode de conversion temporelle:

animation.beginTime = [view.layer convert​Time:CACurrentMediaTime() from​Layer:nil];

UIKit ajoute des transformations temporelles à l'arborescence des calques lorsque vous appelez afterScreenUpdates:YES Pour éviter les sauts dans l'animation en cours, qui seraient sinon causés par des validations intermédiaires de CoreAnimation. Si vous souhaitez démarrer l'animation à un moment précis (pas now), utilisez la méthode de conversion temporelle mentionnée ci-dessus.

Et pendant que vous y êtes, préférez fortement utiliser -[UIView snapshotViewAfterScreenUpdates:] Et amis au lieu de -[UIView drawViewHierarchyInRect:] Famille (en spécifiant de préférence NO pour afterScreenUpdates partie). Dans la plupart des cas, vous n'avez pas vraiment besoin d'une image persistante et la vue instantanée est ce que vous voulez réellement. L'utilisation d'un instantané de vue au lieu d'une image rendue présente les avantages suivants:

  • 2x-10x plus rapide
  • Utilise 2x-3x moins de mémoire
  • Il utilisera toujours un espace colorimétrique et un format de tampon corrects (par exemple sur les appareils avec un grand écran couleur)
  • Il utilisera une échelle et une orientation correctes, vous n'avez donc pas besoin de savoir comment positionner votre image pour qu'elle soit belle.
  • Il fonctionne mieux avec les fonctionnalités d'accessibilité (par exemple avec les couleurs Smart Invert)
  • La vue instantanée capture également correctement les vues hors processus et sécurisées (tandis que drawViewHierarchyInRect les rendra en noir ou blanc).
1
wonder.mice

Lorsque le paramètre afterScreenUpdates est défini sur YES, le système doit attendre que toutes les mises à jour d'écran en attente se soient produites avant de pouvoir afficher la vue.

Si vous lancez des animations en même temps, le rendu et les animations tentent peut-être de se produire ensemble, ce qui entraîne un retard.

Il vaut peut-être la peine d'essayer de lancer vos animations un peu plus tard pour éviter cela. Évidemment non trop beaucoup plus tard car cela vaincrait l'objet, mais un petit intervalle dispatch_after valait la peine d'être essayé.

1
jrturton