Android Jetpack Compose による再編成範囲の決定と再編成の最適化 1. 概要

1。概要

前回の記事で、Compose の再構成がインテリジェントであると述べましたが、Composable 機能は、再構成の際に不要な再構成を可能な限りスキップし、変更が必要な UI のみを再構成します。では、Compose は UI を変更する必要があるとどのように判断するのでしょうか? 言い換えれば、Compose は再編成の範囲をどのように決定するのでしょうか。再編成がランダムに発生すると、UI のパフォーマンスは非常に不安定な状態になり、良い場合もあれば悪い場合もあります。また、記述された UI コードに問題がある場合、再編成により状態が混乱し、UI 表示エラーが発生します。したがって、Compose の再編成の範囲を明確にすることによってのみ、再編成の落とし穴を回避し、特定の範囲に合わせて最適化することができるため、この記事では、Compose の再編成の範囲を決定し、再編成のパフォーマンスを最適化する方法を紹介します。

2. コンポーザブル再編成の範囲を決定する

再編成の範囲を決定することは、ComposeUI のパフォーマンスの最適化をより深く理解するのに役立ちます。まず例を見てみましょう。

    @Composable
    fun CounterDemo(){
        Log.d("zhongxj","范围1=>运行")
        var counter by remember { mutableStateOf(0) }
        Column {
            Log.d("zhongxj","范围2=>运行")
            Button(onClick = {
                Log.d("zhongxj","onButtonClick:点击按钮")
                counter ++
            }){
                Log.d("zhongxj","范围3=>运行")
                Text(text = "+")
            }

            Text(text = "$counter")
        }
    }

上記のコードでも、再編成の範囲を確認するためにカウンターの例を使用しています。再編成が発生する可能性があるすべての場所に Log を配置します。ボタンをクリックすると、カウンターのステータス更新が CounterDemo の再編成をトリガーします。ログは以下のようになります。ここに画像の説明を挿入します

この図から、ログのこの行が型指定されていないことがわかりますLog.d("zhongxj","范围3=>运行")。ログのこの行が型指定されていない理由を理解するには、Compose 再編成の基本原理を理解する必要があります。

Compose では、Compose コンパイラによって処理される Composable 関数は、State の読み取り中に自動的に関連付けを確立できます。実行プロセス中、State が変化すると、Compose は関連付けられたコード ブロックを見つけて、無効としてマークします。次のレンダリング フレームが到着する前に、 , Compose は再編成をトリガーして無効なコード ブロックを実行し、無効なコード ブロックは次の再編成のスコープになります。無効としてマークできるコードには 2 つの要件があります。1 つ目は、無効としてマークされたコードは、戻り値のない非インラインのコンポーザブル関数である必要があり、2 つ目は、戻り値のない Lambda である必要があります。

では、なぜ再編成に関与するコード ブロックは非インライン、非戻り関数でなければならないのでしょうか? インライン関数はコンパイル中に呼び出しサイトで展開されるため、次の再編成中に適切な呼び出しエントリを見つけることができず、呼び出し元の再編成スコープのみを共有できます。値を返す関数の場合、戻り値は呼び出し元に影響するため、再編成に参加するには呼び出し元に連絡する必要があります。したがって、戻り値のあるインライン関数は無効なコード ブロックとして使用できません。

そして、Compose の基礎となる再編成原理を理解することで、我们就可以清楚的知道了只有受到State变化影响的代码块,才会参与到重组。不依赖State的代码则不参与重组,这就是重组的最小化原则。

再編成最小化の原則に基づいて、反例の出力結果を分析できます。実際、ログを確認したところ、ログのLog.d("zhongxj","范围3=>运行")この行が記録されていないことがわかりました。つまり、ログのこの行が記録されているコード ブロックはは再編成に参加しておらず、スコープ 2 のスコープ内にあります。このコード行が表示されますText(text = "$counter")。このコード行がカウンタの状態に依存していることは明らかです。このコード行が意味するものではないことに注意してください。カウンタの値を読み取るとは、スコープ2のスコープでカウンタを読み取ることを意味します。値はTextに渡されるので、スコープ2も再編成に参加し、ログが出力されます。このとき、読者によってはLog.d("zhongxj","范围2=>运行")、再編成最小化の原則によれば、カウンタにアクセスするための最小スコープは、スコープ 2 のスコープである必要があります。なぜレンジ 1 のログも出力されるのですか? ここで、前に述べたことを思い出す必要があります。最小化されたスコープの定義は、非インラインでコンポーズ可能な関数またはラムダである必要があります。Column コンポーネントはインラインで宣言された高階関数です:ここに画像の説明を挿入しますそのため、呼び出しサイトで内容が内部展開されるため、スコープ 1 とスコープ 2 が再編成のスコープを共有するため、ログが出力されますLog.d("zhongxj","范围1=>运行")。 -inline Composable. の場合、Log.d("zhongxj","范围1=>运行")出力はありません。たとえば、Card コンポーネントに置き換えられた場合、読者は自分で試すことができます。

なお、Button はカウンタに依存していませんが、スコープ 2 の再編成により Button の再呼び出しが発生するため、それも出力されますが、その内容は内部的にカウンタにLog.d("zhongxj","onButtonClick:点击按钮")依存していないため、スコープ 3 のログは出力されません。 : Log.d("zhongxj" , "scope 3 => run") は出力されません。

补充说明: Composable 函数观察State变化并触发重组是在被称为”快照“的系统中完成的,所谓”快照“就是将被访问的状态像拍照一样保存下来,当状态变化时,通知相关的Composable应用的最新状态。”快照“有利于对状态管理进行线程隔离,在多线程场景下的重组有重要的应用

3. 再編パフォーマンスの最適化

前回の分析では、Compose の再編成がインテリジェントであり、スコープ最小化の原則に従っていることがわかりませんでした。再編成中に実行されたコンポーザブルは、パラメータが変更された場合にのみこの再編成に参加します。

Compose は実行後にビュー ツリーを生成し、各 Composable はツリー上のノードに対応しますComposable 智能重组的本质其实是从树上寻找对应位置的节点并与之进行比较,如果节点未发生变化则不用更新

また、ビューツリーの実際の構築プロセスは比較的複雑であることに注意してください。Composable の実行中、生成されたコンポジション状態はまず SlotTable に格納され、フレームワークは SlotTable に基づいて LayoutNode ツリーを生成し、最終的なインターフェイスのレンダリングが完了します。したがって、Composable の比較ロジックは SlotTable で行われると考えるのが賢明です。

3.1 構成可能な位置インデックス

再編成プロセス中、Composition 上のノードは、追加、削除、移動、更新などのさまざまな変更を完了できます。Compose コンパイラは、コード呼び出し位置に基づいて Composable のインデックス キーを生成し、Composition に保存します。Composable は、それを実行中に渡します。 Key との比較により、現在どのような操作を実行すべきかを知ることができます。たとえば、次のサンプルコード:

    Box {
            if (state) {
                val str = remember(Unit) { "call_site_1" }
                Text(text = str) // Text_of_call_site_1
            } else {
                val str = remember(Unit) { "call_site_2" }
                Text(text = str) // Text_of_call_site_2
            }
        }

上記のコードに示すように: Composable が if/else などの条件ステートメントに遭遇すると、startXXXGroup に似たコードを挿入し、インデックス キーを追加することでノードの増加または減少を識別します。上記のコードは、さまざまな条件に応じて異なるテキストを表示します。コンパイラは if 分岐と else 分岐のインデックスをそれぞれ作成します 状態が true から false に変化すると、ボックスが再編成されます キーの判定により、else のコードを挿入する必要があることがわかりますロジックの実行に組み込まれ、if で生成されたノードを削除する必要があります。

コンパイル時の位置インデックスがなく、実行時の比較のみに依存すると仮定すると、最初に remember(Unit) を実行するときに、キャッシュ上の理由により現在のツリーに格納されている str (つまり call_site_1) が返され、その後実行されます。 text_of_call_site_1 を見ると、現在のツリーに格納されている str と同じであることがわかり、ノードの種類も同じで、パラメータ str も変更されていないため、再編成の必要がないと判断され、テキストを再構成することはできません。更新します。

要約すると、コンパイル時のコンポーザブル インデックス作成は、再編成をインテリジェントかつ正確に実行できるようにするための基礎となります。このインデックスは、静的コード内で Composable が呼び出される場所に基づいて決定されます。ただし、シナリオによっては、静的コードの場所を介して Composable にインデックスを作成できない場合があるため、再編成中の比​​較を容易にするために手動でインデックスを追加する必要があります。

3.2 Keyによるインデックス情報の追加

映画のリストを与え、映画に関する一般的な情報を表示する必要があるとします。コードは次のとおりです。

@Composable
    fun MoviesScreen(movies:List<Movie>){
        Column { 
            for (movie in movies){
                // showMoveCardInfo 无法在编译期间进行索引,只能根据运行时的index进行索引
                showMoveCardInfo(movie)
            }
        }
    }

上記のコードに示すように、ムービーの情報はムービーの名前に基づいて表示されます。現時点では、コード内の位置に基づいてインデックスを作成することはできません。実行時のインデックスに基づいてのみインデックスを作成できます。この場合、項目数に応じて指数が変化してしまい、正確に比較することができなくなります。この場合、再編成が発生すると、新しく挿入されたデータと以前の 1 番目のデータとが比較され、以前の 1 番目のデータと 2 番目のデータが比較され、その後、以前の 2 番目のデータが新しいデータが挿入されたものとして扱われます。結果として、すべての項目が再編成されますが、予期される動作では、新しく挿入されたデータのみが再編成される必要があり、その他の変更されていないデータは再編成されるべきではないため、key メソッドを使用して手動でコンポーザブルにインデックスを追加できます。ランタイム。次のように:

@Composable
    fun MoviesScreen(movies:List<Movie>){
        Column { 
            for (movie in movies){
               key(movie.id){ // 使用movie的唯一ID作为Composable的索引
                showMoveCardInfo(movie)
                }
            }
        }
    }

Composable に一意のインデックスとして渡されるムービー ID を使用します。新しいデータが挿入されるとき、以前のオブジェクトのインデックスは破壊されず、比較中に引き続きアンカーの役割を果たすことができるため、変更されていない他のアイテムは、変更する必要がありません。再編に参加します。

3.3 アノテーション @Stable を使用して再編成を最適化する

Composable は、パラメータの比較結果に基づいて再編成するかどうかを決定します。つまり、比較に参加するパラメータ オブジェクトが安定しており、equals が true を返す場合にのみ、それらは等しいとみなされます。Kotlin の一般的な基本型 (Boolean、Int、Long、Float、Char) String 式と Lambda 式はすべて不変型であるため、安定していると考えることができます。したがって、パラメータ比較の結果は信頼できるものになります。ただし、パラメータが変数型の場合、比較結果は信頼できません。

data class Mutabledata(var data:String)

    @Composable
    fun MutableDemo(){
        var mutable = remember { Mutabledata("walt") }

        var state by remember { mutableStateOf(false) }
        if(state){
            mutable.data = "zxj"
        }

        Button(onClick = {state = true}){
           showText(mutable)
        }
    }
    @Composable
    fun ShowText(mutable:MutableData){
     Text(text = mutable.data) // 会随着state的变化而变化
    }

上記のコードでは、MutableData は Var 型の変数データを持っているため、不安定なオブジェクトです。ボタンをクリックして状態を変更すると、可変オブジェクトがデータを変更します。ShowText の場合、パラメーター可変テーブルは前後で同じ値を指します。オブジェクトなので、equals だけで判断するとパラメーターは変わっていないと思われますが、実際には ShowText 関数が再編成されていることがテストで判明したため、Mutabledata パラメーターの型が不安定で、equals の結果は次のようになります。信頼できない。

したがって、インターフェイスやリストなど、デフォルトでは安定しているとはみなされない一部のコレクション クラスについては、実行時の安定性を確保できる場合は、それらに @State アノテーションを追加できます。そうすれば、コンパイラはこれらの型を安定した型として扱います。したがって、組織再編の役割を果たし、パフォーマンスを向上させます。コードは次のようになります。

@Stable
interface UiState<T>{
    val value:T?
    val exception:Throwable?
    val hasError:Boolean
        get() = exception != null
}


注意: 被添加为@Statble的普通父类、密封类、接口等其派生子类也会被认为时稳定的

おすすめ

転載: blog.csdn.net/Coo123_/article/details/133393809