1. Hintergrund
Warum steckt meine App so fest? Wer hat den Code vergiftet?
Eines Tages stellte ich plötzlich fest, dass das Debug-Paket extrem verzögert lief. Nach dem folgenden einfachen Test stellte ich fest, dass es ein Problem mit dem Debug-Paket auf Android 14 gab.
2. Aufzeichnungen zur Fehlerbehebung
Routinemäßige Untersuchungsmethoden
Verwendet systrace und das interne Debug-Paket-Trace-Tool dutrace zur Fehlerbehebung.
Fazit: Die CPU ist im Leerlauf und der Hauptthread ist offensichtlich nicht blockiert. Es scheint, dass die reine Methodenausführung zeitaufwändig ist.
Zweifel gefunden
Im ersten Schritt der Fehlerbehebung gab es keinen großen Gewinn, aber als ich das Dutrace-Tool zur Fehlerbehebung verwendete, stellte ich eine Anomalie fest. Hier ist eine kurze Einführung in das Implementierungsprinzip von Dutrace:
Dutrace verwendet einen Inline-Hook, um vor und nach der Ausführung von artmethod Atrace-Punkte hinzuzufügen und zeigt sie dann über das Perfetto-UI-Tool an. Es hat folgende Vorteile:
1. Unterstützung der Offline-Analyse des Funktionsausführungsprozesses und der zeitaufwändigen Funktion.
2. Unter dem Analysefunktionsaufrufprozess:
a. Sie können die Funktionsaufrufe des gesamten Prozesses anzeigen (einschließlich Framework-Funktionen);
b. Möglichkeit, überwachte Funktionen und Threads anzugeben, um nutzlose Spuren effektiv zu filtern;
c. Für die dynamische Konfiguration ist kein Umpacken erforderlich.
3. Sie können vorgefertigte UI-Analysetools verwenden, einschließlich Funktionsaufrufen wichtiger System-Threads wie Rendering-Zeit, Thread-Sperren, GC-Zeit usw. sowie E/A-Vorgängen, CPU-Auslastung und anderen Ereignissen.
Flussdiagramm
Beim Hooking vor und nach der Ausführung der Kunstmethode geht es darum, drei Interpretations- und Ausführungssituationen der Kunstmethode zu verarbeiten.
ART-Laufzeitinterpreter
- Der C++-Interpreter, der der traditionelle Switch-Struktur-basierte Interpreter ist, verwendet diesen Zweig im Allgemeinen nur, wenn die Debugumgebung, die Methodenverfolgung oder Anweisungen nicht unterstützt werden oder wenn eine Ausnahme im Bytecode auftritt (z. B. eine fehlgeschlagene Überprüfung der strukturierten Sperre).
- Der mterp-Schnellinterpreter führt im Kern eine Handlertabelle für die Befehlszuordnung ein und implementiert einen schnellen Wechsel zwischen Befehlen durch handschriftliche Assemblierung, wodurch die Leistung des Interpreters verbessert wird.
- Nterp ist eine weitere Optimierung von Mterp. Nterp macht die Wartung verwalteter Code-Stacks überflüssig, verwendet dieselbe Stack-Frame-Struktur wie die Native-Methode und der gesamte Decodierungs- und Übersetzungsausführungsprozess wird durch Assembler-Code implementiert, wodurch die Leistungslücke zwischen dem Interpreter und dem kompilierten Code weiter verringert wird.
Ich habe hier eine Anomalie entdeckt, nämlich dass die Interpretation und Ausführung von Android 14 tatsächlich die Switch-Interpretations- und Ausführungsmethode verwendet. Ich habe die Interpretations- und Ausführungsmethoden mehrerer Android-Versionen erneut getestet. Android 12 verwendet mterp, Android 13 verwendet nterp und wechselt beim Debuggen nur zum Switch. Theoretisch sollte Android 14 auch nterp verwenden. Im Folgenden sind die Methoden der Versionen 12, 13 und 14 aufgeführt, um Backtrace auszuführen.
Suchen Sie nach Zweifeln
Ich begann zu vermuten, dass die Ausführung des Interpreters die Verzögerung verursachte. Ich habe den Quellcode
art/runtime/interpreter/mterp/nterp.cc durchgesehen und festgestellt, dass er tatsächlich Änderungen enthielt, wenn er javaDebuggable wäre Verwenden Sie nterp. Versuchen Sie als Nächstes zu beweisen, dass dieses Problem verursacht wird.
isJavaDebuggable wird durch RuntimeDebugState runtime_debug_state_ in runtime.cc gesteuert. Wir können die Laufzeitinstanz finden und das runtime_debug_state_-Attribut über den Offset ändern. Nachdem wir uns den Quellcode angesehen haben, können wir
ihn auch über _ZN3art7Runtime20SetRuntimeDebugStateENS0_17RuntimeDebugStateE festlegen.
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;
}
Ich habe versucht, dies mit der oben genannten Methode zu überprüfen und habe „isJavaDebuggable“ des Testpakets auf „false“ gesetzt, aber es blieb hängen. Als ich „isJavaDebuggable“ des Produktionspakets auf „true“ setzte, blieb es ein wenig hängen. Daher habe ich meine Vermutung widerlegt, dass die Ausführungsmethode die Verzögerung verursacht hat.
Die Fehlerbehebung bei nativen Systemen ist zeitaufwändig
Ich vermute, dass die Ausführung der Nativie-Methode zeitaufwändig ist. Versuchen Sie erneut, das Problem mit simpleperf zu lokalisieren.
Fazit: Grundsätzlich ist es zeitaufwändig, den Stapel im Ausführungscode zu erklären, und es gibt keinen anderen speziellen Stapel.
Targeting
DEBUG_JAVA_DEBUGGABLE
Denken Sie dann darüber nach, von der Quelle der debuggbaren Variablen auszugehen und den Umfang schrittweise einzugrenzen, um die Einflussvariablen zu lokalisieren.
Das debuggable in AndroidManifest wirkt sich auf den Systemprozess aus, um ein runtimeFlags in unserem Prozess zu starten.
Der sechste Parameter der Startmethode in Frameworks/Base/Core/Java/Android/OS/Process.java ist runtimeFlags. Wenn es debuggableFlag ist, werden runtimeFlags mit den folgenden Flags hinzugefügt.
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");
}
}
Wir müssen die Startparameter unseres Prozesses ändern. Dann müssen Sie den Systemprozess einbinden. Dazu gehört das Rooten des Telefons, das Installieren einiger Vorgänge des Hook-Frameworks und das anschließende Vornehmen einiger Parameteränderungen bis zum Start des Hook-Prozesses.
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]);
}
}
}
);
Diesmal gab es einige offensichtliche Ergebnisse. Die Laufzeitflags des Testpakets bleiben nach dem Entfernen von DEBUG_JAVA_DEBUGGABLE nicht mehr hängen. Das Produktionspaket, einschließlich der Anwendungen auf dem Anwendungsmarkt, blieb alle hängen, nachdem es mit DEBUG_JAVA_DEBUGGABLE markiert wurde. Dann kann bewiesen werden, dass es durch die Variable DEBUG_JAVA_DEBUGGABLE verursacht wird.
Targeting
DeoptimizeBootImage
Fahren Sie mit dem Quellcode fort, um die Auswirkungen von DEBUG_JAVA_DEBUGGABLE zu beobachten.
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;
}
Die Logik hier ist die Auswirkung von DEBUG_JAVA_DEBUGGABLE, und SetRuntimeDebugState wurde bereits getestet. Es ist nicht
die Auswirkung von DEBUG_GENERATE_MINI_DEBUG_INFO, sondern runtime->DeoptimizeBootImage()? Also habe ich das Paket mit debugable als false verwendet, um die DeoptimizeBootImage-Methode über _ZN3art7Runtime19DeoptimizeBootImageEv aktiv aufzurufen, und dann wurde es reproduziert!
Ursachenanalyse
DeoptimizeBootImage konvertiert die AOT-Codemethode in bootImage in Java, das debuggbar ist. Initialisieren Sie den Methodeneinstiegspunkt neu und gehen Sie zur interpretierten Ausführung, ohne AOT-Code zu verwenden. Zurück zur
Methode Instrumentation::InitializeMethodsCode gelangen wir immer noch zum Punkt CanUseNterp(method) CanRuntimeUseNterp. Außerdem kann Android 13 nterp verwenden und Android 14 kann nur Switch verwenden.
Ich habe den Code erneut eingebunden und CanRuntimeUseNterp gebeten, direkt „true“ zurückzugeben, aber er blieb immer noch hängen. Ich habe das gefunden, auch wenn ich es angeschlossen habe. Die folgenden Methoden wechseln weiterhin die Interpretation und Ausführung. Umgekehrt betrachtet liegt es daran, dass mein Hook zurückgeblieben ist und DeoptimizeBootImage ausgeführt wurde. Wenn die Basismethode aufgerufen wird, wird der Schalter ausgeführt.
Ich habe zum Testen das debugbare True-Paket für Android 13 verwendet, zuerst CanRuntimeUseNterp return false angehängt und dann DeoptimizeBootImage ausgeführt, und die Verzögerung trat erneut auf.
Vorläufige Positionierung: Die Methode im Bootimage ist nterp in Android 13 und die Switch-Methode in Android 14. Die Methode im Bootimage ist sehr einfach und fragmentiert, sodass die Ausführung der Switch-Methode sehr zeitaufwändig ist.
Die Verifizierung ist ein Systemproblem
Wenn es sich um ein Systemproblem handelt, sollte es jedem begegnen. Nicht nur unsere App hat dieses Problem, daher habe ich ein paar Freunde gefunden, die mir helfen, das Problem mit dem Debug-Paket zu überprüfen. Tatsächlich haben sie alle dieses Problem, wenn sie dasselbe Paket auf Android 14 und Android 13 installieren.
Feedback-Frage
Jemand hat auf Issuetracker berichtet, dass das Debug-Paket für Android 14 langsam ist
https://issuetracker.google.com/issues/311251587. Da es aber noch kein Ergebnis gab, habe ich das festgestellte Problem behoben.
Übrigens habe ich auch ein Problem angesprochen:
https://issuetracker.google.com/issues/328477628
3. Übergangslösung
Während ich auf die Antwort von Google warte, denke ich auch darüber nach, wie die App-Ebene dieses Problem vermeiden und die Erfahrung des Debug-Pakets wieder reibungsloser gestalten kann, beispielsweise wie die Methode im Bootimage erneut optimiert werden kann. Mit dieser Idee im Hinterkopf habe ich den Art-Code noch einmal studiert und festgestellt, dass Android 14 eine neue
UpdateEntrypointsForDebuggable-Methode hinzugefügt hat. Diese Methode setzt die Ausführungsmethode der Methode gemäß den Regeln zurück, z. B. aot und nterp. Dann habe ich CanRuntimeUseNterp eingebunden, bevor ich zurückgehe . True Wenn Sie UpdateEntrypointsForDebuggable erneut aufrufen, gehen Sie dann nicht erneut zu 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);
}
Ich habe es nach der obigen Idee ausprobiert und es wurde viel glatter! ! !
Tatsächlich gibt es bei der oben genannten Lösung noch einige verbleibende Probleme. Im Vergleich zu dem Paket, bei dem debugable auf „false“ gesetzt ist, gibt es immer noch eine gewisse Verzögerung. Ich habe auch festgestellt, dass die Methoden in bootImage auf nterp umgestellt wurden, der Großteil des Codes in der APK jedoch weiterhin auf Interpretation und Ausführung umschaltete, also habe ich es mir anders überlegt.
Ist es in Ordnung, wenn ich RuntimeDebugState vor dem Aufruf von UpdateEntrypointsForDebuggable auf „non-debugable“ setze und dann RuntimeDebugState nach dem Aufruf von „UpdateEntrypointsForDebuggable“ auf debugable setze? Der endgültige Code lautet wie folgt. Das Hook-Framework verwendet 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. Endlich
Kürzlich habe ich auch einen Artikel eines Qualcomm-Ingenieurs in der Community gesehen. Er hat eine detailliertere Analyse basierend auf dem von mir identifizierten Problem erstellt und bestätigt, dass Google dieses Problem auf Android 15 beheben wird. Wenn es sich um eine ausländische Version von Android 14-Geräten handelt, wird Google plant, dieses Problem durch ein Update des com.android.artapex-Moduls zu beheben. Aufgrund von Netzwerkproblemen in China kann der Vorstoß von Google jedoch nicht funktionieren, sodass jeder Mobiltelefonhersteller diese beiden Änderungen aktiv berücksichtigen muss. [1]
Wenn Sie das Problem festsitzender debugfähiger Pakete vorübergehend lösen müssen, können Sie es auch mit der oben genannten Methode lösen.
Referenzartikel:
[1] https://juejin.cn/post/7353106089296789556
*Text/ Wuyou
Dieser Artikel stammt ursprünglich von Dewu Technology. Weitere spannende Artikel finden Sie auf der offiziellen Website von Dewu Technology
Ein Nachdruck ohne die Genehmigung von Dewu Technology ist strengstens untersagt, andernfalls wird eine rechtliche Haftung gemäß dem Gesetz verfolgt!
Linus nahm die Sache selbst in die Hand, um zu verhindern, dass Kernel-Entwickler Tabulatoren durch Leerzeichen ersetzen. Sein Vater ist einer der wenigen Führungskräfte, die Code schreiben können, sein zweiter Sohn ist Direktor der Open-Source-Technologieabteilung und sein jüngster Sohn ist ein Kern Mitwirkender bei Open Source: Es dauerte ein Jahr, 5.000 häufig verwendete mobile Anwendungen zu konvertieren. Java ist die Sprache, die am anfälligsten für Schwachstellen von Drittanbietern ist. Wang Chenglu, der Vater von Hongmeng: Open Source Hongmeng ist die einzige architektonische Innovation im Bereich der Basissoftware in China. Ma Huateng und Zhou Hongyi geben sich die Hand, um „den Groll zu beseitigen.“ Ehemaliger Microsoft-Entwickler: Die Leistung von Windows 11 ist „lächerlich schlecht“. sind sehr herzerwärmend . Meta Llama 3 wird offiziell veröffentlicht