1. Origines
Pourquoi mon application est-elle si bloquée ? Qui a empoisonné le code ?
Un jour, j'ai soudainement découvert que le package de débogage fonctionnait extrêmement lentement. Après le test simple suivant, j'ai découvert qu'il y avait un problème avec le package de débogage sur Android 14.
2. Enregistrements de dépannage
Moyens d'enquête de routine
Systrace et l'outil de trace du package de débogage interne dutrace sont utilisés pour le dépannage.
Conclusion : le CPU est inactif et le thread principal n'est visiblement pas bloqué. Il semble que l'exécution de la méthode pure prenne du temps.
J'ai trouvé des doutes
Il n'y a pas eu de gros gain dans la première étape de dépannage, mais j'ai trouvé une anomalie lorsque j'ai utilisé l'outil dutrace pour dépanner. Voici une brève introduction au principe de mise en œuvre de dutrace :
Dutrace utilise un hook en ligne pour ajouter des points de trace avant et après l'exécution de la méthode art, puis l'affiche via l'outil d'interface utilisateur perfetto. Il présente les avantages suivants :
1. Prend en charge l'analyse hors ligne du processus d'exécution des fonctions et des fonctions chronophages.
2. Sous le processus d'appel de la fonction d'analyse :
a. Vous pouvez afficher les appels de fonction de l'ensemble du processus (y compris les fonctions du framework) ;
b. Possibilité de spécifier des fonctions et des threads surveillés pour filtrer efficacement les traces inutiles ;
c. La configuration dynamique ne nécessite pas de reconditionnement.
3. Vous pouvez utiliser des outils d'analyse d'interface utilisateur prêts à l'emploi, y compris les appels de fonction des threads système clés, tels que le temps de rendu, les verrous de thread, le temps GC, etc., ainsi que les opérations d'E/S, la charge du processeur et d'autres événements.
organigramme
Lors de l'accrochage avant et après l'exécution de la méthode artistique, il s'agit de traiter trois situations d'interprétation et d'exécution de la méthode artistique.
Interpréteur d'exécution ART
- L'interpréteur C++, qui est l'interpréteur traditionnel basé sur la structure du commutateur, n'utilise généralement cette branche que lorsque l'environnement de débogage, le suivi des méthodes, les instructions ne sont pas pris en charge, ou lorsqu'une exception se produit dans le bytecode (comme un échec de vérification du verrouillage structuré). .
- L'interpréteur rapide mterp, à la base, introduit une table de gestionnaires pour le mappage d'instructions et implémente une commutation rapide entre les instructions via un assemblage manuscrit, améliorant ainsi les performances de l'interpréteur.
- Nterp est une autre optimisation de Mterp. Nterp élimine le besoin de maintenance des piles de code géré, utilise la même structure de trame de pile que la méthode native, et l'ensemble du processus d'exécution de décodage et de traduction est implémenté par du code assembleur, réduisant ainsi davantage l'écart de performances entre l'interpréteur et le code compilé.
J'ai découvert une anomalie ici, c'est-à-dire que l'interprétation et l'exécution d'Android 14 utilisent en fait la méthode d'interprétation et d'exécution switch. J'ai re-testé les méthodes d'interprétation et d'exécution de plusieurs versions d'Android. Android 12 utilise mterp, Android 13 utilise nterp et ne basculera que lors du débogage. En théorie, Android 14 devrait également utiliser nterp. Comment se fait-il qu'il utilise le commutateur le plus lent. Voici les méthodes des versions 12, 13 et 14 pour exécuter le backtrace.
Vérifier les doutes
J'ai commencé à soupçonner que l'exécution de l'interpréteur était à l'origine du décalage. J'ai parcouru le code source
art/runtime/interpreter/mterp/nterp.cc et j'ai découvert qu'il y avait effectivement des changements. S'il s'agissait de javaDebuggable, je ne le ferais pas. utilisez NTRP. Ensuite, essayez de prouver que ce problème est causé.
isJavaDebuggable est contrôlé par RuntimeDebugState runtime_debug_state_ dans runtime.cc. Nous pouvons trouver l'instance d'exécution et modifier l'attribut runtime_debug_state_ via le décalage. Après avoir examiné le code source, nous pouvons également
le définir via _ZN3art7Runtime20SetRuntimeDebugStateENS0_17RuntimeDebugStateE.
void Runtime::SetRuntimeDebugState(RuntimeDebugState state) {
if (state != RuntimeDebugState::kJavaDebuggableAtInit) {
// We never change the state if we started as a debuggable runtime.
DCHECK(runtime_debug_state_ != RuntimeDebugState::kJavaDebuggableAtInit);
}
runtime_debug_state_ = state;
}
J'ai essayé de le vérifier via la méthode ci-dessus. J'ai défini le isJavaDebuggable du package de test sur false et il est toujours bloqué. J'ai défini le isJavaDebuggable du package de production sur true et il est devenu un peu bloqué. J’ai donc renversé ma conjecture selon laquelle la méthode d’exécution était à l’origine du décalage.
Le dépannage natif prend beaucoup de temps
Je soupçonne que l'exécution de la méthode nativie prend du temps. Essayez à nouveau d'utiliser simpleperf pour localiser le problème.
Conclusion : Fondamentalement, expliquer la pile dans le code d'exécution prend beaucoup de temps, et il n'y a pas d'autre pile spéciale.
Ciblage
DEBUG_JAVA_DEBUGGABLE
Pensez ensuite à partir de la source du débogable et à réduire progressivement la portée pour localiser les variables d'influence.
Le débogable dans AndroidManifest affecte le processus système pour démarrer un runtimeFlags dans notre processus.
Le sixième paramètre de la méthode de démarrage dans frameworks/base/core/java/android/os/Process.java est runtimeFlags. S'il s'agit de debuggableFlag, runtimeFlags sera ajouté avec les indicateurs suivants. Ensuite, réduisez d'abord la plage d'étiquettes.
if (debuggableFlag) {
runtimeFlags |= Zygote.DEBUG_ENABLE_JDWP;
runtimeFlags |= Zygote.DEBUG_ENABLE_PTRACE;
runtimeFlags |= Zygote.DEBUG_JAVA_DEBUGGABLE;
// Also turn on CheckJNI for debuggable apps. It's quite
// awkward to turn on otherwise.
runtimeFlags |= Zygote.DEBUG_ENABLE_CHECKJNI;
// Check if the developer does not want ART verification
if (android.provider.Settings.Global.getInt(mService.mContext.getContentResolver(),
android.provider.Settings.Global.ART_VERIFIER_VERIFY_DEBUGGABLE, 1) == 0) {
runtimeFlags |= Zygote.DISABLE_VERIFIER;
Slog.w(TAG_PROCESSES, app + ": ART verification disabled");
}
}
Nous devons modifier les paramètres de démarrage de notre processus. Ensuite, vous devez accrocher le processus système. Cela implique de rooter le téléphone, d'installer certaines opérations du framework hook, puis d'apporter certaines modifications aux paramètres jusqu'au démarrage du processus de hook.
hookAllMethods(
Process.class,
"start",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
final String niceName = (String) param.args[1];
final int uid = (int) param.args[2];
final int runtimeFlags = (int) param.args[5];
XposedBridge.log("process_xx " + runtimeFlags);
if (isDebuggable(niceName, user)) {
param.args[5] = runtimeFlags&~DEBUG_JAVA_DEBUGGABLE;
XposedBridge.log("process_xx " + param.args[5]);
}
}
}
);
Cette fois, les résultats furent évidents. Les runtimeflags du package de test ne sont plus bloqués après la suppression de DEBUG_JAVA_DEBUGGABLE. Le package de production, y compris les applications sur le marché des applications, est resté bloqué après l'ajout de la marque DEBUG_JAVA_DEBUGGABLE. On peut alors prouver que cela est dû à la variable DEBUG_JAVA_DEBUGGABLE.
Ciblage
DésoptimiserBootImage
Continuez vers le code source pour observer l’impact de DEBUG_JAVA_DEBUGGABLE.
if ((runtime_flags & DEBUG_JAVA_DEBUGGABLE) != 0) {
runtime->AddCompilerOption("--debuggable");
runtime_flags |= DEBUG_GENERATE_MINI_DEBUG_INFO;
runtime->SetRuntimeDebugState(Runtime::RuntimeDebugState::kJavaDebuggableAtInit);
{
// Deoptimize the boot image as it may be non-debuggable.
ScopedSuspendAll ssa(__FUNCTION__);
runtime->DeoptimizeBootImage();
}
runtime_flags &= ~DEBUG_JAVA_DEBUGGABLE;
needs_non_debuggable_classes = true;
}
La logique ici est l'impact de DEBUG_JAVA_DEBUGGABLE, et SetRuntimeDebugState a déjà été testé. Ce n'est pas
l'impact de DEBUG_GENERATE_MINI_DEBUG_INFO C'est runtime->DeoptimizeBootImage() ? J'ai donc utilisé le package avec debugable comme false pour appeler activement la méthode DeoptimizeBootImage via _ZN3art7Runtime19DeoptimizeBootImageEv, puis il s'est reproduit !
Analyse des causes
DeoptimizeBootImage convertit la méthode de code AOT dans bootImage en java déboguable. Réinitialisez le point d’entrée de la méthode et passez à l’exécution interprétée sans utiliser de code AOT. En remontant à
la méthode Instrumentation::InitializeMethodsCode, nous atteignons toujours le point de CanUseNterp(method) CanRuntimeUseNterp. De plus, Android 13 peut utiliser nterp et Android 14 ne peut utiliser que Switch.
J'ai de nouveau accroché le code et demandé à CanRuntimeUseNterp de renvoyer directement true, mais il est toujours bloqué. J'ai trouvé ça même si je l'accrochais. Les méthodes suivantes servent toujours à changer d’interprétation et d’exécution. En pensant à l'inverse, c'est parce que mon hook est à la traîne et que DeoptimizeBootImage a été exécuté. Lorsque la méthode de base est appelée, le commutateur est exécuté.
J'ai utilisé le package true déboguable Android 13 pour les tests, j'ai d'abord accroché CanRuntimeUseNterp return false, puis j'ai exécuté DeoptimizeBootImage, et le décalage est réapparu.
Positionnement préliminaire : la méthode dans l'image de démarrage est nterp dans Android 13 et la méthode switch dans Android 14. La méthode dans l'image de démarrage est très basique et fragmentée, donc l'exécution de la méthode switch prend beaucoup de temps.
La vérification est un problème de système
S'il s'agit d'un problème système, alors tout le monde devrait le rencontrer, non seulement notre application a ce problème, j'ai donc trouvé quelques amis pour m'aider à vérifier le problème avec le package de débogage. Effectivement, ils ont tous ce problème. L’expérience d’installation du même package sur Android 14 et Android 13 est complètement incohérente.
Question de commentaires
Quelqu'un a signalé sur issuetracker que le package de débogage Android 14 est lent
https://issuetracker.google.com/issues/311251587. Mais il n’y a pas encore eu de résultat, alors j’ai compensé le problème que j’avais identifié.
Au fait, j'ai également soulevé un problème
https://issuetracker.google.com/issues/328477628
3. Solution temporaire
En attendant la réponse de Google, je réfléchis également à la façon dont la couche App peut éviter ce problème et rendre l'expérience du package de débogage fluide, par exemple comment ré-optimiser la méthode dans l'image de démarrage. Avec cette idée en tête, j'ai étudié à nouveau le code artistique et j'ai découvert qu'Android 14 ajoutait une nouvelle
méthode UpdateEntrypointsForDebuggable. Cette méthode réinitialisera la méthode d'exécution de la méthode selon les règles, telles que aot et nterp, puis j'ai accroché CanRuntimeUseNterp avant de revenir. . True Si vous appelez à nouveau UpdateEntrypointsForDebuggable, n'allez-vous pas à nouveau sur Nterp ?
void Instrumentation::UpdateEntrypointsForDebuggable() {
Runtime* runtime = Runtime::Current();
// If we are transitioning from non-debuggable to debuggable, we patch
// entry points of methods to remove any aot / JITed entry points.
InstallStubsClassVisitor visitor(this);
runtime->GetClassLinker()->VisitClasses(&visitor);
}
Je l'ai essayé selon l'idée ci-dessus, et c'est devenu beaucoup plus fluide ! ! !
En fait, la solution ci-dessus pose encore quelques problèmes. Par rapport au package avec debugable défini sur false, il y a encore un certain décalage. J'ai également découvert que les méthodes de bootImage sont allées vers nterp, mais la plupart du code de l'apk était toujours destiné à changer d'interprétation et d'exécution, j'ai donc changé d'avis.
Est-ce que je peux définir RuntimeDebugState sur non déboguable avant d'appeler UpdateEntrypointsForDebuggable, puis définir RuntimeDebugState sur déboguable après avoir appelé UpdateEntrypointsForDebuggable ? Le code final est le suivant. Le framework hook utilise https://github.com/bytedance/android-inline-hook.
Java_test_ArtMethodTrace_bootImageNterp(JNIEnv *env,
jclass clazz) {
void *handler = shadowhook_dlopen("libart.so");
instance_ = static_cast<void **>(shadowhook_dlsym(handler, "_ZN3art7Runtime9instance_E"));
jobject
(*getSystemThreadGroup)(void *runtime) =(jobject (*)(void *runtime)) shadowhook_dlsym(handler,
"_ZNK3art7Runtime20GetSystemThreadGroupEv");
void
(*UpdateEntrypointsForDebuggable)(void *instrumentation) = (void (*)(void *i)) shadowhook_dlsym(
handler,
"_ZN3art15instrumentation15Instrumentation30UpdateEntrypointsForDebuggableEv");
if (getSystemThreadGroup == nullptr || UpdateEntrypointsForDebuggable == nullptr) {
LOGE("getSystemThreadGroup failed ");
shadowhook_dlclose(handler);
return;
}
jobject thread_group = getSystemThreadGroup(*instance_);
int vm_offset = findOffset(*instance_, 0, 4000, thread_group);
if (vm_offset < 0) {
LOGE("vm_offset not found ");
shadowhook_dlclose(handler);
return;
}
void (*setRuntimeDebugState)(void *instance_, int r) =(void (*)(void *runtime,
int r)) shadowhook_dlsym(
handler, "_ZN3art7Runtime20SetRuntimeDebugStateENS0_17RuntimeDebugStateE");
if (setRuntimeDebugState != nullptr) {
setRuntimeDebugState(*instance_, 0);
}
void *instrumentation = reinterpret_cast<void *>(reinterpret_cast<char *>(*instance_) +
vm_offset - 368 );
UpdateEntrypointsForDebuggable(instrumentation);
setRuntimeDebugState(*instance_, 2);
shadowhook_dlclose(handler);
LOGE("bootImageNterp success");
}
4. Enfin
Récemment, j'ai également vu un article d'un ingénieur Qualcomm sur la communauté. Il a fait une analyse plus détaillée basée sur le problème que j'ai identifié et a confirmé que Google résoudrait ce problème sur Android 15. S'il s'agit d'une version étrangère des appareils Android 14, Google prévoit de résoudre ce problème via une mise à jour du module com.android.artapex. Cependant, en raison de problèmes de réseau en Chine, les efforts de Google ne peuvent pas fonctionner. Chaque fabricant de téléphones mobiles doit donc activement intégrer ces deux changements. [1]
Si vous devez résoudre temporairement le problème des packages déboguables bloqués, vous pouvez également le résoudre via la méthode ci-dessus.
Article de référence :
[1] https://juejin.cn/post/7353106089296789556
*Texte/ Wuyou
Cet article est original de Dewu Technology. Pour des articles plus intéressants, veuillez consulter : Site officiel de Dewu Technology.
La réimpression sans l'autorisation de Dewu Technology est strictement interdite, sinon la responsabilité légale sera engagée conformément à la loi !
Linus a pris les choses en main pour empêcher les développeurs du noyau de remplacer les tabulations par des espaces. Son père est l'un des rares dirigeants capables d'écrire du code, son deuxième fils est directeur du département de technologie open source et son plus jeune fils est un noyau. contributeur à l'open source. Huawei : Il a fallu 1 an pour convertir 5 000 applications mobiles couramment utilisées Migration complète vers Hongmeng Java est le langage le plus sujet aux vulnérabilités tierces Wang Chenglu, le père de Hongmeng : l'open source Hongmeng est la seule innovation architecturale. dans le domaine des logiciels de base en Chine, Ma Huateng et Zhou Hongyi se serrent la main pour « éliminer les rancunes ». Ancien développeur de Microsoft : les performances de Windows 11 sont « ridiculement mauvaises » " Bien que ce que Laoxiangji est open source, ce ne soit pas le code, les raisons qui le sous-tendent. sont très réconfortants. Meta Llama 3 est officiellement publié. Google annonce une restructuration à grande échelle.