The most complete analysis of Android jectpack compose [recommended collection]

1. @Composable

A function annotated with @Composable will change the function type , which internally depends on Composer throughout the scope of the function . The characteristics of @Composable are as follows:

  • @Composable is not an annotation processor in essence . Compose relies on the Kotlin compiler plug-in to work during the type detection and code generation phase of the Kotlin compiler, so Compose can be used without an annotation processor.

  • @Composable will cause its type to change , and the same function type that is not annotated is incompatible with the annotated type

    @Composable will assist the kotlin compiler to know that this function is used to convert data into UI interface, which is used to describe the display state of the screen

  • @Composable is not a language feature and cannot be implemented as a language keyword

Next, we analyze its internal implementation based on the simplest function

The kotlin code is as follows:

@Composable
fun HelloWord(text: String) {
    Text(text = text)
}

The decompiled code is as follows:

public static final void HelloWord(String text, Composer $composer, int $changed) {
        int i;
        Composer $composer2;
        Intrinsics.checkNotNullParameter(text, "text");
        Composer $composer3 = $composer.startRestartGroup(1404424604,"C(HelloWord)7@159L17:Hello.kt#nlh07n");
        if (($changed & 14) == 0) {
            i = ($composer3.changed(text) ? 4 : 2) | $changed;
        } else {
            i = $changed;
        }
        if (((i & 11) ^ 2) != 0 || !$composer3.getSkipping()) {
            $composer2 = $composer3;
            TextKt.m855Text6FffQQw(text, null, Color.m1091constructorimpl(ULong.m2915constructorimpl(0)), TextUnit.m2481constructorimpl(0), null, null, null, TextUnit.m2481constructorimpl(0), null, null, TextUnit.m2481constructorimpl(0), null, false, 0, null, null, $composer2, i & 14, 0, 65534);
        } else {
            $composer3.skipToGroupEnd();
            $composer2 = $composer3;
        }
        ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();
        if (endRestartGroup != null) {
            endRestartGroup.updateScope(new HelloKt$HelloWord$1(text, $changed));
        }
    }

There are many concept points inside the function, so let's analyze them one by one.

2. Composer

Compose is actually the context that runs through the scope of the entire Composable function , providing some methods for creating internal Groups and so on. The internal data structure is Slot Table , and its characteristics are as follows:

  • Slot Table is a type that stores data in continuous space , and the underlying layer is implemented by an array. But the difference lies in its remaining space , called Gap .

    • Gap has the ability to move to any area , so it is more efficient when inserting and deleting data.
  • Slot Table is essentially a linear data structure , so it supports storing View tree in Slot Table.

    • According to the characteristics of the movable insertion point of the Slot Table, the data structure of the entire View tree does not need to be recreated after the View tree is changed .
  • Although Slot Table has the ability to insert data at any position compared to ordinary arrays, Gap movement is still an inefficient operation .

    • Google judges that interface updates with a high probability are data changes , and the View tree structure does not change frequently .
    • And basically only the continuous memory data structure of the array can meet the requirements of Compose Runtime in terms of access efficiency

ps: All Composeable functions depend on Composer internally, so non-Composeable functions cannot call Composeable functions

![image-20210718173155867](/Users/dumengnan/Library/Application Support/typora-user-images/image-20210718173155867.png)

At this point, we reverse and try to infer several results:

  • If you use a linked list, the time complexity of inserting = o(1), but the time complexity of searching = o(n),

  • If you use an array, the time complexity of searching = o(1), and the time complexity of inserting = o(n).

    In addition to gap buffer there are other solutions:

    1. Block linked list (insert and search can be done at o(n^1/2) complexity)
    2. Rope tree (a balanced search tree)
    3. piece table (an improved version of the gap buffer, which is the algorithm used by Microsoft doc, and can quickly achieve undo and reorganization)
  • The logic code is as follows:

    int GAP_SIZE = 5;//默认gap 大小
    
    //基本结构
    struct GapBuffer{
    	char array[size];
    	int gapStart;
    	int gapSize;
    	int len;
    } gapBuffer;
    
    //插入逻辑实现
    void insert(char c){
    	if(gapBuffer.gapSize<0)
    		expanison();
    	gapBuffer.array[++gapBuffer.gapStart]=c;
    	--gapBuffer.gapSize;
    	++len;
    }
    
    //扩容逻辑实现
    void expanison(){
      //扩容两倍增长
    	GAP_SIZE = GAP_SZIE*2;
    	gapBuffer.gapSize = GAP_SIZE;
      //将数据后半段向后复制,给予GAP_SiZE空间
    	arraycopy(gapBuffer.array,gapBuffer.gapStart,gapBuffer.gapStart+gapBuffer.gapSize,len-gapBuffer.start);
    }
    
    //移动gap逻辑实现
    //不会扩容buffer
    void moveGap(int pos){
      //相同位置不做移动
    	if(gapBuffer.gapStart == pos)return;
    	
    	//如果pos小于当前gap
    	if(pos<gapBuffer.gapStart)
        //copy数组
    		arraycopy(gapBuffer.array,pos,pos+gapBuffer.gapSize,gapBuffer.gapStart-pos);
    	else
        //copy数组
    		arraycopy(gapBuffer.array,gapBuffer.gapStart+gapBuffer.gapSize,gapBuffer.gapStart,gapBuffer.gapSize);
    	
    }
    //数组拷贝逻辑实现
    void arraycopy(char array[],int srcSatrt,int dstStart,int len){
    	for(int i = 0;i<len;++i)
    		array[dstStart+i]=array[srcStart+i];
    }
    
    
    
    • The essence of the bearing structure of the UI is a tree structure, and the measurement layout rendering is a deep traversal of the UI tree.

2.1. Group creation and reorganization logic

2.1.1. Group Creation

  • Group is created according to startRestartGroup and endRestartGroup methods, and finally created in Slot Table
  • The created Group is used to manage the dynamic processing of the UI (that is, the movement and insertion of the data structure perspective )
    • The created Group will let the compiler know which developer's code will change which UI structure

2.1.2. Reorganization

Compose Runtime will determine the composable functions that need to be re-executed according to the scope of data influence . This step is called recombination

  • No matter how large the Composable structure is, the Composable function can be recalled and reorganized at any time

  • According to its characteristics, when a part of it changes, the Composable function does not recalculate the entire hierarchy

  • Composer can determine the specific call based on whether the UI is being modified

    • Data update causes partial UI refresh

    • Scenes Composer code processing illustrate
      Data update causes partial UI refresh 1. Composer.skipToGroupEnd()
      Jump directly to the end of the current Group The non-refresh part is not recreated, but skips repainting and directly accesses
      2. Composer.endRestartGroup() returns ScopeUpdateScope type object
      The Lambda that invokes the current composable function is finally passed in Compose Runtime can determine the call scope of composable functions based on the current environment .

    PS:Composer's operation on the Slot Table is separated from reading and writing , and all written content will be updated to the Slot Table only after the writing operation is completed.

The above only explains the reorganized upper-level code call, in fact, internally depends on State data state management , Positional Memoization


3. State

  • Compose is a declarative framework , and State adopts the observer mode to realize the automatic update of the interface with the data

Simple function implementation including state management:

@Composable
fun Content() {
    var state by remember { mutableStateOf(1) }
    Column {
        Button(onClick = { state++ }) { Text(text = "click to change state")}
        Text("state value: $state")
    }
}

3.1. remember()

remember() is a Composable function, and its internal implementation is similar to a delegate, which realizes the object memory in the Composable function call chain .

  • When the position of the call chain remains unchanged, the Composable function remember()can be to obtain the memory content of the last call .
  • The same Composable function is called at different locations, and the content obtained by its remember() function is also different.

If the same Composable function is called multiple times, multiple instances will be generated . Each call has its own lifecycle


3.2. mutableStateOf

  • mutableStateOf is actually the real internal implementation of SnapshotMutableStateImpl
fun <T> mutableStateOf(
    value: T,
    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = SnapshotMutableStateImpl(value, policy)

  • In addition to the incoming value, there is also Policy (processing strategy)
    • The processing strategy is used to control the data passed in by mutableStateOf() and how to report it (observed timing). The strategy types are as follows:
      • structuralEqualityPolicy (default policy)
      • referentialEqualityPolicy (Equality Policy)
      • You can also customize the interface implementation strategy
private class SnapshotMutableStateImpl<T>(
    value: T,
    override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {
    @Suppress("UNCHECKED_CAST")
    override var value: T
        get() = next.readable(this).value
        set(value) = next.withCurrent {
            if (!policy.equivalent(it.value, value)) {
                next.writable(this) { this.value = value }
            }
        }
		//当前state状态信息
    private var next: StateStateRecord<T> = StateStateRecord(value)

    override val firstStateRecord: StateRecord
        get() = next

    override fun prependStateRecord(value: StateRecord) {
        @Suppress("UNCHECKED_CAST")
        next = value as StateStateRecord<T>
    }

    @Suppress("UNCHECKED_CAST")
    override fun mergeRecords(
        previous: StateRecord,
        current: StateRecord,
        applied: StateRecord
    ): StateRecord? {
        val previousRecord = previous as StateStateRecord<T>
        val currentRecord = current as StateStateRecord<T>
        val appliedRecord = applied as StateStateRecord<T>
        return if (policy.equivalent(currentRecord.value, appliedRecord.value))
            current
        else {
            val merged = policy.merge(
                previousRecord.value,
                currentRecord.value,
                appliedRecord.value
            )
            if (merged != null) {
                appliedRecord.create().also {
                    (it as StateStateRecord<T>).value = merged
                }
            } else {
                null
            }
        }
    }

    private class StateStateRecord<T>(myValue: T) : StateRecord() {
        override fun assign(value: StateRecord) {
            @Suppress("UNCHECKED_CAST")
            this.value = (value as StateStateRecord<T>).value
        }

        override fun create(): StateRecord = StateStateRecord(value)

        var value: T = myValue
    }
}

  • SnapshotMutableStateImpl will internally process part of the writing to Composer (data comparison, data merging, reading, writing, etc.)

    PS: The logic of processing is carried out according to the strategy introduced above

3.2.1. Data Notification

In the separate set() method of SnapshotMutableStateImpl, the observer notification is completed. The specific process is as follows:

3.2.1.1 Get Snapshots
inline fun <T : StateRecord, R> T.writable(state: StateObject, block: T.() -> R): R {
    var snapshot: Snapshot = snapshotInitializer
    return sync {
        snapshot = Snapshot.current
        this.writableRecord(state, snapshot).block()
    }.also {
        notifyWrite(snapshot, state)
    }
}

The purpose of calling block is to directly control writable in the first state record

  • Snapshot.current gets the current Snapshot. The specific scenario analysis is as follows:
    • If it is updated through an asynchronous operation, because Snapshot is a ThreadLocal, it will return the Snapshot of the current execution thread
    • If the Snapshot of the current execution thread is empty, it returns GlobalSnapshot by default
    • If the mutableState is directly updated in Composable, the Snapshot of the current Composable execution thread is MutableSnapshot
3.2.1.2. Control write|Save to modified
  • After getting the snapshot, write it
internal fun <T : StateRecord> T.writableRecord(state: StateObject, snapshot: Snapshot): T {
    ........
    snapshot.recordModified(state)
    return newData
}

  • Finally, write through recordModified
override fun recordModified(state: StateObject) {
    (modified ?: HashSet<StateObject>().also { modified = it }).add(state)
}

  • Add the currently modified state to the modified of the current Snapshot
3.2.1.3. Observer notifications
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
    snapshot.writeObserver?.invoke(state)
}

  • Finally call the notifyWrite method to complete the notification of the observer
3.2.1.4. Differences between Kotlin functions
function structure object inside the function return value Is it an extension function Scenes
let fun <T, R> T.let(block: (T) -> R): R = block(this) it==current object Closure yes not null
with fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block() this==current object Closure no Calling multiple methods of the same class
You can omit the class name and call the method of the class directly
run fun <T, R> T.run(block: T.() -> R): R = block() this==current object Closure yes Any scenario of let,with function
apply fun T.apply(block: T.() -> Unit): T { block(); return this } this==current object this yes In any scenario of the run function, while initializing the instance, directly manipulate the attributes and return
The View of dynamic inflate XML binds data at the same time
Multiple extension function chain calls
Data model multi-level package judgment empty processing problem
also fun T.also(block: (T) -> Unit): T { block(this); return this } it==current object this yes Applicable to any scenario of the let function, generally can be used for multiple extension function chain calls

3.2.2. Observer Registration

  • First, the GlobalSnapshotManager.ensureStarted() method is called in the outermost setContent() method
internal fun ViewGroup.setContent(
    parent: CompositionContext,
    content: @Composable () -> Unit
): Composition {
    GlobalSnapshotManager.ensureStarted()
    ....
}

  • ensureStarted internally registers a global globalWriteObserver
fun ensureStarted() {
    if (started.compareAndSet(false, true)) {
        removeWriteObserver = Snapshot.registerGlobalWriteObserver(globalWriteObserver)
    }
}

started is AtomicBoolean , and its essence is to use the CPU's CAS instruction to ensure atomicity . Since it is a CPU-level instruction, its overhead is smaller than that of locks that require the participation of the operating system.

  • Next, let's look at the implementation of globalWriteObserver
private val globalWriteObserver: (Any) -> Unit = {
    if (!commitPending) {
        commitPending = true
        schedule {
            commitPending = false
            Snapshot.sendApplyNotifications()
        }
    }
}

  • Compose actually ignores multiple schedules, internally uses CallBackList as a monitor lock , and finally executes its invoke (unfinished) synchronously and enters the update state.
private fun schedule(block: () -> Unit) {
    synchronized(scheduledCallbacks) {
        scheduledCallbacks.add(block)
        if (!isSynchronizeScheduled) {
            isSynchronizeScheduled = true
            scheduleScope.launch { synchronize() }
        }
    }
}
private fun synchronize() {
        synchronized(scheduledCallbacks) {
            scheduledCallbacks.forEach { it.invoke() }
            scheduledCallbacks.clear()
            isSynchronizeScheduled = false
        }
}

  • Eventually Snapshot.sendApplyNotifications() will be called
fun sendApplyNotifications() {
    val changes = sync {
        currentGlobalSnapshot.modified?.isNotEmpty() == true
    }
    if (changes)
        advanceGlobalSnapshot()
}

The modified method of this method is also one of the cores of Compse. It is the focus of implementing extended features. The following is an analysis

  • When modified is not empty, call advanceGlobalSnapshot
private fun <T> advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T {
    val previousGlobalSnapshot = currentGlobalSnapshot
    val result = sync {
        takeNewGlobalSnapshot(previousGlobalSnapshot, block)
    }

    val modified = previousGlobalSnapshot.modified
    if (modified != null) {
        val observers = sync { applyObservers.toList() }
        for (observer in observers) {
            observer(modified, previousGlobalSnapshot)
        }
    }
		.....
    return result
}

This method will notify the observer of the value of the state modification, and finally the notifyWrite method above will notify Compose to complete the UI drive

Next, we analyze the specific callbacks


3.2.3. Reorganization

The source code here is very long, so I will make a summary directly on the picture

When events regroup.png

  • Whenever the state is updated, a reorganization occurs. Composable functions must be explicitly informed of the new state in order to update accordingly.
  • The applyObservers proposed above actually contains two observers. The conclusions are as follows:
    • SnapshotStateObserver.applyObserver: used to update Snapshot
    • recompositionRunner: handles the recomposition process

3.3.4. Drawing

  • At this point Compose has completed the creation of the View tree and contains the data it carries , but Compose's rendering is independent of the creation
  1. Composable functions do not necessarily only run on the main thread
  2. Reorganization is an optimistic operation. If the data-driven event is completed before the reorganization is completed , the reorganization will be canceled , so the result should be idempotent when writing .

3.4.5. Rendering

  • Rendering is ultimately to call the ReusableComposeNode() method to create a LayoutNode as a View node

LayoutNode is actually a bit similar to Flutter's Element, which together form the View tree

  • AndroidComposeView is the underlying dependency of Compose, which has CanvasHolder inside

    • CanvasHolder delegates android.graphics.Canvas to androidx.compose.ui.graphics.Canvas, and finally passes it to LayoutNode for various drawing

    The conclusion here is only a summary conclusion. There are still many differences in the implementation of many specific elements, but the essence is Canvas proxy

    Another point is that Compose is independent of the platform [cross-platform], in fact, it is also for higher platform compatibility

4. Intrinsic characteristic measurement

  • Different from the multiple measurements of Android's traditional UI system, Compose only measures once
  • If you need to rely on the measurement information of the sub-view, you can obtain the intrinsic characteristic measurement information of the sub-view through the measurement of the inherent characteristics , and then perform the actual measurement
    • (min|max) IntrinsicWidth: The minimum/maximum width of a given View
    • (min|max)IntrinsicHeight: The minimum/maximum height of a given View
  • Android traditional UI system measurement time complexity: O(2n) n=View hierarchy depth 2=The number of times the parent View measures the child View
    • The View level increases and the number of measurements doubles
  • Intrinsic feature measurement can pre-acquire the width and height information of each child View before the parent View measurement to calculate its own width and height
    • In some scenarios, you do not want the parent View to participate in the calculation, but you want the sub-Views to directly affect each other through the order of measurement. You can use SubcomposeLayout to deal with scenarios where there is a dependency relationship between sub-Views

5. Summary

5.1. Problems

  1. Dynamically display UI: If the UI is constantly changing during the execution process, it needs to be continuously verified and its dependencies must be ensured. It is also necessary to ensure the satisfaction of dependencies in the life cycle.
  2. Tight coupling: One piece of code affects multiple places, and the high probability is implicit, which seems to be unrelated, but actually has an impact
  3. Imperative UI: When writing UI code, always consider how to transition to the corresponding state
  4. Single Inheritance: Are there other ways to break through the limitations of single inheritance?
  5. Code bloat: How to control code bloat as the business continues to expand?

5.2. Separation of Concerns

  • Think about separation of concerns (SOC) from cohesion and coupling
    • Coupling: the dependencies between elements in different modules, and the mutual influence between elements
      • Tight coupling: One piece of code affects multiple places, and the high probability is implicit, which seems to be unrelated, but actually has an impact
    • Cohesion: the relationship between elements in a module, and the reasonableness of the combination of elements in a module
      • Combine related codes together as much as possible, and expand the code as the code size grows

5.3. Composable functions

  • Composable functions can be transformation functions for data. Any Kotlin code can be used to take this data and use it to describe the hierarchy
  • When calling other Composable functions, these calls represent the UI in our hierarchy. And you can use language-level primitives in Kotlin to dynamically perform various operations.
    • Example: If statements and for loops can be used to implement control flow and handle UI logic.
  • Use Kotlin's trailing lambda syntax to realize the Composable function of the Composable lambda parameter, that is, the Composable function pointer

5.4. Declarative UI

  • When writing code, the UI is directly described according to the idea, rather than how to transition to the corresponding state.
  • You don't need to pay attention to what state the UI was in before, but only need to specify the state it should be in now.
  • Compose controls how to get from one state to another, so there is no need to think about state transitions anymore.

5.5. Packaging

  • Composable functions can manage and create state, and then pass that state and any data it receives as arguments to other Composable functions.

5.6. Reorganization

  • No matter how large the Composable structure is, the Composable function can be recalled and reorganized at any time
  • According to its characteristics, when a part of it changes, the Composable function does not recalculate the entire hierarchy

5.7. observeAsState

  • The observeAsState method will map LiveData to State, and use its value in the Scope of the function body.
  • The State instance subscribes to the LiveData instance, and the State will be updated wherever the LiveData changes.
  • No matter where the State instance is read, or the included code, or the read Composable function will automatically subscribe to these changes.
  • No longer need to specify LifecycleOwner or update callback, Composable can implicitly implement the functions of both.

5.8. Composition

  • Different from traditional inheritance, composition can combine a series of simple codes into a piece of complex code logic, breaking through the limitation of single inheritance.

    • Use Kotlin's trailing lambda syntax to realize the Composable function of the Composable lambda parameter, that is, the Composable function pointer

at last

Now it is said that the Internet is in a cold winter. In fact, it is nothing more than that you get on the wrong car and wear less (skills). It's just a business curd that eliminates the end! Nowadays, there are a lot of junior programmers in the market. This set of tutorials is aimed at Android development engineers who have been in the bottleneck period for 1-6 years. Those who want to break through their own salary increase after the next year, advanced Android intermediate and senior architects are especially helpful for you. Feel like a duck to water, hurry up and get it!

Scan the QR code below to join us

The reason why some people are always better than you is because he is very good himself and has been working hard to become better, but are you still satisfied with the status quo and secretly happy in your heart!

The road to Android architects is very long, let's encourage each other!

Guess you like

Origin blog.csdn.net/m0_56255097/article/details/130227225