11
1

More than 1 year has passed since last update.

AndroidStudioのMemory Profilerを使ってComposeとAndroidビューシステムのメモリ使用量を比較してみた

Last updated at Posted at 2022-12-16

これは ZOZO Advent Calendar 2022 カレンダー Vol.6 の 17 日目の記事です。

はじめに

この記事は「Memory Profilerを使ってみた」という点に主軸を置いています。その過程でComposeとAndroidビューシステムの比較を行なっていますが、両者の優劣を主張するものではありません。あくまで実装の一例の結果として捉えていただければと思います。

検証について

検証環境

  • Android Studio Dolphin | 2021.3.1 Patch 1
  • Pixel6a(OS13)

検証アプリ

Compose版とAndroidビューシステム版で2つのActivityを用意してそれぞれで同じようなレイアウトを実装しました。
アプリとしては2つの画面から構成されます。
1つ目の画面では「2つ目の画面への遷移」と「画像の表示」の機能をもち、2つ目の画面では「1つ目の画面へ戻る」の機能のみを持ちます。

Compose版アプリ

compose_version = '1.3.1'

※空の画像はこちらのフォトスクというサービスからダウンロードさせていただきました

また、ソースコードは末尾の備考に記載してます。

Androidビューシステム版アプリ

※ところどころCompose版とデザインが異なっていますがご了承ください

ソースコードは同じく末尾の備考に記載してます。

検証方法

それぞれの画面内に設置されたボタンを押して1つ目の画面と2つ目の画面を交互に何度も行き来しながらGCの動きにも注意しつつCapture heap dumpの結果をみていきました。

結果

Compose版の結果

スクリーンショット 2022-12-17 1.09.04.png
往復の操作数をちゃんと数えてはなかったのですが、 AndroidImageBitmap の数が往復した数とニアリーっぽかったのがぱっと見でわかりました。

ここでGCするとこんな感じ
スクリーンショット 2022-12-17 1.11.16.png
AndroidImageBitmap の数が1つになりました。

Androidビューシステム版の結果

途中自動でGCが動いてCompose版と同数の往復による比較ができなかったのですが、こっちは往復の数とニアリーになっていたのはBitmapの数となっていました。
スクリーンショット 2022-12-17 1.14.01.png

ここでGCするとこんな感じ。Bitmapのサイズが減ってCompose版と同じになっています。
スクリーンショット 2022-12-17 1.15.50.png

結果の比較

今回は画像データの影響を受けやすそうなNative Sizeの変化の度合いに注目しました。(見れるところは他にもいろいろあるかと思いますが)

dumpのタイミング Compose版のNative Size Androidビューシステム版のNative Size
往復操作直後 10,721,138 11,470,948
GC後 10,717,081 10,718,569
差分 4,057 752,379

Compose版はGC前とあとで Native Size にそこまで差はありませんでした。
一方でAndroidビューシステム版はGC前とあとで Native Size にCompose版よりも差があり、さらに Bitmap のサイズに変化が見られました。

まとめと考察

Androidビューシステムの方は、Bitmapのサイズが画面の往復を重ねるごとに増えて行ったのに対し、Compose版はずっと変わらない挙動となっていました。あまり私自身メモリ管理に詳しいわけでもないのでAndroidビューシステム側の実装方法に問題があっただけの可能性もありますが、今回の結果だけ見るとCompose版の方がメモリ効率がいいのかも?と思わせるような結果となりました。

ただ、実はこの検証再現性があやしく、他のアプリを複数起動状態で同様の検証をするとComposeの方だけBitmapが2倍になる現象が起きていました。
スクリーンショット 2022-12-16 15.44.57.png

ちなみにAndroidビューシステムは同じ状況でも本記事に載せている結果とほとんど同じで2倍まで増えることはなかったです。

この辺もう少し公式ドキュメント をみれば分析できるのかもしれませんが、これにかける労力がタイムオーバーとなりここまでになりました。申し訳ありません。また時間が取れたら他のデータの見方などを試してみたいと思います。

備考

Composeのコード

MainComposeActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MemoryChecker()
        }
    }
}

@Composable
fun MemoryChecker() {
    MemoryCheckerTheme {
        val navController = rememberNavController()
        NavHost(
            navController = navController,
            startDestination = "first"
        ) {
            composable("first") {
                FirstScreen {
                    navController.navigate(it)
                }
            }
            composable("second") {
                SecondScreen {
                    navController.popBackStack()
                }
            }
        }
    }
}

@Composable
fun FirstScreen(onClickTransitionButton: (String) -> Unit) {
    Column() {
        Text(text = "FirstScreenComposable")
        Button(onClick = { onClickTransitionButton("second") }) {
            Text(text = "Go SecondScreenComposable")
        }
        Image(painter = painterResource(id = R.drawable.test), contentDescription = "blue sky")
    }
}

@Composable
fun SecondScreen(onClickBackButton: () -> Unit) {
    Column() {
        Text(text = "SecondScreenComposable")
        Button(onClick = { onClickBackButton() }) {
            Text(text = "前の画面に戻る")
        }
    }
}

1ファイルで全部書けるのいいですね、このあとにAndroidビューのコード載せるんですが情報量が多いこと

Androidビューシステムのコード

MainViewActivity
class MainViewActivity : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_view_activity)
        supportFragmentManager.beginTransaction()
            .add(R.id.fragment_container, ViewFirstFragment())
            .commit()
    }
}
ViewFirstFragment
class ViewFirstFragment : Fragment() {

    lateinit var binding: FirstFragmentBinding

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = FirstFragmentBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.transactionButton.setOnClickListener { v ->
            parentFragmentManager.beginTransaction()
                .replace(R.id.fragment_container, ViewSecondFragment(), "second")
                .addToBackStack("first")
                .commit()
        }
    }
}
ViewSecondFragment
class ViewSecondFragment : Fragment() {

    lateinit var binding: SecondFragmentBinding

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = SecondFragmentBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.back.setOnClickListener { v ->
            parentFragmentManager.popBackStack()
        }
    }
}
first_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="ViewFirstFragment" />

        <Button
            android:id="@+id/transaction_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Go SecondFragment" />

        <ImageView
            android:id="@+id/image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/test" />

    </LinearLayout>
</layout>
second_fragment.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="SecondFirstFragment" />

        <Button
            android:id="@+id/back"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="前の画面に戻る" />

    </LinearLayout>
</layout>
main_view_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <FrameLayout
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>
11
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
1