Jetpack ComposeはAndroidシステムの新たなUIフレームワークです。従来のViewシステムと、アプリ画面の描画の仕組みが異なります。
このJetpack Composeによるアプリ画面の描画について、仕組みの大枠をまとめます。
※環境:Android Studio Giraffe | 2022.3.1 Patch 1
Kotlin 1.8.10
Compose Compiler 1.4.3
フレーム処理
Androidは周期的(1/60秒が目標)にフレームメモリーへ1枚の画面データを描画(展開)しています。これにより、画面の更新やシームレスなアニメーションを表現しています。
ここでは、この処理を「フレーム処理」と呼ぶことにします。
フレーム処理の前半は描画処理を実行し、描画コマンドを作成します。これはUIスレッドが担当します。※描画処理については後述
( これ以降は推測ですが… )
フレーム処理の後半は描画コマンドに従い、フレームメモリーへ画面データを描画(展開)します。これはRenderスレッドが担当します。通常、この処理を行うのはOpen GL ESで操作されるGPUです。
Jetpack ComposeのUIツリーはActivity#setContent( )関数によりActivityへ関連付けられます。この時、UIツリーの接続先はViewシステムのルートです。つまり、根底はViewシステムと同じ構成になっていると考えられます。

※Layout Inspectorの一部を抜粋

※Layout Inspectorの一部を抜粋
描画処理
描画処理は3つのフェーズで構成され、上から下へ一方向に処理されます。
3フェーズ(Composition, Layout, Drawingの順番)の実行が基本です。
しかし、表示の状態(変更の有無)によって、フェーズ毎に実行したりスキップしたりします。※フェーズの実行については後述
フェーズ:Composition
Composable関数を実行することで、関数(UI要素)のツリー構造をデータ化し、メモリーへ格納します。この処理をComposeといい、データをCompositionといいます。
※Composable関数:@Composableの付いた関数
※ComposeコンパイラがComposable関数をComposeするバイトコードへ変換
フェーズ:Layout
Compositionの結果に従って、UI要素のサイズ(w,h)と位置(x,y)を算出します。
フェーズ:Drawing
Layoutの結果に従って、UI要素をCanvasへ描画します。
フェーズの実行
周期的に行われる描画処理において、表示が変更されないUI要素(Composable関数)の再描画は無駄な処理です。ですので、表示が変更されたUI要素のみを再描画する手法が取られます。
しかも、この再描画の判断はフェーズ毎に行われます。つまり、各フェーズは表示が変更された場合に実行され、変更されない場合はスキップします。
実行の条件
フェーズの実行の条件は図のようになります。
各フェーズの状態の読み取り対象が異なる点に注意してください。
フローチャートに示した通り、フェーズ:Compositionが実行されると、フェーズ:LayoutとDrawingの実行は必須となります。
Jetpack Composeのドキュメントに「再Compose」という言葉が登場します。これは、再描画でフェーズ:Compositionを実行する事と同意です。さらに、再描画で描画処理(3フェーズ全て)を実行する事と同意です。
実行の例
例はフェーズの実行を見える形に表現したサンプルです。
アプリの起動から10秒後に状態が更新されます。これにより表示が変更されて、条件に合ったフェーズが実行されます。
- 上段の写真 –> フェーズ:Drawingを実行
- 中段の写真 –> フェーズ:Layoutを実行
- 下段の写真 –> フェーズ:Compositionを実行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | val _photos = arrayOf( Pair( "Azami" , R.drawable.azami), Pair( "Bike" , R.drawable.bike), Pair( "Leaf" , R.drawable.leaf1), Pair( "Leaf" , R.drawable.leaf2) ) setContent { val _dummy = remember { mutableStateOf( 0 ) } val _flag = mutableStateOf( false ) val _photo0 = _photos[ 0 ].second val _modifier0 = Modifier.drawWithContent { drawContent() if (_flag.value) // _flagの変更 -> Drawingをスケジューリング drawRoundRect(color = Color.Red, style = Stroke( 3 .0f)) } val _photo1 = _photos[ 1 ].second val _modifier1 = Modifier.offset { if (_flag.value) // _flagの変更 -> Layoutをスケジューリング IntOffset( 10 .dp.roundToPx(), 0 ) else IntOffset.Zero } var _photo2 = _photos[ 2 ].second val _modifier2 = Modifier MyApplicationTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Photo(_photo0, modifier = _modifier0) Spacer(modifier = Modifier.size( 10 .dp)) Photo(_photo1, modifier = _modifier1) Spacer(modifier = Modifier.size( 10 .dp)) Photo(_photo2, modifier = _modifier2) Spacer(modifier = Modifier.size( 10 .dp)) // ↓↓Surface階層に再Composeをスケジューリングためのダミー↓↓ val __dummy = _dummy.value Button(onClick = {_dummy.value++}) { Text(text = "Recompose ${__dummy}" ) } } } } // 別スレッドで状態を更新 val _coroutineScope = rememberCoroutineScope() _coroutineScope.launch(Dispatchers.Default) { delay( 10000 ) _flag.value = true _photo2 = _photos[ 3 ].second // _photo2の変更 -> Compositionをスケジューリング } } |
1 2 3 4 5 6 7 | @Composable fun Photo( @DrawableRes id: Int, modifier: Modifier = Modifier) { Image( painter = painterResource(id), contentDescription = null , modifier = modifier.size( 140 .dp, 105 .dp) ) } |
フェーズ:DrawingとLayoutは再Compose(再Composition)で無いため、Layout Inspectorで観測(カウント値)されません。
※Layout Inspectorの表示は「Jetpack Compose:Layout Inspectorで再Composeを確認」を参照
関連記事: