Gel (figeage) est un état dans lequel une application mobile cesse de répondre à toute action de l'utilisateur pendant une période prolongée. Contrairement aux lags (ralentissement) et aux glitches (comportement incorrect), le gel bloque complètement l'UI : les touches ne sont pas traitées, l'animation s'arrête, l'écran se « fige ». La cause est le blocage du thread principal par une opération synchrone, un deadlock dans du code multithread ou un garbage collection anormalement long. Selon la Documentation Apple Main Thread Checker, plus de 40 % des rapports de crash iOS sont liés au blocage du thread principal. Sous Android, une situation similaire conduit à l'ANR — le dialogue système « L'application ne répond pas ».
Points clés
Gel (figeage) dans une application mobile est un état dans lequel l'application cesse de traiter les événements d'entrée et de mettre à jour l'interface pendant plusieurs secondes ou plus. Techniquement, cela signifie que le thread principal est bloqué et ne peut pas exécuter l'itération suivante de la boucle d'exécution.
Un lag est un retard allant jusqu'à 500 ms où l'utilisateur remarque un ralentissement mais l'application continue de fonctionner. Le gel dure de 1 seconde à des dizaines de secondes. L'ANR sous Android est un cas particulier de gel qui a duré plus de 5 secondes et a été détecté par le système. Tous les gels ne mènent pas à l'ANR, mais chaque ANR est un gel documenté par le système.
Sous Android, un gel de plus de 5 secondes déclenche une boîte de dialogue ANR proposant de fermer l'application. Sous iOS, le système dispose d'un watchdog — si l'application ne répond pas aux événements pendant 10–20 secondes, le Watchdog termine le processus avec le code 0x8badf00d (ate bad food). L'utilisateur voit seulement l'application se fermer soudainement vers l'écran d'accueil.
Toute opération qui prend plus de 100 ms et est lancée sur le thread principal peut potentiellement provoquer un gel. Examinons les principales sources de blocages.
Lire un gros fichier, une requête réseau sans asynchronie, sauvegarder des données dans SharedPreferences avec la méthode synchrone apply suivie de commit — toutes ces opérations bloquent le thread principal. Sous Android, lire de manière synchrone un fichier de 10 Mo peut prendre 200–500 ms selon la vitesse de la mémoire flash. Sous iOS, un chargement synchrone URLSession sans completionHandler bloque l'UI pendant le temps de réponse du serveur.
Lorsque deux threads attendent la libération de ressources détenues mutuellement, un deadlock se produit. Dans les applications mobiles, un scénario typique est que le thread A verrouille Lock1 et attend Lock2, tandis que le thread B verrouille Lock2 et attend Lock1. Les deux threads se figent pour toujours. Si l'un d'eux est le thread principal, l'application se fige complètement.
Une erreur de logique — par exemple, while(true) sans condition de sortie ou une récursion sans cas de base — entraîne une exécution infinie sur le thread principal. Android le détecte via ANR après 5 secondes, iOS — via Stackshot, qui capture une pile d'appels infiniment répétée.
Le diagnostic des gels nécessite des outils capables de capturer l'état de tous les threads au moment du blocage.
À chaque ANR, le système Android enregistre un fichier /data/anr/traces.txt contenant un dump de la pile de chaque thread de l'application. Analyser ce fichier est la méthode de diagnostic principale : trouvez le thread main et voyez sur quelle méthode il s'est arrêté. Si la pile se termine par Thread.sleep, InputStream.read ou Lock.lock — la cause est trouvée.
Xcode peut prendre un Stackshot — un instantané des piles de tous les threads — lorsque l'application se fige (signal SIGSTOP). Activez « Logging » → « Include Stackshot Logs » dans le schéma. Lors d'un crash avec le code 0x8badf00d, extrayez le log de crash depuis Devices & Simulators et trouvez le thread com.apple.main-thread avec la pile bloquée.
Main Thread Checker détecte automatiquement les appels UIKit depuis des threads d'arrière-plan pendant l'exécution de l'application. Activez-le dans le schéma (Diagnostics → Main Thread Checker). Chaque avertissement est une cause potentielle de gel, surtout s'il se produit dans une closure completionHandler d'une requête réseau.
Exemple de détection de blocage via StrictMode sous Android :
class App : Application() {
override fun onCreate() {
super.onCreate()
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build())
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyDeath()
.build())
}
}
L'élimination des gels commence par le déplacement de toutes les opérations potentiellement longues vers des threads d'arrière-plan. Examinons des techniques spécifiques pour chaque plateforme.
Kotlin Coroutines avec viewModelScope.launch(Dispatchers.IO) garantissent que les opérations réseau ou les lectures de base de données s'exécutent dans un thread d'arrière-plan. Dispatchers.Main est utilisé uniquement pour les mises à jour de l'UI. Important : toutes les fonctions suspend doivent être structurées — les coroutines enfants sont annulées lorsque le parent est annulé, empêchant les fuites de threads.
Grand Central Dispatch avec DispatchQueue.global(qos: .userInitiated) pour les tâches d'arrière-plan et DispatchQueue.main.async pour les mises à jour UI est le modèle standard. Évitez sync() sur la file principale — c'est un deadlock garanti. Utilisez async/await (Swift 5.5+) pour un code asynchrone plus lisible avec retour automatique sur le thread principal via MainActor.
Les blocs synchronized dans Kotlin et @synchronized dans Swift sur le thread principal sont dangereux : si un autre thread a déjà acquis ce verrou, le thread principal se figera en attendant. Utilisez des types atomiques (AtomicInteger, propriétés atomiques dans Swift) ou des files d'attente sérialisées au lieu de verrous.
Exemple de chargement asynchrone de données avec coroutines sous Android :
class DataViewModel : ViewModel() {
private val _data = MutableStateFlow<List<Item>>(emptyList())
val data: StateFlow<List<Item>> = _data.asStateFlow()
fun loadData() {
viewModelScope.launch(Dispatchers.IO) {
val result = fetchFromNetwork()
_data.emit(result)
}
}
}
Une combinaison d'outils, de principes architecturaux et de processus de révision de code aide à prévenir systématiquement les gels.
Configurez StrictMode avec penaltyDeath pour les politiques de thread — cela entraînera un crash immédiat de l'application lorsqu'un appel réseau ou une E/S disque est détecté sur le thread principal. Le développeur ne pourra pas ignorer le problème. Dans les builds de production, utilisez penaltyLog pour collecter des statistiques sans crash.
Sous iOS, activez Main Thread Checker dans le schéma Debug et configurez le CI pour exécuter des tests avec cette option. Si un test contient un appel UIKit depuis un thread d'arrière-plan — il doit échouer. C'est la seule façon fiable d'identifier le problème avant l'envoi à TestFlight.
Ajoutez un point obligatoire au processus de révision de code : vérifier que tout appel réseau, opération fichier, accès à la base de données ou calcul lourd s'exécute dans un thread d'arrière-plan. Le Deadlock peut être détecté avec des analyseurs statiques : Infer de Facebook et Thread Safety Checker d'Xcode trouvent les verrous potentiels avant l'exécution.
Questions fréquentes
ANR (Application Not Responding) est une notification système Android qui apparaît lorsque le thread principal gèle pendant plus de 5 secondes. Le gel est un concept plus large : tout blocage de l'UI, quelle que soit sa durée. iOS n'a pas d'ANR, mais dispose d'un Watchdog avec un délai de 10–20 secondes.
Le fichier se trouve dans /data/anr/traces.txt. L'accès nécessite un accès root ou adb shell : exécutez adb shell cat /data/anr/traces.txt \> traces.txt avec les droits root. Dans la pile, trouvez le thread « main » — la dernière méthode appelée indique la cause du blocage.
Si le gel dure moins de 10 secondes, le Watchdog ne se déclenche pas et l'application « reste bloquée » jusqu'à la fin de l'opération bloquante. L'utilisateur ne voit pas de crash mais ressent de la frustration. Pour détecter ces cas, utilisez MetricKit avec des traces personnalisées de temps d'exécution.
Utilisez des tests UI avec vérification que l'écran s'ouvre en moins d'1 seconde. Ajoutez au CI la mesure du temps entre le tap et l'apparition de l'écran suivant. Sous Android, utilisez Espresso avec IdlingResource pour attendre les opérations asynchrones. Sous iOS, utilisez XCTest avec XCTWaiter pour vérifier le temps de chargement.
SwiftUI lui-même ne provoque pas de gels, mais des calculs complexes dans la propriété body, si. Si le calcul de body prend 500 ms en raison d'opérations lourdes, l'UI se fige. La solution consiste à décharger les calculs dans Task.detached et à mettre à jour @State de manière asynchrone sur l'acteur principal.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi