🤖

Android 8.1でYUV変換処理が原因のメモリリーク

に公開

📢 はじめに

私は業務用アプリを開発する企業に所属しており、古いAndroid OSバージョンへの対応を求められるケースが多くあります。
現在でも Android OS 7〜9 系 は現役で利用されています。

※ Android OSのバージョンが上がるにつれてセキュリティ制限が厳しくなり、業務用途として扱いづらくなることが理由のようです。

今回、Android 8.1 環境において長時間カメラ処理を行うアプリで深刻なメモリリーク問題に直面しました。
調査したところ、日本語で同様の事例を詳しく解説した記事が見当たらなかったため、本記事として共有することにしました。

🐣 想定読者

  • Androidアプリ開発者
  • 長時間カメラを使用するAndroidアプリを扱っている人
  • Android 8.x 系端末をサポートしている人

🤖 環境

  • 問題が発生したOS: Android 8.1
  • 端末: 業務用セットトップボックス STB
  • 比較対象OS: Android 9 / Android 11
  • 開発言語: Java
  • API・SDK: パートナー企業SDK(画像処理系)
  • アプリ特徴:
    • バックグラウンドでカメラ映像を処理する
    • 長時間稼働前提

イメージ


☝🏼 直ぐ結論

メモリーリークの原因は、カメラから渡されるNV21形式の YUV を JPEG に変換する処理で呼び出したYuvImage.compressToJpeg()の内部処理に解放漏れがあることでした。

きっかけは以下の Issue を確認したことです。
https://issuetracker.google.com/issues/70016687

そこで、実際にYuvImage.compressToJpeg()を使用しない方法を試したところ、Native Heapの増加が止まりました。

<!-- 使用例 -->
YuvImage yuvimage = new YuvImage(data, ImageFormat.NV21, width, height, null);
yuvimage.compressToJpeg(new Rect(0, 0, width, height), 80, baos);

本来 Native 側では以下の解放処理が必要ですが、これが呼ばれていないことが判明しました。

jpeg_destroy_compress(&cinfo);

処理の流れは以下の通りです。

YuvImage.compressToJpeg()

内部で JNI → nativeCompressToJpeg()

libjpeg を使って JPEG 圧縮

本来必要な
jpeg_destroy_compress(&cinfo);
が無い

結果的に

  • フレームごとに native メモリが解放されない
  • カメラを連続処理すると OOM / クラッシュ
  • 特に Android 8.x 系で顕著

補足:

  • 以前からYUV変換でメモリーリークの報告はあった模様
  • OS 8.x ではNative Heapの管理方法が大きく切り替わった時期であり、YuvImage.compressToJpeg()の処理でJNI (C/C++層)内部で確保された一時的なメモリ領域の解放漏れに加え、Java GC の監視対象外となる領域が増えたことで、Native メモリリークが顕在化しやすくなった。
    • 結果としてダブルパンチで加速度的にNative Heapが増加した。
  • OS 9以降では、ネイティブメモリ管理(Native Allocation)の仕組みが強化され、かつ、YuvImage.compressToJpeg()も修正された。

そのため、
Android 9 以降では問題は発生しませんでしたが、端末の OS を気軽に更新できないため、
YuvImage.compressToJpeg() を使用しない実装へ変更して対応しました。

まとめ

  • Android 8.1は大幅な変化が多く、不安定なバージョン、なるべく避けたいOS
    • Bitmap系の管理がJava HeapからNative Heapに移った際にメモリ管理が甘い
    • YuvImage.compressToJpeg()を使用する際は注意 ⚠️

学びと所感

今回の調査で強く感じたのは以下です。

  • 今まで何となく避けてきた中華系の技術サイトも貴重な情報が多くて良さそう
  • 今までAndroidの開発はほとんどしたことが無いので、Android IssueTrackerの存在を知らなかった。GitHubのIssueと似た仕組みですね。なるほど
  • 天下のGoogleエンジニアでも不安定なコードを作成することがある (そりゃそうだ)
  • 古いAndroid OSの対応と調査はコスト高
  • 感覚的ですが、Androidの調査はやはりGeminiが強いかも?
  • 圧倒的テスト不足!!!!
    • OS8.xを使用するクライアントが今まで居なかったため、アプリが完成してから月日が経ってから本件のバグが確認されました。弊社としてはOS 8.xを禁止にしていない以上、本来はよりしっかりテストをすべきだったと反省しています

Android OS 8.xかつ、カメラストリームを長時間処理する特殊な条件下のため、この記事が多くの方の役に立つか?と言われると厳しいかと思いますが、日本のどこかのエンジニア仲間の役に立つことを願い、締めさせてもらいます。


参考

見つけた時嬉し泣き 🥲
CSDN Android 系统Api YuvImage.compressToJpeg 存在native级别的内存泄漏
Google Issue Tracker - Memory leak in YuvImage
Out of Memory when using compresstojpeg on multiple YuvImage one at a time

YUVの解説 📹
YUV入門前
YUVフォーマットの違いを世界一分かりやすく解説
Digital video concepts — YUV vs RGB(MDN Web Docs)

Discussion