Androidカメラのメモリ問題の分析

推奨される適切
テキスト:作成者:バイト技術チームのビート
リンク:https://juejin.cn/post/6862508868438589447

この記事では、Androidモデルのクラスでのカメラ撮影中のネイティブメモリOOMの問題を調査し、メモリスナップショットのトリミングとネイティブメモリ監視ツールの強化を利用して、このような問題を詳細に分析します。

バックグラウンド

Raphaelは、Watermelon Video Androidチームによって開発されたネイティブメモリ監視ツールであり、Bytedanceの内部製品(Watermelon、Douyin、Toutiaoなど)のネイティブメモリリークを監視するために広く使用されています。仮想メモリのピークが原因でクラッシュした多数のメモリログシーン(pthread_create、GLエラー、EGL_BAD_ALLOCなど)がDouyin 7.8.0-8.3.0で収集され、そのうち60%以上がカメラ関連のメモリリークです。全体的なクラッシュの場合15%以上(Javaおよびネイティブ)。同時に、OPPOなどのメーカーから、新しいモデルでのDouyinアプリのネイティブクラッシュが他のモデルの3倍以上であるというフィードバックも受け取りました。メーカーから提供されたログの分析では、基本的にすべてが車両は仮想メモリのピークによって引き起こされ、そのうちの80%はカメラ関連のメモリ割り当ての失敗のログです。

問題

ネイティブメモリモニタリングによって収集されたログのスタック集約とレベルのメモリ使用統計により、OOM時にツールによってインターセプトされたネイティブメモリの合計量が約1.3G(直接使用できるネイティブ)に達していることがわかります。 32ビットアプリケーションによる)メモリの上限は約2Gです。その中で最大の割合はCameraMetaDataオブジェクトによって間接的に参照されるメモリであり、ネイティブメモリリークは非常に深刻です。

ネイティブメモリ割り当ての頻度が高すぎるため、Javaレイヤースタックの取得に時間がかかり、ネイティブメモリ割り当てをインターセプトするときにJavaスタックを頻繁に直接取得することは適切ではありません。ネイティブメモリはJavaメモリとは異なり、傍受されたデータだけから直感的に結論を引き出すことは困難です。一般に、メモリなどのリソースの不当な使用によるリソース不足が原因の問題を特定することは困難です。傍受されたデータから、CameraMetaDataは最大のメモリと最も疑わしいものを指します。この決定に基づいて、これを分析することにしました。問題。

初期分析

ネイティブメモリの割り当てと解放を分析します

インターセプトされたスタックから、CameraMetaData作成スタックの上位層がJava呼び出しであり、メモリ割り当て(boot-framework.oat&libandroid_runtime.so)が最終的にネイティブ層で実行されていることがわかります。CameraMetaDataオブジェクトには、オブジェクト自体と、mBufferが指すcamera_metadata_tが参照するメモリの、2つの部分のメモリがあります。ソースコードは、各CameraMetadataオブジェクトのmBufferが指すcamera_metadata_tが独立しており、互いに重複していないことを示しています。

ツールは非常に多くの解放されていないメモリ割り当てをインターセプトできるため、これらのメモリの解放ロジックの問題が原因である必要があります。最初にCameraMetadata.mBufferの解放ロジックを調査する必要があります。CameraMetadata.cppのソースコードを分析すると、CameraMetadata :: release()はmBufferが指すメモリを解放せず、mBufferが指すメモリを別のCameraMetadataオブジェクトに割り当てていることがわかります。CameraMetadata:: clear()は真のリリースです。clear呼び出しには2つのシナリオがあります。1つはcamera_metadata_tが再利用されるとき、もう1つはCameraMetadataオブジェクトが破棄されるときです。

前述の結論は、CameraMetadata.mBufferが指すcamera_metadata_tが互いに独立していることを示しています。ツールによってインターセプトされたスタックと割り当て量を推測することにより、ネイティブOOMのメモリに多数のCameraMetadataインスタンスが存在する必要があります。C ++オブジェクトの破棄は、通常、deleteを呼び出すことによって実装されます。削除時に変数名を知ることが難しいため、AOSPでCameraMetaDataオブジェクトが削除された場所を検索することは困難です。メモリが通常作成される基本的なC ++プログラミング仕様によれば、メモリはそこで解放される必要があります。新しいCameraMetaData文字列をグローバルに検索すると、CameraMetaDataオブジェクトの作成と解放が[/ frameworks / base / coreにあることが簡単にわかります。 。/jni/android_hardware_camera2_CameraMetadata.cpp]

android_hardware_camera2_CameraMetadata.cppの登録リストから、これらの関数に関連付けられているJavaレイヤークラスがandroid / hardware / camera2 / impl / CameraMetadataNativeであり、JavaのCameraMetadata_close関数がnativeClose関数に対応していることがわかります。さらに、CameraMetaDataNativeのnativeClose関数がclose関数で呼び出され、close関数がfinalize関数で呼び出されていることがわかります。

上記の分析により、CameraMetaDataNativeオブジェクトがfinalizeメソッドを実行し、finalizeメソッドがFinalizerDaemonスレッドで実行された場合にのみ、対応するネイティブメモリが再利用されることがわかります。上記のスタックのネイティブOOMの場合、発生した場合、finalizeメソッドを実装していないJavaレイヤーCameraMetaDataNativeオブジェクトが多数存在する必要があります。

Javaヒープサイトのトラブルシューティング

幸い、この種のネイティブOOMに対応する多数のJavaヒープメモリスナップショットファイルは、メモリスナップショットトリミングツール(Tailor)を使用して簡単に取得できます。これらのメモリスナップショットファイルは、この種のネイティブOOMが発生すると、Javaレイヤーに実際に多数のCameraMetadataNativeオブジェクトが存在するという以前の推測を完全に確認します。次の図を例として取り上げます。他のコードによって参照されるこれらのCameraMetadataNativeオブジェクトのうち6つを除いて、残りのオブジェクトはすべてFinalizerDaemonスレッドのキューにあり、finalizeメソッドが実行されるのを待っています。同時に、スナップショットには6658個のオブジェクトがあり、mMetadataPtrが0に等しいのは約600個以上のオブジェクトのみです。これは、オブジェクトのこの部分に対応するネイティブメモリをファイナライズ中に解放する必要があることを示しています。これは、傍受されたデータと完全に一致します。ツールによって、これも間接的に検証されます。ネイティブメモリモニタリングの正確性と信頼性

詳細な分析

トラブルシューティングファイナライズ実行

上記の分析は問題を検証し、以前の推測を確認しましたが、そのような問題の根本的な原因はまだ発見されておらず、最終的にそのような問題を解決する方法はまだありません。finalizeメソッドの実行を待機しているCameraMetadataNativeオブジェクトが非常に多いのはなぜか、次の調査の方向性かもしれません。Java安定性管理を行った学生は、よく知られているTimeoutException例外に注意する必要があります。このタイプの例外の根本的な原因は、ファイナライズ実行のタイムアウトです。この場合、オブジェクトのファイナライズ実行のタイムアウトが原因である可能性がありますか。

FinalizerDaemonのソースコードと組み合わせると、オブジェクトのfinalizeメソッドが実行されるたびに、現在のオブジェクトfinalizingObjectプロパティを介して記録されることがわかります。本当にファイナライズタイムアウトが原因である場合は、finalizingObjectプロパティが空でないシーンが存在する必要があります。関連するすべてのメモリスナップショットでFinalizerDaemonスレッドの状態をトラバースした後、これらのシーンのfinalizingObjectプロパティがすべて空であることがわかりました。この結果は予期しないものであり、オブジェクトのfinalizeメソッドの実行タイムアウトが原因ではないようです。

finalizingReference =(FinalizerReference <?>)queue.remove()を分析すると、このコード行の背後にあるロジックがfinalizingReference nullを呼び出さないことがわかり  ます。これは、この場所がnullを返さないことを示しています。空ではないため、queue.remove()は待機をブロックすることしかできません。ReferenceQueue.javaのソースコードも推測を確認します。

ソースコードは、goToSleepが同期メソッドであり、ブロックする可能性があることを示しています。ただし、関連するすべてのスナップショットをトラバースし、すべてのneedToWork属性がfalseであることを確認すると、渡されたことが証明されます(FinalizerWatchdogDaemon.INSTANCE.goToSleep()のみがfalseに設定され、この関数はプライベートであり、FinalizerDaemonスレッドでのみ呼び出されます) 、ブロックが入っているので、ここにはいくつかの可能性があります。


実際、ブロックがここにある理由は、通常、ファイナライズする必要のあるオブジェクトがGC中にのみFinalizerDaemonのキューに追加されるためです。一定期間GCがなく、キューが空の場合、上記の削除は常にブロックされ、GCが終了するまでオブジェクトはキューに追加されません。偶然にも、この種のネイティブOOMが発生すると、Tailorを介してJavaヒープのメモリスナップショットをアクティブにダンプし、スナップショットがダンプされるとGCとサスペンドがトリガーされます。これにより、最終的に多数のCameraMetadataNativeオブジェクトが追加されます。同時にFinalizerDaemon.queueのキューに移動します。

GC戦略を分析する

上記の分析により、GCでない場合、これらのオブジェクトはFinalizerDaemon.queueに追加されないことがわかります。これは、この種のネイティブOOMが発生する前の一定期間GCがなかったことを示しています。これにより、多数のCameraMetadataNativeオブジェクトが時間内にファイナライズを実行せず、ネイティブOOMが発生しました。上記の分析は、オフラインで撮影ページに入った後の立ち観察実験でも検証されています。その中で、Javaヒープは30〜40秒またはそれ以上ごとにGCをアクティブにトリガーします。この期間中、ネイティブメモリはGCその後、大幅に低下し、Javaとネイティブメモリは通常のレベルに戻ります。問題はファイナライズリンクのブロックではありませんが、最終的にこの問題の原因はGCロジックにロックされます。


GCを理解している学生は、ART仮想マシンには多くのGCの原因があることを知っているかもしれません。また、kGcCauseForAlloc / kGcCauseBackgroundが最も頻繁にトリガーされる仮想マシンです。操作を行わずに撮影ページにとどまる場合、プログラムロジックは比較的単純です。この期間中、カメラサービスサイクル(> = 30回/秒)のみがトリガーされ、バインダーを介してアプリケーション側にCameraMetadataNativeオブジェクトが作成されます。撮影ページにカメラキャプチャーが表示されます。このプロセスでは、JavaヒープはCameraMetadataNativeオブジェクトによってのみ作成され、CameraMetadataNative自体は比較的小さなメモリを占有します。GCの後、Javaヒープメモリが比較的豊富な場合、仮想マシンは長時間GCをアクティブにトリガーしません。この期間中のネイティブメモリの増加が大きすぎる場合、次のGCの前にピークに達するとネイティブOOMが発生します

要約すると、このタイプのネイティブOOMの基本的な理由は、アプリケーション自体のネイティブメモリがすでに高水位にある場合、カメラの電源を入れた後、カメラサービスはアプリケーション側でCameraMetadataNativeオブジェクトを作成し続けることです。バインダー通信、およびCameraMetadataNativeオブジェクトの作成を同時に行います。アプリケーション側では、camera_metadata_tを格納するための比較的大きなメモリが、jniインターフェイスを介してネイティブレイヤーで作成/再利用されます。Javaレイヤー自体のCameraMetadataNativeオブジェクトは比較的小さいため、小さなオブジェクトを継続的に作成するこの動作では、一定期間内にJavaレイヤーのGCをトリガーすることはほとんどできません。これにより、間接的に参照されるネイティブメモリが継続的に上昇し、最終的にトリガーされます。仮想メモリの上限とクラッシュ。

ソリューション

問題の原因は比較的単純ですが、このタイプの問題をどのように解決するかを決定することは依然として困難です。GCは時間内に発生しないため、簡単な解決策は、撮影ページで定期的にGCをトリガーすることです。ただし、GC間隔が比較的小さい場合は、結局GCに時間がかかります。GCの頻度が高すぎると、撮影体験に深刻な影響を及ぼします。GC間隔が比較的長い場合でも、この種のミスを繰り返す可能性が高くなります。ネイティブOOM。

パフォーマンスへの影響と、GCをアクティブにトリガーするスキームとのバランスを取ることは困難です。実際、問題の焦点はJava層ではなく、Javaオブジェクトによって参照されるネイティブメモリです。メモリのこの部分が時間内にアクティブに解放されれば、この種の問題は完全に解決できます。以前の分析から、メモリのこの部分は元々GCのファイナライズリンクで回復されたことがわかりますが、CameraMetadataNativeが使用されなくなったことが事前に発見された場合は、メモリのこの部分を解放するようにトリガーできます。前進すれば、それは一度限りで行うことができます。ソースコードを分析すると、CameraMetadataNativeは、アプリケーション層に渡された後は使用されていないことがわかります。アプリケーション層がCameraMetadataNativeオブジェクトを使用した後、リフレクションを介してclose関数を呼び出すことにより、参照されているネイティブメモリを解放できます。[画像のアップロード...(image-98dc21-1615988565752-1)]

オフライン実験では、アクティブリカバリ戦略をオンにした後、ネイティブメモリの増加率が以前よりも大幅に低くなることもわかります。この期間中、Javaヒープとネイティブレイヤーは小さなオブジェクトで増加し続けますが、ネイティブの成長速度はJavaレイヤーの成長速度よりもはるかに遅いです。このシナリオでは、Javaメモリはネイティブメモリが最上位に達する前にGCをトリガーします、ネイティブOOMの発生を大幅に削減します。可能性

結局、プログラムがオンラインになった後、その影響は非常に明白であり、そのようなクラッシュ(Javaとネイティブの合計比率> 15%)は基本的にクリアされました。後で収集されるメモリ監視ログのCameraMetadataに関連するメモリは、基本的に2M以内であり、すぐに効果があります。

総括する

この種の問題は、少なくともAndroid 4.4以降、CameraMetadataNativeのファイナライズ機能を介してネイティブメモリが解放されるまで、長い間存在していました。以前は、撮影の需要は比較的単純でした。ほとんどの場合、ROMに付属のカメラアプリケーションを使用して写真を撮影していました。この種のアプリは比較的単純であるため、ネイティブメモリレベル自体が非常に低く、仮想メモリの上限をトリガーするのは難しいので、この種の問題は明らかにされませんでした。小さな動画などのアプリの台頭に伴い、撮影(特殊効果や美しさなど)の需要が高まり、アプリはますます複雑になっています。アプリ自体のネイティブメモリレベルは、ネイティブメモリと相まって上昇し続けています。リークやその他の理由。このタイプの問題は、撮影ページに留まっているときに簡単に引き起こされる可能性があります。

さらに、CameraMetadataのメモリ割り当てが失敗しても、直接クラッシュすることはありません。このとき、他のメモリ割り当て要求がクラッシュをトリガーします(スレッドの作成、GLメモリ割り当てなど)。これは、の根本的な原因でもあります。撮影中の多くのカメラの黒い画面の問題。このソリューションは、撮影時のカメラの黒い画面の長年の問題を誤って解決します。

このタイプの問題には、アプリケーション自体の理由とメモリ再利用戦略の設計の両方があります。リークを可能な限り減らす一方で、アプリケーションはネイティブメモリの水位を下げるように努める必要があります。AOSPでJavaのfinalizeメソッドを使用して、間接的に参照されるネイティブメモリを解放するのは怠惰な設計であり、同様のケースがAOSPにたくさんあります。実際の開発では、メモリなどの限られたリソースを時間内に回復する必要があり、オブジェクトのライフサイクルも積極的に制限できます。ミッションが完了すると、ミッションが占有するメモリが積極的に回復され、ファイナライズロジックの使用が回避されます。ネイティブメモリを解放します。

この記事で改善された2つのツール(ネイティブメモリ監視ツールRaphael&Androidヒープメモリスナップショットクロッピング圧縮ツール)は、長期記憶最適化管理でスイカビデオAndroidチームによって開発された効率的で実用的な2セットの基本ツールです。アプリケーションは非常に広範囲であり、メモリの最適化と安定性の管理のための絶対的な最初の選択肢です。これら2セットのツールについては、監視ツールの構築や最適化されたガバナンスプラクティスなど、関連する技術的な詳細を後続の技術記事で紹介しますので、ご期待ください。

Android関連の知識について詳しく知りたい場合は、私の[ GitHubプロジェクト:https://github.com/733gh/GH-Android-Review-master ]をクリックして、自分で確認することができます。その中にAndroidの知識ポイントを記録します。最後に、応援よろしくお願いします!

画像

おすすめ

転載: blog.csdn.net/dongrimaomaoyu/article/details/114951734