1.背景
なぜ私のアプリはこんなにスタックしてしまうのでしょうか? 誰がコードを汚染したのでしょうか?
ある日、突然、デバッグ パッケージの実行が非常に遅いことに気づきました。次の簡単なテストの後、Android 14 のデバッグ パッケージに問題があることがわかりました。
2.問題解決記録
日常的な調査手段
トラブルシューティングには systrace と内部デバッグ パッケージ トレース ツール dutrace を使用しました。
結論: CPU はアイドル状態であり、メインスレッドは明らかにブロックされていないため、純粋なメソッドの実行に時間がかかっているようです。
疑問が見つかった
トラブルシューティングの最初のステップでは大きな成果はありませんでしたが、dutrace ツールを使用してトラブルシューティングを行ったときに異常を発見しました。ここでは、dutrace の実装原理を簡単に紹介します。
Dutrace は、インライン フックを使用して、artmethod の実行の前後にアトレース ポイントを追加し、perfetto ui ツールを通じて表示します。次のような利点があります。
1. 関数の実行プロセスと時間のかかる関数のオフライン分析をサポートします。
2. 分析関数呼び出しプロセスでは、次のようになります。
a. プロセス全体の関数呼び出し (フレームワーク関数を含む) を表示できます。
b. 監視対象の関数とスレッドを指定して、無駄なトレースを効果的にフィルタリングする機能。
c. 動的構成では再パッケージ化は必要ありません。
3. レンダリング時間、スレッドロック、GC 時間などの主要なシステムスレッドの関数呼び出しや、I/O 操作、CPU 負荷、その他のイベントなどの既製の UI 分析ツールを使用できます。
フローチャート
アートメソッドの実行前後にフックする場合、アートメソッドの解釈と実行の3つの状況を処理することになります。
ART ランタイム インタプリタ
- スイッチ構造に基づく従来のインタープリタである C++ インタープリタは、通常、デバッグ環境、メソッド トレース、命令がサポートされていない場合、またはバイトコードで例外が発生した場合 (構造化ロック検証の失敗など) にのみこの分岐を実行します。 。
- mterp 高速インタープリターは、その中核として、命令マッピング用のハンドラー テーブルを導入し、手書きアセンブリによる命令間の高速切り替えを実装して、インタープリターのパフォーマンスを向上させます。
- Nterp は Mterp を最適化したものです。 Nterp は、マネージド コード スタックのメンテナンスの必要性を排除し、ネイティブ メソッドと同じスタック フレーム構造を使用し、デコードと変換の実行プロセス全体がアセンブリ コードによって実装されるため、インタプリタとコンパイルされたコード間のパフォーマンス ギャップがさらに狭まります。
ここで異常を発見しました。つまり、Android 14 の解釈と実行では、実際には switch 解釈と実行メソッドが使用されます。いくつかの Android バージョンの解釈と実行方法を再テストしました。 Android 12 は mterp を使用し、Android 13 は nterp を使用し、理論的には Android 14 も nterp を使用する必要があります。なぜ最も遅いスイッチを使用するのでしょうか。バージョン12、13、14でバックトレースを実行する方法は以下のとおりです。
疑問点がないか確認する
インタプリタの実行が遅延の原因ではないかと思い、ソース コード
art/runtime/interpreter/mterp/nterp.cc を調べたところ、javaDebuggable であれば変更が加えられていないことがわかりました。インタープを使用します。次に、この問題が原因であることを証明してみます。
isJavaDebuggable は、runtime.cc の RuntimeDebugState runtime_debug_state_ によって制御されます。ソース コードを確認した後、ランタイム インスタンスを見つけて、オフセットを通じて runtime_debug_state_ 属性を変更することもできます。また、
_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;
}
上記の方法で検証しようとしたのですが、テストパッケージの isJavaDebuggable を false に設定しても、やはりスタックしてしまいました。本番パッケージの isJavaDebuggable を true に設定しても、少しスタックしてしまいました。そこで、実行方法がラグの原因であるという私の推測を覆しました。
ネイティブのトラブルシューティングには時間がかかります
nativie メソッドの実行には時間がかかると思われます。simpleperf を再度使用して問題を特定してください。
結論: 基本的に、スタックを実行コード内で説明するのは時間がかかり、他に特別なスタックはありません。
ターゲティング
DEBUG_JAVA_DEBUGGABLE
次に、デバッグ可能なソースから始めて、影響を与える変数を特定するために範囲を徐々に狭めることを考えます。
AndroidManifest のデバッグ可能ファイルは、プロセス内で runtimeFlags を開始するシステム プロセスに影響を与えます。
Frameworks/base/core/java/android/os/Process.java の start メソッドの 6 番目のパラメータが runtimeFlags である場合、次のフラグで runtimeFlags が追加されます。次に、ラベルの範囲を絞り込みます。
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");
}
}
プロセスの起動パラメータを変更する必要があります。次に、システムプロセスをフックする必要があります。これには、電話機のルート化、フック フレームワークのいくつかの操作のインストール、およびフック プロセスの開始によるいくつかのパラメータの変更が含まれます。
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]);
}
}
}
);
今回は明らかな結果がいくつかありました。 DEBUG_JAVA_DEBUGGABLE を削除した後、テスト パッケージ runtimeflags がスタックしなくなりました。アプリケーション マーケットのアプリケーションを含む製品パッケージはすべて、DEBUG_JAVA_DEBUGGABLE マークを追加した後にスタックしてしまいました。その後、変数 DEBUG_JAVA_DEBUGGABLE が原因であることが証明できます。
ターゲティング
ブートイメージの最適化を解除する
ソース コードに進み、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;
}
ここのロジックは DEBUG_JAVA_DEBUGGABLE の影響であり、SetRuntimeDebugState は以前にテストされています。 DEBUG_GENERATE_MINI_DEBUG_INFO の影響ではありません
。runtime->DeoptimizeBootImage() です。そこで、debuggable を false にしたパッケージを使用して、_ZN3art7Runtime19DeoptimizeBootImageEv を通じて DeoptimizeBootImage メソッドをアクティブに呼び出したところ、再現されました。
原因分析
DeoptimizeBootImage は、bootImage の AOT コード メソッドを Java デバッグ可能に変換します。メソッドのエントリ ポイントを再初期化し、AOT コードを使用せずに解釈された実行に進みます。 Instrumentation::InitializeMethodsCode メソッドまで遡ると
、CanUseNterp(method) CanRuntimeUseNterp のポイントに到達します。また、Android 13 では nterp が使用でき、Android 14 では switch のみが使用できます。
コードを再度フックし、CanRuntimeUseNterp に直接 true を返すように依頼しましたが、それでもスタックします。引っ掛けてもそれが分かりました。次のメソッドは引き続き解釈と実行を切り替えます。逆に考えると、フックが遅れて DeoptimizeBootImage が実行されたため、基本メソッドが呼び出されたときにスイッチが実行されます。
テストには Android 13 のデバッグ可能な true パッケージを使用し、最初に CanRuntimeUseNterp return false をフックしてから DeoptimizeBootImage を実行すると、ラグが再発しました。
予備的な位置付け:ブートイメージ内のメソッドは、Android 13 では nterp であり、Android 14 では switch メソッドです。ブートイメージ内のメソッドは非常に基本的で断片化されているため、switch メソッドの実行には非常に時間がかかります。
検証はシステムの問題です
これがシステムの問題であれば、私たちのアプリだけがこの問題を抱えているわけではなく、誰もが遭遇するはずです。そのため、デバッグ パッケージの問題の検証を手伝ってくれる友人を何人か見つけました。案の定、Android 14 と Android 13 で同じパッケージをインストールした場合、すべてにこの問題が発生します。
フィードバックの質問
誰かが issuetracker で Android 14 デバッグ パッケージが遅いと報告しました
https://issuetracker.google.com/issues/311251587。しかし、まだ結果が出ていないので、特定した問題を補うことにしました。
ちなみに、私も問題を提起しました
https://issuetracker.google.com/issues/328477628
3.一時的な解決策
Google の返答を待ちながら、ブートイメージ内のメソッドを再最適化する方法など、アプリ層がこの問題を回避し、デバッグ パッケージのエクスペリエンスをスムーズに戻す方法についても考えています。この考えを念頭に置いて、アート コードを再度調べたところ、Android 14 では新しい
UpdateEntrypointsForDebuggable メソッドが追加されていることがわかりました。このメソッドは、aot や nterp などのメソッドの実行メソッドをリセットします。次に、戻る前に CanRuntimeUseNterp をフックしました。 True もう一度 UpdateEntrypointsForDebuggable を呼び出すと、再び 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);
}
上記の考え方に従って試してみたところ、かなりスムーズになりました! ! !
実際、上記の解決策にはまだいくつかの問題が残っています。 debuggable を false に設定したパッケージと比較すると、まだ多少の遅れがあります。また、bootImage 内のメソッドは nterp に移行しましたが、apk 内のコードの大部分は依然として解釈と実行の切り替えに移行していることもわかりました。そのため、考えを変更しました。 UpdateEntrypointsForDebuggableを呼び出す前に RuntimeDebugState をデバッグ不可に設定し、UpdateEntrypointsForDebuggable を呼び出した後に RuntimeDebugState をデバッグ可能に設定して
も問題ありませんか?最終的なコードは次のとおりです。フック フレームワークは 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.最後に
最近、コミュニティでクアルコムのエンジニアによる記事も目にしました。彼は、私が特定した問題に基づいてより詳細な分析を行い、Google が Android 15 でこの問題を修正することを確認しました。海外バージョンの Android 14 デバイスの場合は、Google が対応します。 com.android.artapex モジュールの更新を通じてこの問題を修正する予定です。しかし、中国のネットワーク問題によりGoogleの押しが効かないため、各携帯電話メーカーはこの2つの変更を積極的に取り入れる必要がある。 [1]
デバッグ可能パッケージのスタックの問題を一時的に解決する必要がある場合は、上記の方法で解決することもできます。
参考記事:
[1] https://juejin.cn/post/7353106089296789556
※文/武勇
この記事は Dewu Technology によるものです。さらに興味深い記事については、Dewu Technology 公式 Web サイトをご覧ください。
Dewu Technology の許可なく転載することは固く禁じられています。さもなければ、法律に従って法的責任が追及されます。
ライナスは、カーネル開発者がタブをスペースに置き換えることを阻止するために自ら問題を解決しました。 彼の父親はコードを書くことができる数少ないリーダーの 1 人であり、次男はオープンソース テクノロジー部門のディレクターであり、末息子は中核です。ファー ウェイ: 一般的に使用されている 5,000 のモバイル アプリケーションを変換するのに 1 年かかった Java はサードパーティの脆弱性が最も発生しやすい言語です。Hongmeng の父: オープンソースの Honmeng は唯一のアーキテクチャ上の革新です。中国の基本ソフトウェア分野で 馬化騰氏と周宏毅氏が握手「恨みを晴らす」 元マイクロソフト開発者:Windows 11のパフォーマンスは「ばかばかしいほど悪い」 老祥基がオープンソースであるのはコードではないが、その背後にある理由は Meta Llama 3 が正式にリリースされ、 大規模な組織再編が発表されました。