Android のメモリ リーク分析のアイデアと事例分析

アイデアの分析

メモリ リークとは、一部のオブジェクトが使用されなくなったが、より長いライフ サイクルを持つ一部のオブジェクトによって参照され、その結果、オブジェクトが占有しているメモリ リソースが GC によってリサイクルできなくなり、メモリ使用量が増加し続けるという Android プロセスの現象を指します。 ; メモリ リークは、アプリケーションのパフォーマンスが低下したりフリーズしたりする一般的な要因です。このような問題を解決するための中心となるアイデアは、次の 2 つのステップに要約できます。

  1. メモリ リークの操作パスをシミュレートし、アプリケーション ヒープ メモリの変化を観察し、問題のおおよその場所を特定します。
  2. 特定の場所で分析を実行し、GC ルートを指すリークされたオブジェクトの完全な参照チェーンを見つけて、ソースからのメモリ リークを制御します。
分析ツール:Android Stuido Profiler

Profiler には、メモリ カーブ グラフとヒープ ダンプという 2 つの一般的に使用されるメモリ分析ツールがあります。メモリ カーブはメモリ使用状況をリアルタイムで観察し、メモリの動的分析を支援します。

メモリ リークが発生すると、メモリ曲線の典型的な現象は階段状になり、一度上昇すると下がりにくくなります。たとえば、アクティビティ リークの後、ページのメモリ使用量はすべてのページで増加します。ページを開いたり閉じたりを繰り返したり、手動GCでゴミ箱アイコンをクリックしたりしても使用量が減らず、アクティビティを開く前のレベルまでメモリリークが発生する可能性が高くなります。

この時点で、静的分析のためにアプリケーション ヒープ メモリ内のメモリ分布を手動でダンプできます。

UI のさまざまなインジケーターの説明:

  1. Allocations: ヒープ メモリ内のこのクラスのインスタンスの数。
  2. Native Size: このクラスのすべてのインスタンスによって参照されるネイティブ オブジェクトによって占有されるメモリ
  3. Shallow Size: このクラスのすべてのインスタンスの実際のメモリ使用量 (参照するオブジェクトのメモリ使用量を除く)。
  4. Retained Size:Shallow Sizeとは異なり、この数値は、クラスのすべてのインスタンスとそのクラスによって参照されるすべてのオブジェクトのメモリ フットプリントを表します。

画像を利用すると、次の属性をより直感的に理解できます。

上の図に示すように、赤い点はメモリサイズを表しを表しますShallow Size点はNative Size、青い メモリ リークは多くの場合「連鎖効果」を形成するため、リークしたオブジェクトから始まるすべてのオブジェクトとそのオブジェクトが参照するネイティブ リソースをリサイクルできなくなり、メモリ使用効率が低下します。Retained SizeRetained Size

さらに、Leaksメモリ リークの可能性があるインスタンスの数を表します。リスト内のクラスをクリックすると、そのクラスのインスタンスの詳細が表示されます。インスタンス リストでは、インスタンスが到達する最短のコール チェーンの深さを表します。depthこれGC RootReference、図 1 の右側の列にあるスタック 完全な呼び出しチェーンを使用すると、最も疑わしい参照を見つけるまで遡って追跡し、コードに基づいてリークの原因を分析し、問題を解決するための適切な薬を処方することができます。

次に、プロジェクトで典型的なメモリ リークが発生したいくつかのケースを分析します。

事例分析

ケース 1: BitmapBinder のメモリ リーク

クロスプロセスのビットマップ送信シナリオに関しては、次の方法を採用しますBitmapBinder。インテントはカスタム Binder で渡すことをサポートしているため、Binder を使用してビットマップ オブジェクトのインテント送信を実装できます。

// IBitmapBinder AIDL文件 
import android.graphics.Bitmap; 
interface IBitmapInterface { 
    Bitmap getIntentBitmap(); 
}

ただし、ビットマップをusing に渡したActivity1、2 つの重大なメモリ リークが発生しました。BitmapBinderActivity2

  1. ジャンプ後に戻りますが、Activity1フィニッシュ中にリサイクルすることはできません。
  2. 繰り返しジャンプすると、BitmapオブジェクトBinderが繰り返し作成され、リサイクルできなくなります。

まずヒープ ダンプを分析します。

これは「マルチインスタンス」メモリ リークです。つまり、finish がActivity1開かれるたびに、Activity オブジェクトが追加されてヒープに残され、破棄できません。これは、内部クラス参照や静的配列参照などのシナリオで一般的です。 (リスナー リストなど); プロファイラーによると、参照チェーンが与えられた場合、BitmapExt次のクラスが見つかります。

suspend fun Activity.startActivity2WithBitmap() {
    val screenShotBitmap = withContext(Dispatchers.IO) { 
        SDKDeviceHelper.screenShot() 
    } ?: return
    startActivity(Intent().apply {
        val bundle = Bundle()
        bundle.putBinder(KEY_SCREENSHOT_BINDER, object : IBitmapInterface.Stub() {
            override fun getIntentBitmap(): Bitmap {
                return screenShotBitmap
            }
        }) 
        putExtra (INTENT_QUESTION_SCREENSHOT_BITMAP, bundle)
    })
}

BitmapExtActivity のグローバル拡張メソッドがありstartActivity2WithBitmap、Binderを作成し、取得したスクリーンショットのBitmapをそこに投げ込み、IntentにラップしてActivity2に送信します; 明らかにここに匿名の内部クラスがあり、リークが発生しているようIBitmapInterfaceですここから。

ただ、疑問が2つあり、1つはこの内部クラスがメソッド内に書かれているのですが、メソッドが終了するとメソッドスタック内の内部クラスの参照はクリアされないのでしょうか?次に、この内部クラスは Activity を参照しません。

これら 2 つの点を理解するには、Kotlin コードを Java に逆コンパイルして確認する必要があります。

@Nullable
public static final Object startActivity2WithBitmap(@NotNull Activity $this$startActivity2WithBitmap, boolean var1, @NotNull Continuation var2) {
    ...
    Bitmap var14 = (Bitmap)var10000;
    if (var14 == null) {
        return Unit.INSTANCE;
    } else {
        Bitmap screenShotBitmap = var14;
        Intent var4 = new Intent();
        int var6 = false;
        Bundle bundle = new Bundle();
        // 内部类创建位置:
        bundle.putBinder("screenShotBinder", (IBinder)(new BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1($this$startActivity2WithBitmap, screenShotBitmap)));
        var4.putExtra("question_screenshot_bitmap", bundle);
        Unit var9 = Unit.INSTANCE;
        $this$startActivity2WithBitmap.startActivity(var4);
        return Unit.INSTANCE;
    }
}

// 这是kotlin compiler自动生成的一个普通类:
public final class BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1 extends IBitmapInterface.Stub {
    // $FF: synthetic field
    final Activity $this_startActivity2WithBitmap$inlined; // 引用了activity
    // $FF: synthetic field
    final Bitmap $screenShotBitmap$inlined;

    BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1(Activity var1, Bitmap var2) {
        this.$this_startActivity2WithBitmap$inlined = var1;
        this.$screenShotBitmap$inlined = var2;
    }
    @NotNull
    public Bitmap getIntentBitmap() {
        return this.$screenShotBitmap$inlined;
    }
}

Kotlin Compiler でコンパイルされた Java ファイルでは、IBitmapInterface匿名内部クラスが通常のクラスに置き換えられBitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1、この通常のクラスがアクティビティを保持します。この状況が発生する理由は、通常、クラス内でメソッドの変数を使用するために、Kotlin はメソッドの入力パラメーターと、内部クラス コードの上で作成されたすべての変数をクラスのメンバー変数に書き込むためです。 Activityはこのクラスの参照であり、またBinder自体のライフサイクルがActivityよりも長いため、メモリリークが発生します。

解決策は、通常のクラスを直接宣言して Kotlin コンパイラーの「最適化」をバイパスし、Activity への参照を削除することです。

class BitmapBinder(private val bitmap: Bitmap): IBitmapInterface.Stub() {
    override fun getIntentBitmap( ) = bitmap
}

// 使用:
bundle.putBinder(KEY_SCREENSHOT_BINDER, BitmapBinder(screenShotBitmap))

次に問題となるのは、BitmapとBinderが繰り返し作成されて再利用できないことですが、メモリ現象は図のとおりで、ジャンプして閉じるたびにメモリが梯子状に少しずつ増えていき、解放できなくなります。 GC後。

ヒープ内の2560x1600, 320densityビットマップ サイズから、これらはリサイクルできず、Binder によって保持されているスクリーンショット ビットマップ オブジェクトであると推測できます。しかし、Binder の参照チェーンを見ると、アプリケーションに関連する参照は見つかりませんでした。

Binder は、Binder の実装に関連するライフサイクルの長いネイティブ層によって参照されるべきであると推測していますが、Binder をリサイクルする効果的な方法は見つかっていません。

解決策の 1 つは、Binder を再利用して、Activity2 を開くたびに Binder が再作成されないようにすることです。さらに、Binder をリサイクルできない場合でも、Binder を再利用できるように、BitmapBinderビットマップを弱参照に変更します。結局のところ、ビットマップはメモリの大きな家です。

object BitmapBinderHolder {
    private var mBinder: BitmapBinder? = null // 保证全局只有一个BitmapBinder

    fun of(bitmap: Bitmap): BitmapBinder {
        return mBinder ?: BitmapBinder(WeakReference(bitmap)).apply { mBinder = this }
    }
}

class BitmapBinder(var bitmapRef: WeakReference<Bitmap>?): IBitmapInterface.Stub() {
    override fun getIntentBitmap() = bitmapRef?.get()
}

// 使用:
bundle.putBinder(KEY_SCREENSHOT_BINDER, BitmapBinderHolder.of(screenShotBitmap))

検証: メモリ マップと同様に、GC 後、作成されたすべてのビットマップは通常どおりリサイクルできます。

ケース 2: Flutter マルチエンジン シーン プラグインのメモリ リーク

多くのプロジェクトでは、マルチエンジン ソリューションを使用して Flutter ハイブリッド開発を実装しています。Flutter ページを閉じるとき、メモリ リークを避けるために、 、 、およびその他の関連コンポーネントを適時にバインド解除して破棄する必要があるだけFlutterViewFlutterEngineなくMessageChannel、各Flutterプラグインが正常にリリースされているかに注意して動作させてください。

たとえば、マルチエンジン プロジェクトの 1 つでは、ページを開いたり閉じたりを繰り返すことによってメモリ リークが発見されました。

このアクティビティは、マルチエンジン ソリューションを使用し、その上で実行されるセカンダリ ページです。FlutterView「単一インスタンス」のメモリ リークであるようです。つまり、何度オン/オフを切り替えても、アクティビティは保持されるだけです。 1 つのインスタンスであり、ヒープ内で解放できないこれは一般的であり、シナリオはグローバル静的変数への参照です。この種のメモリ リークは、マルチインスタンス リークよりもメモリへの影響が若干軽いですが、アクティビティが大きく、多くのフラグメントとビューを保持しており、これらの関連コンポーネントが一緒にリークしている場合は、最適化にも重点を置く必要があります。

参照チェーンから判断すると、これはFlutterEngine内部の通信チャネルによって引き起こされるメモリ リークです。 がFlutterEngine作成されると、エンジン内の各プラグインは独自のプラグインを作成してMessageChannel登録し、FlutterEngine.dartExecutor.binaryMessenger各プラグインが独立してネイティブと通信できるようにします。

たとえば、一般的なプラグインは次のように記述できます。

class XXPlugin: FlutterPlugin {
    private val mChannel: BasicMessageChannel<Any>? = null

    override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { // 引擎创建时回调
        mChannel = BasicMessageChannel(flutterPluginBinding.flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NAME, JSONMessageCodec.INSTANCE)
        mChannel?.setMessageHandler { message, reply ->
            ...
        }
    }

    override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { // 引擎销毁时回调
        mChannel?.setMessageHandler(null)
        mChannel = null
    }
}

実際にはへの参照をFlutterPlugin保持し、 ... への参照を持っていることがわかります。この一連の参照チェーンは最終的に を保持するため、プラグインが参照を正しく解放しないと、必然的にメモリ リークが発生します。binaryMessengerbinaryMessengerFlutterJNIFlutterPluginContext

上記の参照チェーンでloggerChannelどのように記述されているかを見てみましょう。

class LoggerPlugin: FlutterPlugin {
    override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        LoggerChannelImpl.init(flutterPluginBinding.getFlutterEngine())
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
    }
}

object LoggerChannelImpl { // 这是一个单例
    private var loggerChannel: BasicMessageChannel<Any>?= null

    fun init(flutterEngine: FlutterEngine) {
        loggerChannel = BasicMessageChannel(flutterEngine.dartExecutor.binaryMessenger, LOGGER_CHANNEL, JSONMessageCodec.INSTANCE)
        loggerChannel?.setMessageHandler { messageJO, reply ->
            ...
        }
    }
}

ではLoggerPlugin.onAttachedToEngine、 がFlutterEngineシングルトンに渡されてLoggerChannelImplシングルトンbinaryMessengerによって保持されており、onDetachedFromEngineメソッドが破棄されていないため、シングルトンによって参照されており、コンテキストを解放できません。

このプラグインは、設計時にマルチエンジン シナリオを考慮していない可能性があります。単一エンジンでは、プラグインはアプリケーションのライフ サイクルに従うこととonAttachedToEngine同等であるため、メモリ リークは発生しません。onDetachedFromEngineマルチエンジン シナリオでは、DartVMエンジンごとに使用されます。エンジンは、プロセスに似た isolate を割り当てます。isolate のダート ヒープ メモリは完全に独立しているため、エンジン間のオブジェクト (静的オブジェクトを含む) は相互運用できません。したがって、それぞれのインスタンスは独自のアイソレートで作成され、エンジンを作成FlutterEngineするFlutterPluginたびにプラグインのライフサイクルが繰り返されます。エンジンが破壊されると、プラグインは正常にリサイクルされず、関連する参照が時間内に解放されず、メモリ リークが発生しますContextFlutterEngine

修正:

  1. LoggerChannelImplシングルトンの書き込みを使用する必要はありません。各エンジンがMessageChannel独立していることを保証するために、シングルトンを通常のクラスに置き換えるだけです。
  2. LoggerPlugin.onDetachedFromEngineMessageChannel破壊して空にする必要があります。
ケース 3: サードパーティ ライブラリのネイティブ参照メモリ リーク

サードパーティのリーダー SDK がプロジェクトに接続されています。メモリ分析中に、リーダーを開くたびにメモリが増加し、減少できないことがわかりました。ヒープ ダンプ ファイルから、プロファイラは、メモリがあることを示しませんでした。プロジェクトでメモリ リークが発生していますが、アプリ ヒープ内に、リサイクルできない非常に多くのインスタンスを含むアクティビティがあり、メモリ使用量が多いことがわかります。

GCRoot 参照を見ると、これらのアクティビティが既知の GCRoot によって参照されていないことがわかりました。

関連するページが終了し、操作中に手動で GC が実行されているため、このアクティビティにメモリ リークがあることは間違いありません。したがって、その理由は、アクティビティが非表示の GCRoot によって参照されているということだけです。

実際、プロファイラーのヒープ ダンプでは Java ヒープ メモリの GCRoot のみが表示され、ネイティブ ヒープの GCRoot はこの参照リストには表示されません。では、このアクティビティがネイティブ オブジェクトによって保持される可能性はあるのでしょうか?

動的分析ツールを使用してAllocations Recordネイティブ ヒープ内の Java クラスの参照を調べたところ、案の定、このアクティビティの参照チェーンがいくつか見つかりました。

しかし、残念なことに、参照チェーンはすべてメモリアドレスであり、クラス名は表示されません。Activity がどこを参照しているのかを知る方法はありません。後で LeakCanary で試してみました。メモリ リークの原因は明確に記載されていましたが、ネイティブ層の参照には、Global Variable特定の呼び出し場所を提供する機能はまだありませんでした。

ソースコードに戻って、可能な呼び出し位置を分析する必要があります。これは、DownloadActivityリーダー SDK に適応するために作成した書籍ダウンロード ページです。ローカルに書籍がない場合、書籍ファイルが最初にダウンロードされ、次に SDK に渡されて SDK 独自のアクティビティが開きます。そのため、この機能はダウンロードすることになります。 、DownloadActivity校正 ブックの検証、解凍、および SDK リーダーの一部の起動プロセスの処理を行います。

大まかな考え方としては、まずダウンロードコード、検証コード、解凍コードを確認したところ、問題はありませんでしたが、リスナー等が弱い参照でカプセル化されていたため、 の記述方法がメモリリークの原因であると推測されます。 SDK自体。

リーダー SDK が開始されると、コンテキスト パラメーターがあることがわかります。

class DownloadActivity {
    ... 
    private fun openBook() {
        ... 
        ReaderApi.getInstance().startReader(this, bookInfo) 
    } 
}

この SDK のソース コードは難読化されているため、これを掘り下げてstartReaderメソッド ポイントから呼び出しチェーンを追跡することしかできません。

class ReaderApi: void startReader(Activity context, BookInfo bookInfo) 
        ↓ 
class AppExecutor: void a(Runnable var1) 
        ↓ 
class ReaderUtils: static void a(Activity var0, BookViewerCallback var1, Bundle var2) 
        ↓ 
class BookViewer: static void a(Context var0, AssetManager var1) 
        ↓ 
class NativeCpp: static native void initJNI(Context var0, AssetManager var1);

NativeCpp最後に、このクラスのメソッドに到達するとinitJNI、このローカル メソッドがアクティビティを渡していることがわかります。その後の処理は不明ですが、上記のメモリ分析に基づいて、基本的にはこのメソッドのせいであると結論付けることができます。アクティビティの参照がネイティブに渡され、存続期間の長いオブジェクトが保持されるため、アクティビティでメモリ リークが発生します。

なぜネイティブがコンテキストを使用する必要があるのか​​については、分析する方法がなく、この問題を SDK サプライヤーにフィードバックして、さらに処理してもらうことしかできません。解決策も難しくありません。

  1. リーダーが破壊されると、アクティビティ参照は即座にクリアされます。
  2. startReaderこのメソッドでは、Activity オブジェクトを指定する必要はなく、入力パラメーターの宣言を Context に変更するだけで、外部パラメーターをApplication Context渡すことができます。

誰もがパフォーマンスの最適化をより包括的かつ明確に理解できるように、関連するコア ノート (基礎となるロジックを含む) を用意しました。https://qr18.cn/FVlo89

パフォーマンスの最適化に関する重要な注意事項:https://qr18.cn/FVlo89

起動の最適化

、メモリの最適化、

UIの最適化、

ネットワークの最適化、

ビットマップの最適化、画像圧縮の最適化マルチスレッド同時実行の最適化、データ伝送効率の最適化、ボリュームパッケージの最適化https://qr18.cn/FVlo89




「Android パフォーマンス監視フレームワーク」:https://qr18.cn/FVlo89

「Androidフレームワーク学習マニュアル」:https://qr18.cn/AQpN4J

  1. ブート初期化プロセス
  2. 起動時に Zygote プロセスを開始する
  3. 起動時に SystemServer プロセスを開始する
  4. バインダードライバー
  5. AMSの起動プロセス
  6. PMSの起動プロセス
  7. ランチャーの起動プロセス
  8. Android の 4 つの主要コンポーネント
  9. Androidシステムサービス - 入力イベント配信処理
  10. Android の基盤となるレンダリング - 画面更新メカニズムのソース コード分析
  11. Android のソースコード解析の実践

おすすめ

転載: blog.csdn.net/weixin_61845324/article/details/133355082