1. @Composable
A function with the @Composable annotation changes the type of the function, depending internally on the Composer throughout the function scope. @Composable features are as follows:
-
Composable is not an annotation handler per se. Compose relies on the Kotlin compiler plug-in during the Kotlin compiler’s type detection and code generation phases, so you can use it without an annotation handler.
-
@Composable causes its type to change, and the same function type that is not annotated is incompatible with the annotated type
@Composable assists the Kotlin compiler to know that this function is used to convert data into a UI that describes the display state of the screen
-
@Composable is not a language feature and cannot be implemented as a language keyword
Next, we analyze the internal implementation in terms of the simplest function
The kotlin code is as follows:
@Composable
fun HelloWord(text: String) {
Text(text = text)
}
Copy the code
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)); }}Copy the code
There are a lot of conceptual points inside the function, so let’s analyze them one by one.
2. Composer
Compose is actually the context throughout the Composable function scope, providing methods for creating internal groups and so on. The internal data structure is Slot Table, which has the following characteristics:
-
Slot Table is a type that stores data in continuous space, with an array implementation at the bottom. But the difference is the remaining space, called Gap.
- Gap has the ability to move to any area, so it is more efficient during data insertion and deletion.
-
Slot tables are linear data structures by nature, so they support storing View trees in Slot tables.
- According to the Slot Table can move the insertion point, so that the View tree after the change does not need to recreate the data structure of the whole View tree.
-
While Slot tables have the ability to insert data anywhere compared to ordinary arrays, Gap movement is still inefficient.
- Google determines that in most cases the interface updates are data changes and that the View tree structure doesn’t change very often.
- And basically, only arrays, which are memory contiguous data structures, can meet the requirements of Compose Runtime in terms of access efficiency
Ps: All Composeable functions rely internally on Composer, 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 try to deduce several results by backward inference:
-
If you use a linked list, the insertion time is o(1), but the lookup time is o(n),
-
If an array is used, the lookup time is o(1) and the insertion time is o(n) degrees.
There are other solutions besides gap Buffer:
- Block linked lists (insertion and lookup can be done at O (n^1/2) complexity)
- Rope Tree (a balanced search tree)
- Piece Table (an improved version of the Gap Buffer used by Microsoft Doc, which also allows for fast undo and recombination)
-
The logical code is as follows:
int GAP_SIZE = 5;// Default gap size // Basic structure struct GapBuffer{ char array[size]; int gapStart; int gapSize; int len; } gapBuffer; // Insert the logical implementation void insert(char c){ if(gapBuffer.gapSize<0) expanison(); gapBuffer.array[++gapBuffer.gapStart]=c; --gapBuffer.gapSize; ++len; } // Logical expansion implementation void expanison(a){ // Double the capacity GAP_SIZE = GAP_SZIE*2; gapBuffer.gapSize = GAP_SIZE; // Copy back the second half of the data, giving GAP_SiZE space arraycopy(gapBuffer.array,gapBuffer.gapStart,gapBuffer.gapStart+gapBuffer.gapSize,len-gapBuffer.start); } // Move the gap logic implementation // The buffer will not be expanded void moveGap(int pos){ // Do not move the same position if(gapBuffer.gapStart == pos)return; // If pos is less than the current gap if(pos<gapBuffer.gapStart) / / copy the array arraycopy(gapBuffer.array,pos,pos+gapBuffer.gapSize,gapBuffer.gapStart-pos); else / / copy the array arraycopy(gapBuffer.array,gapBuffer.gapStart+gapBuffer.gapSize,gapBuffer.gapStart,gapBuffer.gapSize); } // Array copy logic implementation void arraycopy(char array[].int srcSatrt,int dstStart,int len){ for(int i = 0; i<len; ++i)array[dstStart+i]=array[srcStart+i]; } Copy the code
- The load-bearing structure of THE UI is essentially a tree structure, and measurement, layout and rendering are all depth traversal of the UI tree.
2.1. Group creation and reorganization logic
2.1.1. The Group created
- Groups are created according to the startRestartGroup and endRestartGroup methods and are finally created in the Slot Table
- The Group is created for managementDynamic processing of UI(i.e.Data structure perspectivetheMove and insert)
- The created Group lets the compiler know which developers’ code will change which UI structure
2.1.2. Restructuring
The Compose Runtime determines which combinable functions need to be reexecuted based on the data impact scope, a step called regrouping
-
A Composable function can be recalled at any time, no matter how large the Composable structure is
-
By its nature, the Composable function does not recalculate the entire hierarchy when one part of it changes
-
Composer can determine specific calls based on whether the UI is being modified
-
Data updates cause part of the UI to refresh
-
scenario Composer code processing instructions Data updates cause part of the UI to refresh 1. Composer.skipToGroupEnd()
Jump directly to the end of the current GroupThe non-refresh part is not recreated, butSkip redrawing, direct access 2.composer.endrestartGroup () returns an object of type ScopeUpdateScope
Finally, a Lambda is passed that calls the currently composable functionCompose Runtime determines which functions can be composed based on the current environmentCall the range.
PS:Composer performs separate read and write operations on the Slot Table. All written information is updated to the Slot Table only after the write operation is complete
-
The above only explains the reorganization of the upper layer of code calls, in fact, internal is dependent on State data State management, Positional Memoization
3. State
- Compose is a declarative framework, and State uses the observer pattern to automatically update the interface with data
Simple function implementation that includes state management:
@Composable
fun Content(a) {
var state by remember { mutableStateOf(1) }
Column {
Button(onClick = { state++ }) { Text(text = "click to change state")}
Text("state value: $state")}}Copy the code
3.1. Remember ()
Remeber () is a Composable function with an internal implementation similar to a delegate that implements object memory in the Composable function call chain.
- The Composable function is called without changing the location of the call chain
remember()
availableThe last callWhen the content of memory. - The same Composable function is called in different places, and its remember() function gets different content.
The same Composable function is called multiple times, resulting in multiple instances. Each call has its own lifecycle
3.2. mutableStateOf
- The real internal implementation of mutableStateOf is SnapshotMutableStateImpl
fun <T> mutableStateOf(
value: T,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = SnapshotMutableStateImpl(value, policy)
Copy the code
- In addition to the value that’s passed in, there’s the Policy
- Processing policies are used to control the incoming data of mutableStateOf() and howTo report(observed timing), the strategy types are as follows:
- structuralEqualityPolicy
- ReferentialEqualityPolicy equality (strategy)
- You can also customize the interface to implement the policy
- Processing policies are used to control the incoming data of mutableStateOf() and howTo report(observed timing), the strategy types are as follows:
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 }
}
}
// Current state Indicates the 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(a): StateRecord = StateStateRecord(value)
var value: T = myValue
}
}
Copy the code
-
SnapshotMutableStateImpl internally performs some processing of writing to Composer (data comparison, data merge, read, write, etc.)
PS: The logic for processing is based on the strategy described above
3.2.1. Data notification
In a separate set() method for SnapshotMutableStateImpl, observer notification is completed for it. The specific process is as follows:
3.2.1.1 get Snapshot
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)
}
}
Copy the code
Block is called to directly control writability in the first state record
- Snapshot.current Obtains the current Snapshot. The scenario is described as follows:
- If updated asynchronously, the Snapshot is a ThreadLocal, so the Snapshot of the current executing thread is returned
- If the Snapshot of the current thread is empty, GlobalSnapshot is returned by default
- If you update the mutableState directly in the Composable, the Snapshot of the current executing thread in the Composable is a MutableSnapshot
3.2.1.2. Control write | save to Modified
- Once the snapshot is obtained, write to it
internal fun <T : StateRecord> T.writableRecord(state: StateObject, snapshot: Snapshot): T {
........
snapshot.recordModified(state)
return newData
}
Copy the code
- Finally, recordModified is used to implement writing
override fun recordModified(state: StateObject){ (modified ? : HashSet<StateObject>().also { modified = it }).add(state) }Copy the code
- Add the current modified state to modified of the current Snapshot
3.2.1.3. Observer notification
internal fun notifyWrite(snapshot: Snapshot, state: StateObject){ snapshot.writeObserver? .invoke(state) }Copy the code
- Finally, the notifyWrite method is called to complete the notification of the observer
3.2.1.4. Differences between Kotlin functions
function | The structure of the body | Function object | The return value | Extension function or not | scenario |
---|---|---|---|---|---|
let | fun <T, R> T.let(block: (T) -> R): R = block(this) | It == Current object | closure | is | Not null |
with | fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block() | This == current object | closure | no | Call multiple methods of the same class You can simply call the methods of the class without the class name |
run | fun <T, R> T.run(block: T.() -> R): R = block() | This == current object | closure | is | Any scenario of the let,with function |
apply | fun T.apply(block: T.() -> Unit): T { block(); return this } | This == current object | this | is | Any scenario in which the run function, while initializing the instance, directly operates on the property and returns Views with the dynamic Inflate XML bind data at the same time Multiple extension function chained calls The problem of data model multi – level package void processing |
also | fun T.also(block: (T) -> Unit): T { block(this); return this } | It == Current object | this | is | Applies to any scenario of a let function, and can generally be used for chained calls to multiple extension functions |
3.2.2. Observer registration
- In the first place in the external setContent () method invocation of the GlobalSnapshotManager. EnsureStarted () method
internal fun ViewGroup.setContent(
parent: CompositionContext,
content: @Composable() - >Unit
): Composition {
GlobalSnapshotManager.ensureStarted()
....
}
Copy the code
- EnsureStarted internally registered a global globalWriteObserver
fun ensureStarted(a) {
if (started.compareAndSet(false.true)) {
removeWriteObserver = Snapshot.registerGlobalWriteObserver(globalWriteObserver)
}
}
Copy the code
Started is AtomicBoolean, which essentially uses the CPU’s CAS instruction to ensure atomicity. Because it is cpu-level instruction, it is less expensive than locking that requires the operating system to participate.
- Next let’s look at the Implementation of globalWriteObserver
private val globalWriteObserver: (Any) -> Unit = {
if(! commitPending) { commitPending =true
schedule {
commitPending = false
Snapshot.sendApplyNotifications()
}
}
}
Copy the code
- Compose ignores multiple schedules, internally uses the CallBackList as a monitor lock, and eventually synchronously executes its invoke (unfinished) 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(a) { synchronized(scheduledCallbacks) { scheduledCallbacks.forEach { it.invoke() } scheduledCallbacks.clear() isSynchronizeScheduled = false}}Copy the code
- Will call the Snapshot. SendApplyNotifications ()
fun sendApplyNotifications(a) {
valchanges = sync { currentGlobalSnapshot.modified? .isNotEmpty() ==true
}
if (changes)
advanceGlobalSnapshot()
}
Copy the code
The modified version of this method is also at the heart of Compse and is the focus of implementing the extension feature, which is discussed below
- When Modified is not empty, advanceGlobalSnapshot is called
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
}
Copy the code
This method notifies the observer of the value of the state change, and the notifyWrite method above notifies the Compose, completing the UI driver
Next, let’s look at which callbacks are available
Reorganization of 3.2.3.
The source code is quite large here, so I’ll just summarize it from the figure above
- Reorganizations occur every time a status update occurs. The Composable function must be explicitly informed of the new state before it can update accordingly.
- ApplyObservers, as proposed above, actually includes two observers, and the conclusion is as follows:
- SnapshotStateObserver. ApplyObserver: used to update the Snapshot
- RecompositionRunner: Handles the reorganization process
Drawing 3.3.4.
- At this point, Compose completes the creation of the View tree and contains the hosted data, but the rendering of Compose is independent of the creation
- The Composable function does not have to run only on the main thread
- Regrouping is an optimistic operation. If the data-driven event is completed before the regrouping is complete, the regrouping will be cancelled, so the result should be written in such a way that it is idempotent.
Render 3.4.5.
- Rendering ends up calling the ReusableComposeNode() method to create the LayoutNode as the View node
The LayoutNode is a bit like the elements of the Flutter, which together make up the View tree
-
AndroidComposeView is the underlying dependency of Compose, which has a CanvasHolder inside it
- CanvasHolder android. Graphics. Canvas agent into androidx.com pose. The UI. Graphics. Canvas, eventually to LayoutNode used in a variety of drawing
The conclusion here is only a summary conclusion. There are many differences in the implementation of many specific elements, but their essence is Canvas proxy
The other thing is that Compose is platform-independent, which is for greater platform compatibility
4. Measurement of inherent characteristics
- Instead of measuring multiple times in Android’s traditional UI system, Compose measures only once
- If you need to rely on the subview measurement information, you can passMeasurement of natural propertiesObtain the inherent characteristics of the subview measurement information, and then the actual measurement
- (min | Max) IntrinsicWidth: given the minimum/maximum width of the 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= parent View to child View measurement times
- The View level is increased, and the measurement times are doubled
- Intrinsic characteristic measurement can obtain the width and height information of each child View in advance before the parent View measurement, so as to calculate its own width and height
- In some scenarios where the parent View is not required to participate in the calculation, but the child View directly influences each other through the measurement sequence, SubcomposeLayout can be used to deal with the scenario where there is a dependency relationship between the child View
5. To summarize
Problem of 5.1.
- Dynamic display of the UI: Changing the UI during execution requires constant validation and ensuring its dependencies. Also ensure dependency satisfaction throughout the lifecycle.
- Tight coupling: Code in one place affects many places, and in most cases it is implicit, seemingly unrelated, but actually has an impact
- Imperative UI: When writing UI code, always think about how to transition to the corresponding state
- Single inheritance: Are there other ways to break through the limitations of single inheritance?
- Code bloat: How can you control code bloat as your business expands?
5.2. Separation of concerns
- Thinking about Separation of concerns (SOC) in terms of cohesion and coupling
- Coupling: Dependencies between elements in different modules that affect each other
- Tight coupling: Code in one place affects many places, and in most cases it is implicit, seemingly unrelated, but actually has an impact
- Cohesion: The relationship between the elements of a module, the reasonable degree to which the elements of a module are combined with each other
- Combine related code together as much as possible, and expand the code as it grows
- Coupling: Dependencies between elements in different modules that affect each other
5.3. Composable functions
- The Composable function can be a conversion function for the data. You can use any Kotlin code to take this data and use it to describe the hierarchy
- When other Composable functions are called, these calls represent the UI in our hierarchy. And you can use language-level primitives in Kotlin to dynamically perform various operations.
- For example, you can use if statements and for loops to implement control flow to handle UI logic.
- Make use of Kotlin’s trailing lambda syntax to implement the Composable function of the Composable lambda parameter, that is, the Composable function pointer
5.4. Declarative UI
- Write code that describes the UI as it is, not how to transition to the corresponding state.
- You don’t care what state the UI was in before, you just specify what state it should be in now.
- Compose controls how you go from one state to another, so you don’t have to worry about state transitions anymore.
5.5. Encapsulation
- The Composable function manages and creates the state, and then passes that state and any data it receives as parameters to the other Composable functions.
Reorganization of 5.6.
- A Composable function can be recalled at any time, no matter how large the Composable structure is
- By its nature, the Composable function does not recalculate the entire hierarchy when one part of it changes
5.7. observeAsState
- The observeAsState method maps LiveData to State and uses its value in the Scope of the function body.
- The State instance subscribes to the LiveData instance, and the State is updated wherever the LiveData changes.
- These changes are automatically subscribed to wherever the State instance is read, the already included code, or the Composable function that has been read.
- Instead of specifying LifecycleOwner or update callbacks, the Composable can implement both implicitly.
Combination of 5.8.
-
Different from traditional inheritance, combination can combine a series of simple code into a complex code logic, breaking through the limitation of single inheritance.
- Make use of Kotlin’s trailing lambda syntax to implement the Composable function of the Composable lambda parameter, that is, the Composable function pointer