L'affichage de l'interface utilisateur est une action qui consiste à générer une image depuis votre application et à l'afficher à l'écran. Pour garantir une interaction fluide entre l'utilisateur et votre application, elle doit afficher des images en moins de 16 ms pour obtenir 60 images par seconde (pourquoi 60 fps ?). Si votre application présente un problème d'affichage lent de l'UI, le système est obligé de sauter des images, de telle sorte que l'utilisateur constate un rendu saccadé dans votre application. On parle d'à-coups.
Pour vous aider à améliorer la qualité de votre application, Android contrôle automatiquement les à-coups et affiche les informations correspondantes dans le tableau de bord Android Vitals. Pour en savoir plus sur la collecte des données, consultez la documentation de la Play Console.
Si votre application présente des à-coups, cette page fournit des conseils sur le diagnostic et la résolution de ce problème.
Identifier les à-coups
Il peut être difficile d'identifier le code de votre application qui provoque un à-coup. Cette section décrit trois méthodes d'identification des à-coups :
L'examen visuel vous permet de parcourir rapidement tous les cas d'utilisation de votre application en quelques minutes, mais il ne fournit pas autant d'informations que Systrace. Systrace permet d'obtenir plus d'informations, mais si vous exécutez Systrace pour tous les cas d'utilisation de votre application, vous serez inondé de données qu'il vous sera difficile d'analyser. L'examen visuel et Systrace détectent les à-coups sur votre appareil local. Si l'à-coup ne peut pas être reproduit sur des appareils locaux, vous pouvez créer un contrôle personnalisé des performances pour mesurer des parties spécifiques de votre application sur les appareils qui fonctionnent dans le champ.
L'examen visuel
L'examen visuel vous aide à identifier les cas d'utilisation qui génèrent des à-coups. Pour effectuer un examen visuel, ouvrez votre application, parcourez manuellement les différentes parties de votre application et recherchez l'interface utilisateur qui fonctionne mal. Voici quelques conseils pour effectuer des examens visuels :
- Exécutez une version de votre application (ou au moins une version non débogable). L'environnement d'exécution ART désactive plusieurs optimisations importantes afin d'accepter les fonctionnalités de débogage. Veillez donc à ce que l'écran ressemble à ce que voit l'utilisateur.
- Activez le rendu GPU du profil. Le rendu GPU du profil affiche à l'écran des barres qui vous donnent une représentation visuelle rapide du délai nécessaire pour afficher les images d'une fenêtre d'interface utilisateur par rapport à la fréquence de 16 ms par image. Chaque barre contient des composants colorés qui correspondent à une étape du pipeline de rendu. Vous pouvez ainsi voir quelle étape prend le plus de temps. Par exemple, si l'image met beaucoup de temps à gérer les entrées, vous devez examiner le code de votre application qui gère les entrées utilisateur.
- Certains composants tels que
RecyclerView
génèrent fréquemment des à-coups. Si votre application utilise ces composants, nous vous recommandons de parcourir les parties correspondantes de l'application. - Parfois, un à-coup ne peut être reproduit que si l'application est lancée via un démarrage à froid.
- Pour résoudre le problème, essayez d'exécuter votre application sur un appareil plus lent.
Une fois que vous aurez identifié les cas d'utilisation qui génèrent un à-coup, cela vous renseignera peut-être sur la cause du problème. Toutefois, si vous avez besoin d'informations supplémentaires, vous pouvez utiliser Systrace.
Systrace
Bien que Systrace soit un outil qui montre ce que fait l'ensemble de l'appareil, il peut s'avérer utile pour identifier les à-coups dans votre application. Systrace engendre des frais généraux système minimes. Vous observerez donc des à-coups réalistes lors de l'instrumentation.
Gardez une trace avec Systrace tout en exécutant le cas d'utilisation qui fonctionne mal sur votre appareil. Consultez le tutoriel Systrace pour savoir comment utiliser Systrace. L'outil Systrace se compose de processus et de threads. Recherchez le processus de votre application dans Systrace, qui doit ressembler à la figure 1.
Figure 1 : Systrace
L'outil Systrace de la figure 1 contient les informations suivantes pour identifier les à-coups :
- Systrace affiche le moment où chaque image est dessinée, et associe un code couleur à chaque image pour mettre en évidence les délais d'affichage lents. Cette méthode vous permet de trouver les images qui fonctionnent mal de manière plus précise que via l'examen visuel. Pour en savoir plus, consultez Inspecter les cadres et les alertes d'interface utilisateur.
- Systrace détecte les problèmes dans votre application et affiche les alertes dans des images séparées et dans le panneau alertes. Nous vous recommandons de suivre les instructions fournies dans l'alerte.
- Certaines parties du framework et des bibliothèques Android telles que
RecyclerView
contiennent des repères de trace. Ainsi, la chronologie Systrace indique à quel moment ces méthodes sont exécutées sur le thread UI, et le temps nécessaire à leur exécution.
Après avoir examiné la sortie Systrace, il est possible que des méthodes dans votre application soient à l'origine des à-coups. Par exemple, si la chronologie montre qu'un affichage lent est dû à la lenteur de RecyclerView
, vous pouvez ajouter des repères de trace au code approprié et réexécuter Systrace pour en savoir plus. Dans le nouvel outil Systrace, la chronologie indique quand les méthodes de votre application sont appelées, et combien de temps est nécessaire à leur exécution.
Si Systrace ne vous indique pas pourquoi le thread UI prend beaucoup de temps, vous devez utiliser le Profileur de processeur Android pour enregistrer une trace de méthode échantillonnée ou instrumentée. En général, les traces de méthode ne sont pas efficaces pour identifier les à-coups, car elles produisent des faux positifs en raison d'importants frais généraux, et ne permettent pas de voir si les threads sont en cours d'exécution ou s'ils sont bloqués. Toutefois, les traces de méthode peuvent vous aider à identifier les méthodes les plus longues dans votre application. Après avoir identifié ces méthodes, ajoutez des repères de trace et réexécutez Systrace pour voir si ces méthodes entraînent des à-coups.
Pour en savoir plus, consultez Comprendre Systrace.
Le contrôle personnalisé des performances
Si vous ne parvenez pas à reproduire les à-coups sur un appareil local, vous pouvez intégrer un contrôle personnalisé des performances dans votre application afin d'identifier ce qui cause les à-coups sur les appareils dans le champ.
Pour ce faire, collectez les délais d'affichage à partir de parties spécifiques de votre application via FrameMetricsAggregator
, et enregistrez et analysez les données à l'aide de Firebase Performance Monitoring.
Pour en savoir plus, consultez Utiliser Firebase Performance Monitoring avec Android Vitals.
Corriger les à-coups
Pour résoudre ce problème, examinez les images qui ne s'affichent pas en l'espace de 16,7 ms et recherchez le problème. Est-ce Record View#draw qui prend trop longtemps pour certaines images, ou s'agit-il plutôt de Layout ? Consultez les causes courantes d'à-coups ci-dessous pour ces problèmes, entre autres.
Pour éviter les à-coups, les tâches de longue durée doivent être exécutées de manière asynchrone en dehors du thread UI. Soyez toujours conscient du thread sur lequel s'exécute votre code et faites preuve de prudence lorsque vous publiez des tâches importantes dans le thread principal.
Si vous disposez d'une UI principale complexe qui est importante pour votre application (comme la liste de défilement centrale), envisagez d'écrire des tests d'instrumentation pouvant détecter automatiquement les affichages lents et effectuez régulièrement des tests pour éviter les régressions. Pour en savoir plus, consultez l'atelier de programmation sur les tests de performances automatisés.
Cause des problèmes courants d'à-coups
Les sections suivantes décrivent les causes courantes des problèmes d'à-coups dans les applications et les bonnes pratiques pour y remédier.
Listes déroulantes
ListView
et surtout RecyclerView
sont couramment utilisés pour les listes déroulantes complexes, les plus susceptibles de subir des à-coups. Ils contiennent tous deux des repères Systrace. Vous pouvez donc utiliser Systrace pour déterminer s'ils contribuent aux à-coups dans votre application. Veillez à transmettre l'argument de ligne de commande -a
<your-package-name>
pour faire apparaître les sections de trace dans RecyclerView (ainsi que tous les repères de trace que vous avez ajoutés). Si possible, suivez les instructions des alertes générées dans la sortie Systrace. Dans Systrace, vous pouvez cliquer sur les sections suivies par RecyclerView pour obtenir une explication du calcul effectué par RecyclerView.
RecyclerView : notifyDataSetChanged
Si tous les éléments de votre RecyclerView
sont reconstruits (et donc redisposés et redessinés) dans une seule image, assurez-vous de ne pas appeler notifyDataSetChanged()
, setAdapter(Adapter)
ou swapAdapter(Adapter, boolean)
pour les petites mises à jour. Ces méthodes signalent que l'intégralité du contenu de la liste a changé, ce qui apparaît dans Systrace sous la forme RV FullInvalidate. Utilisez plutôt SortedList
ou DiffUtil
pour générer des mises à jour minimales lorsque du contenu est modifié ou ajouté.
Prenons l'exemple d'une application qui reçoit une nouvelle version d'une liste de contenus d'actualités du serveur. Lorsque vous publiez ces informations sur l'adaptateur, il est possible d'appeler notifyDataSetChanged()
, comme indiqué ci-dessous :
Kotlin
fun onNewDataArrived(news: List<News>) { myAdapter.news = news myAdapter.notifyDataSetChanged() }
Java
void onNewDataArrived(List<News> news) { myAdapter.setNews(news); myAdapter.notifyDataSetChanged(); }
Toutefois, cela présente un inconvénient majeur : s'il s'agit d'un changement mineur (comme un élément ajouté en haut de la liste), RecyclerView
ne le sait pas : il est invité à supprimer l'ensemble de l'état de l'élément mis en cache, et doit donc tout relier.
Il est préférable d'utiliser DiffUtil
, qui calcule et envoie très peu de mises à jour.
Kotlin
fun onNewDataArrived(news: List<News>) { val oldNews = myAdapter.items val result = DiffUtil.calculateDiff(MyCallback(oldNews, news)) myAdapter.news = news result.dispatchUpdatesTo(myAdapter) }
Java
void onNewDataArrived(List<News> news) { List<News> oldNews = myAdapter.getItems(); DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news)); myAdapter.setNews(news); result.dispatchUpdatesTo(myAdapter); }
Définissez simplement MyCallback en tant qu'implémentation DiffUtil.Callback
pour indiquer à DiffUtil
comment examiner vos listes.
RecyclerView : RecyclerViews imbriquées
Il est courant d'imbriquer des éléments RecyclerView
, en particulier avec une liste verticale de listes déroulantes horizontales (comme les grilles d'applications sur la page principale du Play Store). Cette solution peut être très efficace, mais elle génère de nombreuses vues en mouvement. Si vous constatez qu'un grand nombre d'éléments internes gonflent lorsque vous faites défiler la page pour la première fois, vous pouvez vérifier que vous partagez des RecyclerView.RecycledViewPool
s entre les vues RecyclerView intérieures (horizontales). Par défaut, chaque RecyclerView dispose de son propre pool d'éléments. Lorsqu'une dizaine de itemViews
s'affichent à l'écran à la fois, cela pose problème lorsque itemViews
ne peut pas être partagé par les différentes listes horizontales si toutes les lignes présentent des types de vues similaires.
Kotlin
class OuterAdapter : RecyclerView.Adapter<OuterAdapter.ViewHolder>() { ... override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { // inflate inner item, find innerRecyclerView by ID… val innerLLM = LinearLayoutManager(parent.context, LinearLayoutManager.HORIZONTAL, false) innerRv.apply { layoutManager = innerLLM recycledViewPool = sharedPool } return OuterAdapter.ViewHolder(innerRv) } ...
Java
class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> { RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool(); ... @Override public void onCreateViewHolder(ViewGroup parent, int viewType) { // inflate inner item, find innerRecyclerView by ID… LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(), LinearLayoutManager.HORIZONTAL); innerRv.setLayoutManager(innerLLM); innerRv.setRecycledViewPool(sharedPool); return new OuterAdapter.ViewHolder(innerRv); } ...
Si vous souhaitez optimiser davantage, vous pouvez également appeler setInitialPrefetchItemCount(int)
sur le LinearLayoutManager
du RecyclerView interne. Par exemple, si vous avez toujours 3,5 éléments visibles à la suite, appelez innerLLM.setInitialItemPrefetchCount(4);
. Cela signalera à RecyclerView que lorsqu'une ligne horizontale est sur le point d'apparaître à l'écran, il doit tenter de précharger les éléments qui s'y trouvent s'il reste du temps sur le thread UI.
RecyclerView : le gonflage est trop important/la création prend trop de temps
Dans la plupart des cas, la fonctionnalité de préchargement dans RecyclerView
permet de limiter le coût du gonflage en effectuant le calcul à l'avance, lorsque le thread UI est inactif. Si vous constatez un gonflage sur une image (et non sur une section intitulée RV Prefetch (Précharger RV)), veillez à effectuer le test sur un appareil récent (le préchargement n'est actuellement compatible qu'au niveau d'API 21 sur Android 5.0 et niveau supérieur), en utilisant une version récente de la bibliothèque Support.
Si vous constatez que des gonflages provoquent souvent des à-coups lorsque de nouveaux éléments s'affichent à l'écran, vérifiez que vous ne disposez pas de plus de types de vues que nécessaire. Moins il y a de types de vues dans le contenu d'une RecyclerView, moins il faut avoir recours au gonflage lorsque de nouveaux types d'éléments s'affichent à l'écran. Si possible, fusionnez les types de vues lorsque cela est raisonnable. Si seule une icône, une couleur ou un texte change d'un type à l'autre, vous pouvez effectuer cette modification au moment de la liaison et éviter le gonflage (ce qui permet de réduire l'encombrement de la mémoire de votre application par la même occasion).
Si vos types de vues sont corrects, pensez à réduire le coût du gonflage.
Il peut être utile de réduire les vues structurelles et de conteneurs inutiles. Pensez à créer des itemViews
avec ConstraintLayout, qui peut faciliter la réduction des vues structurelles. Si vous souhaitez vraiment optimiser les performances, si vos hiérarchies d'éléments sont simples et si vous n'avez pas besoin de fonctionnalités de thématisation et de style complexes, envisagez d'appeler vous-même les constructeurs. Notez cependant que cela n'est souvent pas utile, car vous risquez de perdre la simplicité et les fonctionnalités de XML.
RecyclerView : la liaison prend trop de temps
La liaison (soit onBindViewHolder(VH, int)
) doit être très simple et prendre bien moins d'une milliseconde pour tous les éléments, sauf les plus complexes. Elle doit simplement récupérer des éléments POJO à partir des données d'éléments internes de votre adaptateur et appeler des setters sur les vues dans le ViewHolder. Si RV OnBindView prend beaucoup de temps, vérifiez que le code de liaison est simple à réaliser.
Si vous utilisez des objets POJO simples pour stocker des données dans votre adaptateur, vous pouvez complètement vous passer d'écrire le code de liaison dans onBindViewHolder en utilisant la bibliothèque de liaison de données.
RecyclerView ou ListView : la mise en page/le dessin prend trop de temps
Pour les problèmes de dessin et de mise en page, consultez les sections Mise en page et Performances d'affichage.
ListView : gonflage
Le recyclage peut facilement être désactivé par mégarde dans ListView
. Si un gonflage se produit à chaque fois qu'un élément s'affiche à l'écran, vérifiez que votre implémentation de Adapter.getView()
utilise le paramètre convertView
, effectue à nouveau la liaison avec lui et le renvoie. Si votre implémentation getView()
est toujours gonflée, votre application ne bénéficiera pas des avantages du recyclage dans ListView. La structure de votre getView()
devrait presque toujours être semblable à l'implémentation ci-dessous :
Kotlin
fun getView(position: Int, convertView: View?, parent: ViewGroup): View { return (convertView ?: layoutInflater.inflate(R.layout.my_layout, parent, false)).apply { // … bind content from position to convertView … } }
Java
View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { // only inflate if no convertView passed convertView = layoutInflater.inflate(R.layout.my_layout, parent, false) } // … bind content from position to convertView … return convertView; }
Performances de mise en page
Si Systrace indique que le segment Layout de Choreographer#doFrame est trop actif, ou actif trop souvent, cela signifie que vous rencontrez des problèmes de performances liés à la mise en page. Les performances de mise en page de votre application dépendent de la partie de la hiérarchie des vues qui disposent d'entrées ou de paramètres de mise en page différents.
Performances de mise en page : coût
Si les segments durent plus de quelques millisecondes, il est possible que vous obteniez des performances d'imbrication médiocres pour RelativeLayouts ou weighted-LinearLayouts. Chacune de ces mises en page peut déclencher plusieurs passes de mesure/de mise en page de ses enfants. L'imbrication peut donc entraîner un comportement O(n^2) sur la profondeur de l'imbrication. Essayez d'éviter la mise en page RelativeLayout ou la fonction pondération de LinearLayout dans tous les nœuds feuilles les plus bas de la hiérarchie. Pour ce faire, vous disposez de différentes manières :
- Vous pouvez réorganiser vos vues structurelles.
- Vous pouvez définir une logique de mise en page personnalisée. Pour un exemple spécifique, consultez Optimiser les hiérarchies de mise en page. Vous pouvez essayer de passer à ConstraintLayout, qui fournit des fonctionnalités similaires, sans les inconvénients en termes de performances.
Performances de mise en page : fréquence
Une mise en page est normalement effectuée lorsque de nouveaux contenus s'affichent à l'écran, par exemple lorsque la page défile jusqu'à faire apparaître un nouvel élément dans RecyclerView
. Si une mise en page importante est effectuée sur chaque image, il est possible que vous l'animiez, ce qui est susceptible d'entraîner la perte de certaines images. En règle générale, les animations doivent être exécutées sur les propriétés de dessin de View
(comme setTranslationX/Y/Z()
, setRotation()
, setAlpha()
, etc.). Ces paramètres peuvent être modifiés de manière beaucoup plus économique que les propriétés de mise en page (comme les marges ou la marge intérieure). Il est également beaucoup moins coûteux de modifier les propriétés de dessin d'une vue, généralement en appelant un setter qui déclenche un invalidate()
, suivi de draw(Canvas)
dans l'image suivante. Cette opération réenregistre les opérations de dessin pour la vue invalidée. De plus, elle est généralement beaucoup moins chère que la mise en page.
Performances d'affichage
L'interface utilisateur Android fonctionne en deux phases : Record View#draw, dans le thread UI, et DrawFrame sur RenderThread. La première exécute draw(Canvas)
sur chaque View
non valide, et peut effectuer des appels dans des vues personnalisées ou dans votre code.
La seconde s'exécute sur le RenderThread natif, mais fonctionne sur la base du calcul généré par la phase Record View#draw.
Performances d'affichage : thread UI
Si l'opération Record View#draw prend du temps, il arrive souvent qu'un bitmap soit peint dans le thread UI. Peindre sur un bitmap utilise l'affichage processeur. Essayez d'éviter cela dans la mesure du possible. Vous pouvez utiliser le traçage de méthode avec le Profileur de processeur Android pour voir si c'est le cas.
Un bitmap est souvent peint lorsqu'une application souhaite décorer un bitmap avant de l'afficher. Par exemple, vous pouvez ajouter des angles arrondis :
Kotlin
val paint = Paint().apply { isAntiAlias = true } Canvas(roundedOutputBitmap).apply { // draw a round rect to define shape: drawRoundRect( 0f, 0f, roundedOutputBitmap.width.toFloat(), roundedOutputBitmap.height.toFloat(), 20f, 20f, paint ) paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY) // multiply content on top, to make it rounded drawBitmap(sourceBitmap, 0f, 0f, paint) setBitmap(null) // now roundedOutputBitmap has sourceBitmap inside, but as a circle }
Java
Canvas bitmapCanvas = new Canvas(roundedOutputBitmap); Paint paint = new Paint(); paint.setAntiAlias(true); // draw a round rect to define shape: bitmapCanvas.drawRoundRect(0, 0, roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 20, 20, paint); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)); // multiply content on top, to make it rounded bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint); bitmapCanvas.setBitmap(null); // now roundedOutputBitmap has sourceBitmap inside, but as a circle
Si c'est le genre de tâche que vous effectuez dans le thread UI, vous pouvez le faire sur le thread de décodage en arrière-plan. Dans certains cas, comme celui-ci, vous pouvez même effectuer cette tâche au moment du dessin. Par conséquent, si votre code Drawable
ou View
se présente comme suit :
Kotlin
fun setBitmap(bitmap: Bitmap) { mBitmap = bitmap invalidate() } override fun onDraw(canvas: Canvas) { canvas.drawBitmap(mBitmap, null, paint) }
Java
void setBitmap(Bitmap bitmap) { mBitmap = bitmap; invalidate(); } void onDraw(Canvas canvas) { canvas.drawBitmap(mBitmap, null, paint); }
Vous pouvez le remplacer par le code suivant :
Kotlin
fun setBitmap(bitmap: Bitmap) { shaderPaint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) invalidate() } override fun onDraw(canvas: Canvas) { canvas.drawRoundRect(0f, 0f, width, height, 20f, 20f, shaderPaint) }
Java
void setBitmap(Bitmap bitmap) { shaderPaint.setShader( new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP)); invalidate(); } void onDraw(Canvas canvas) { canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint); }
Notez que cela permet souvent de protéger l'arrière-plan (dessiner un gradient par-dessus le bitmap) et le filtrage d'image (avec ColorMatrixColorFilter
), deux autres opérations courantes effectuées pour modifier les bitmaps.
Si vous souhaitez dessiner sur un bitmap pour une autre raison, par exemple pour l'utiliser comme cache, essayez de dessiner directement sur le canevas accéléré par le matériel qui est transmis à votre vue ou à votre dessin. Si nécessaire, envisagez d'appeler setLayerType()
avec LAYER_TYPE_HARDWARE
pour mettre en cache la sortie d'affichage complexe tout en tirant parti du rendu GPU.
Performances d'affichage : RenderThread
L'enregistrement de certaines opérations de canevas n'est pas coûteux en soi, mais déclenche des calculs coûteux sur le RenderThread. Systrace les signale généralement via des alertes.
Canvas.saveLayer()
Évitez d'utiliser Canvas.saveLayer()
, qui peut entraîner un affichage coûteux, non mis en cache et hors écran pour chaque image. Bien que les performances se soient améliorées sous Android 6.0 (lorsque des optimisations ont été apportées pour éviter de changer de cible d'affichage dans le GPU), il est toujours préférable d'éviter cette API coûteuse, si possible, ou au minimum de vous assurer que vous transmettez le Canvas.CLIP_TO_LAYER_SAVE_FLAG
(ou en appelant une variante qui n'accepte pas les indicateurs).
Animer des chemins d'accès volumineux
Lorsque Canvas.drawPath()
est appelé sur le canevas avec accélération matérielle transmise à Views, Android dessine d'abord ces chemins d'accès sur le processeur, puis les importe dans le GPU. Si vos chemins d'accès sont volumineux, évitez de les modifier d'une image à l'autre afin qu'ils puissent être mis en cache et dessinés plus efficacement. drawPoints()
, drawLines()
et drawRect/Circle/Oval/RoundRect()
sont plus efficaces : il est préférable de les utiliser même si vous finissez par utiliser plus d'appels de dessin.
Canvas.clipPath
clipPath(Path)
déclenche un comportement de bornement coûteux et doit généralement être évité. Si possible, optez pour le dessin de formes plutôt que de rogner des formes non rectangulaires. Cette méthode fonctionne mieux et est compatible avec l'anticrénelage. Par exemple, l'appel clipPath
suivant :
Kotlin
canvas.apply { save() clipPath(circlePath) drawBitmap(bitmap, 0f, 0f, paint) restore() }
Java
canvas.save(); canvas.clipPath(circlePath); canvas.drawBitmap(bitmap, 0f, 0f, paint); canvas.restore();
Peut être exprimé comme suit :
Kotlin
paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) // at draw time: canvas.drawPath(circlePath, mPaint)
Java
// one time init: paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP)); // at draw time: canvas.drawPath(circlePath, mPaint);
Importations de bitmaps
Android affiche les bitmaps sous forme de textures OpenGL. La première fois qu'un bitmap est affiché dans une image, il est importé dans le GPU. Vous pouvez le voir dans Systrace sous la forme Texture import(id) width x height. Cette opération peut prendre plusieurs millisecondes (voir figure 2), mais il est nécessaire d'afficher l'image avec le GPU.
Si cela prend beaucoup de temps, vérifiez d'abord les valeurs de largeur et de hauteur de la trace. Assurez-vous que le bitmap affiché n'est pas plus grand que la zone de l'écran. Si c'est le cas, vous perdrez du temps d'importation, et de la mémoire. Généralement, les bibliothèques de chargement bitmap permettent de demander facilement un bitmap de taille appropriée.
Sous Android 7.0, le code de chargement bitmap (généralement effectué par les bibliothèques) peut appeler prepareToDraw()
pour déclencher une importation anticipée, avant que cela ne soit nécessaire. De cette manière, l'importation se fait de façon précoce, lorsque le RenderThread est inactif. Vous pouvez effectuer cette opération après le décodage ou lors de la liaison d'un bitmap à une vue, à condition de connaître le bitmap. Idéalement, votre bibliothèque de chargement de bitmaps effectuera cette opération pour vous. Toutefois, si vous gérez la vôtre ou si vous souhaitez vous assurer de ne pas enregistrer d'importations sur des appareils plus récents, vous pouvez appeler prepareToDraw()
dans votre code.
Figure 2 : Une application passe beaucoup de temps à importer un bitmap volumineux dans une image. Réduisez sa taille ou déclenchez-la plus tôt quand elle est décodée avec prepareToDraw()
.
Retards de planification des threads
Le programmeur de threads est la partie du système d'exploitation Android qui choisit les threads du système et déterminent quand et pendant combien de temps ils doivent s'exécuter. Parfois, il peut y avoir des à-coups si le thread UI de votre application est bloqué ou ne s'exécute pas. Systrace utilise différentes couleurs (voir la figure 3) pour indiquer lorsqu'un thread est En veille (gris), ou Exécutable (en bleu : il peut s'exécuter, mais le programmeur ne l'a pas encore choisi pour s'exécuter), En cours d'exécution (vert) ou en Veille ininterrompue (rouge ou orange). C'est extrêmement utile pour déboguer les problèmes d'à-coups causés par les retards de planification des threads.
Figure 3 : Affiche une période de veille du thread UI.
Souvent, les longues pauses dans l'exécution de votre application sont causées par des appels de liaison, le mécanisme de communication inter-processus (IPC) sur Android. Dans les versions récentes d'Android, il s'agit de l'une des principales raisons susceptibles d'entraîner l'arrêt du thread UI. La solution consiste généralement à éviter d'appeler des fonctions qui effectuent des appels de liaison. Si c'est inévitable, vous devez mettre en cache la valeur ou déplacer la tâche dans un thread d'arrière-plan. Au fur et à mesure que le codebase s'agrandit, il est facile d'ajouter un appel de liaison par mégarde en appelant une méthode de bas niveau, mais il est tout aussi facile de le trouver et de le corriger via le traçage.
Si vous disposez de transactions de liaison, vous pouvez capturer leurs piles d'appels via les commandes adb suivantes :
$ adb shell am trace-ipc start
… use the app - scroll/animate ...
$ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
$ adb pull /data/local/tmp/ipc-trace.txt
Les appels qui semblent inoffensifs tels que getRefreshRate()
peuvent parfois déclencher des transactions de liaison et entraîner de gros problèmes lorsqu'ils sont appelés fréquemment. Le traçage régulier peut vous aider à détecter et à résoudre rapidement ces problèmes lorsqu'ils se présentent.
Figure 4 : Affiche le thread UI en veille en raison des transactions de liaison lors d'un geste vif RV. Simplifiez votre logique de liaison, et utilisez
trace-ipc
pour rechercher et supprimer les appels de liaison.
Si aucune activité de liaison ne s'affiche, mais que votre thread UI ne s'exécute pas, assurez-vous de ne pas attendre un verrouillage ou une autre opération d'un autre thread. En règle générale, le thread UI ne doit pas attendre les résultats d'autres threads. Les autres threads doivent y publier des informations.
Allocation d'objets et récupération de mémoire
Depuis l'introduction d'ART comme environnement d'exécution par défaut dans Android 5.0, l'allocation d'objets et la récupération de mémoire sont désormais beaucoup moins problématiques. Toutefois, il est toujours possible d'alourdir vos threads avec des tâches supplémentaires. Il est possible d'allouer des ressources en réponse à un événement rare (comme un utilisateur qui clique sur un bouton), mais n'oubliez pas que chaque allocation entraîne un coût. S'il s'agit d'une boucle serrée qui est appelée fréquemment, envisagez d'éviter l'allocation pour alléger la charge sur la récupération de mémoire.
Systrace vous indique si la récupération de mémoire s'exécute fréquemment, et le Profileur de mémoire d'Android vous indique la provenance des allocations. Si vous évitez les allocations dans la mesure du possible, en particulier dans les boucles serrées, vous ne devriez pas rencontrer de problème.
Figure 5 : Affiche un GC de 94 ms sur le thread HeapTaskDaemon
Dans les versions récentes d'Android, le GC s'exécute généralement sur un thread d'arrière-plan nommé HeapTaskDaemon. Notez qu'une quantité importante d'allocations peut signifier que davantage de ressources processeur sont utilisées pour le GC, comme le montre la figure 5.