これは 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版の結果
往復の操作数をちゃんと数えてはなかったのですが、 AndroidImageBitmap
の数が往復した数とニアリーっぽかったのがぱっと見でわかりました。
ここでGCするとこんな感じ
AndroidImageBitmap
の数が1つになりました。
Androidビューシステム版の結果
途中自動でGCが動いてCompose版と同数の往復による比較ができなかったのですが、こっちは往復の数とニアリーになっていたのはBitmapの数となっていました。
ここでGCするとこんな感じ。Bitmapのサイズが減ってCompose版と同じになっています。
結果の比較
今回は画像データの影響を受けやすそうな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倍になる現象が起きていました。
ちなみにAndroidビューシステムは同じ状況でも本記事に載せている結果とほとんど同じで2倍まで増えることはなかったです。
この辺もう少し公式ドキュメント をみれば分析できるのかもしれませんが、これにかける労力がタイムオーバーとなりここまでになりました。申し訳ありません。また時間が取れたら他のデータの見方などを試してみたいと思います。
備考
Composeのコード
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ビューシステムのコード
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()
}
}
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()
}
}
}
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 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>
<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 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>