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

これは [ZOZO Advent Calendar 2022](https://qiita.com/advent-calendar/2022/zozo) カレンダー Vol.6 の 17 日目の記事です。

# はじめに
この記事は「[Memory Profiler](https://developer.android.com/studio/profile/memory-profiler?hl=ja)を使ってみた」という点に主軸を置いています。その過程でComposeとAndroidビューシステムの比較を行なっていますが、両者の優劣を主張するものではありません。あくまで実装の一例の結果として捉えていただければと思います。


# 検証について
## 検証環境
- Android Studio Dolphin | 2021.3.1 Patch 1
- Pixel6a(OS13)

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

### Compose版アプリ
compose_version = '1.3.1'

<img width="200px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149859/5a128149-8217-bb14-1eec-b353fbdd4d3e.png"/>
<img width="200px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149859/7f93c5ee-c1a4-60df-57d9-0be80434d9cc.png"/>

※空の画像はこちらの[フォトスク](https://photosku.com/)というサービスからダウンロードさせていただきました


また、ソースコードは末尾の[備考](#composeのコード)に記載してます。

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

<img width="200px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149859/adbeacb1-e6b9-c0f7-8c6f-04904a181c44.png"/>
<img width="200px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149859/1902bfce-b5fb-f0a5-70bb-b688a6b39db7.png"/>

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

ソースコードは同じく末尾の[備考](#androidビューのコード)に記載してます。


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


# 結果
## Compose版の結果
![スクリーンショット 2022-12-17 1.09.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149859/5e47e130-af20-527b-f4d5-aeeeb976e1cc.png)
往復の操作数をちゃんと数えてはなかったのですが、 `AndroidImageBitmap` の数が往復した数とニアリーっぽかったのがぱっと見でわかりました。

ここでGCするとこんな感じ
![スクリーンショット 2022-12-17 1.11.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149859/eed1b370-3c3a-a341-2537-16e2eac2f038.png)
`AndroidImageBitmap` の数が１つになりました。

## Androidビューシステム版の結果
途中自動でGCが動いてCompose版と同数の往復による比較ができなかったのですが、こっちは往復の数とニアリーになっていたのはBitmapの数となっていました。
![スクリーンショット 2022-12-17 1.14.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149859/1e9f3462-7bfa-f42b-eba3-eaee88d0b03e.png)

ここでGCするとこんな感じ。Bitmapのサイズが減ってCompose版と同じになっています。
![スクリーンショット 2022-12-17 1.15.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149859/a6785a20-8347-2f2f-fec1-c77ec55d0a66.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が２倍になる現象が起きていました。
![スクリーンショット 2022-12-16 15.44.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149859/46aa831e-3cb7-18ee-a343-e9fe6e7d6760.png)

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

この辺もう少し[公式ドキュメント](https://developer.android.com/studio/profile/memory-profiler?hl=ja) をみれば分析できるのかもしれませんが、これにかける労力がタイムオーバーとなりここまでになりました。申し訳ありません。また時間が取れたら他のデータの見方などを試してみたいと思います。


# 備考
## Composeのコード

```kotlin: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 = "前の画面に戻る")
        }
    }
}
```
１ファイルで全部書けるのいいですね、このあとにAndroidビューのコード載せるんですが情報量が多いこと

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

```kotlin: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()
    }
}
```

```kotlin: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()
        }
    }
}
```

```kotlin: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()
        }
    }
}
```

```xml: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>
```

```xml: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>
```

```xml: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>
```