当前位置:网站首页>How to optimize the performance of compose? Find the answer through "underlying principles" | developers say · dtalk
How to optimize the performance of compose? Find the answer through "underlying principles" | developers say · dtalk
2022-06-11 16:03:00 【Android Developer】

The original author of this article : Zhu Tao , original text Hair Cloth in : Zhu Tao's study room
This year's Google I/O At the conference ,Android The official target is Jetpack Compose A series of performance optimization suggestions are given , Documents and videos have been released . in general , The official content is very good , After reading it, I still have some unfinished business , I recommend you to have a look .
however , If you are right about Compose Of Underlying principle Not particularly familiar words , that , After watching Android Official document 、 After the video , Your mind may still be full of questions , understand . After all , The official response to 「Compose performance optimization 」 The definition of a topic is 「Intermediate Middle stage 」. For a new technology that has just been launched less than a year ,「 Middle stage 」 It is already a relatively high requirement .

Of course , Don't worry , The purpose of my writing this article , Is to make more Compose Developers can understand the official Compose Performance optimization suggestions .
let me put it another way , We just need to make up 「 Billion points 」Compose Of 「 Underlying principle 」 that will do .


Composable The essence of
We all know ,Jetpack Compose The most magical place is : It can be used Kotlin Write UI Interface ( There is no need to XML). and , With the help of Kotlin The properties of higher order functions of ,Compose UI The interface is also very intuitive .
// Code segment 1
@Composable
fun Greeting() { // 1
Column { // 2
Text(text = "Hello")
Text(text = "Jetpack Compose!")
}
}The above code , Even if you don't have any Compose Basics , It should also be easy to understand .Column amount to Android Middle vertical linear layout LinearLayout, In this layout , We put two Text Control .
The final UI Interface display , As shown in the figure below .

Although the example is simple , But in the code above , There are still two details that need our attention , I've marked it with comments :
notes 1: Greeting() It's a Kotlin Function of , If you put aside its @Composable If we don't talk about notes , that , Its function type should be () -> Unit. however , because @Composable Is a very special annotation ,Compose The compiler plug-in for will see it as an impact Function type One of the factors . therefore ,Greeting() Its function type should be @Composable () -> Unit.( By the way , The other two common function type influence factors are : suspend、 Receiver of function type .)
notes 2: Column {}, Please pay attention to its {}, The reason why we can write code like this , This is actually Kotlin Higher order functions provided Abbreviation . Its complete writing should be like this :
// Code segment 2
Column(content = {
log(2)
Text(text = "Hello")
log(3)
Text(text = "Jetpack Compose!")
})thus it can be seen ,Compose The grammar of , It's really just through Kotlin The higher-order function of .Column()、Text() Looks like it's calling UI Constructor for control , But it's actually just a normal top-level function , So , It's just a kind of DSL Of " Smoke screen " nothing more .
that , Come here , We can actually make a phased summary : Composable The essence of , Is the function . This conclusion seems simple , But it can lay the foundation for the later principle research .
Next , Let's talk about Composable The characteristics of .

Composable The characteristics of
As we have said before ,Composable It's essentially a function . that , Its characteristics , In fact, it is very close to ordinary functions . This remark looks like nonsense , Let me give you an example .
Based on the previous code , Let's add some log:
// Code segment 3
@Composable
fun Greeting() {
log(1)
Column {
log(2)
Text(text = "Hello")
log(3)
Text(text = "Jetpack Compose!")
}
log(4)
}
private fun log(any: Any) {
Log.d("MainActivity", any.toString())
}Excuse me, , What is the output of the above code ? If you have seen my tutorial on collaboration , Then I must feel a little " virtual ", Right ? however , The output of the above code is very intuitive .
// Output results
// Be careful : At present Compose Version is 1.2.0-beta
// In future versions ,Compose The bottom layer is possible to optimize , And change this behavior pattern .
com.boycoder.testcompose D/MainActivity: 1
com.boycoder.testcompose D/MainActivity: 2
com.boycoder.testcompose D/MainActivity: 3
com.boycoder.testcompose D/MainActivity: 4Look at it. ,Composable It is not only a common function from the perspective of source code , Its behavior pattern at runtime , It is similar to ordinary functions . We wrote it out Composable function , They are nested in each other , Eventually a tree structure will be formed , To be exact, it's a N Fork tree . and Composable The order in which functions are executed , It's actually the right one N Fork tree DFS Traverse .
thus , We wrote it out Compose UI It's almost : " What you see is what you get ".

Maybe , You will feel , The above example , It's nothing , After all ,XML You can do something similar . that , Let's look at another example .
// Code segment 4
@Composable
fun Greeting() {
log("start")
Column {
repeat(4) {
log("repeat $it")
Text(text = "Hello $it")
}
}
log("end")
}
// Output results :
com.boycoder.testcompose D/MainActivity: start
com.boycoder.testcompose D/MainActivity: repeat 0
com.boycoder.testcompose D/MainActivity: repeat 1
com.boycoder.testcompose D/MainActivity: repeat 2
com.boycoder.testcompose D/MainActivity: repeat 3
com.boycoder.testcompose D/MainActivity: endWe use repeat{} Repeated calls to 4 Time Text(), We have successfully created on the screen 4 individual Text Control , The key is , They can also be used in Column{} Normal vertical arrangement in the middle . Such a code pattern , In the past XML Times are unimaginable .

Come back , Precisely because Composable The essence of is a function , It has some of the characteristics of ordinary functions , thus , It also allows us to write just like normal code , Use logical statements to describe UI Layout .
Okay , Now we know Composable The essence of is function , But , The ones on our mobile screen UI How did the control appear ? Next , We need to learn again 「 A little bit 」Compose Knowledge of compiler plug-ins .PS: This time , I promise it's really 「 A little bit 」.

Compose Compiler plug-ins
although Compose Compiler Plugin It looks like a very tall thing , But from a macro perspective , What it does is simple .
It's co-operative suspend keyword , It can change the type of function ,Compose Annotations @Composable It's the same thing . in general , The corresponding relationship between them is as follows :

say concretely , We are Kotlin It says Composable function 、 Suspend function , After compiler conversion , Will be injected with additional parameters . For suspended functions , There will be one more parameter list Continuation Parameters of type ; about Composable function , There will be one more parameter list Composer Parameters of type .
Why can't ordinary functions call 「 Suspend function 」 and 「Composable function 」, The underlying reason is : Ordinary functions cannot be passed in at all Continuation、Composer As a parameter of the call .
Be careful : What needs special explanation is , In many scenarios ,Composable The function passes through Compose Compiler Plugin After the transformation , In fact, other parameters may be added . A more complex situation , We'll leave it to the following articles for further analysis .
in addition , because Compose It doesn't belong to Kotlin The category of , In order to achieve Composable Function conversion ,Compose The team is through 「Kotlin Compiler plug-ins 」 In the form of . We wrote Kotlin The code will first be converted to IR, and Compose Compiler Plugin At this stage, it directly changed its structure , This changes the final output Java Bytecode and Dex. This process , That is, the behavior described in the action chart I put at the beginning of the article .
I won't paste the moving pictures again , The following is a static flow chart .

however ,Compose Compiler Not just change 「 Function signature 」 So simple , If you will Composable Function decompiles to Java Code , You will find that its function body will also change .
Let's take a concrete example , To explore Compose Of 「 restructuring 」(Recompose) Implementation principle of .

Recompose Principle
// Code segment 5
class MainActivity : ComponentActivity() {
// Omit
@Composable
fun Greeting(msg: String) {
Text(text = "Hello $msg!")
}
}The code above is very simple ,Greeting() The logic of is very simple , But when it is decompiled into Java after , Its actual logic will become much more complicated .
// Code segment 6
public static final void Greeting(final String msg, Composer $composer,
final int $changed) { // Extra changed Let's analyze later
// 1, Start
// ↓
$composer = $composer.startRestartGroup(-1948405856);
int $dirty = $changed;
if (($changed & 14) == 0) {
$dirty = $changed | ($composer.changed(msg) ? 4 : 2);
}
if (($dirty & 11) == 2 && $composer.getSkipping()) {
$composer.skipToGroupEnd();
} else {
TextKt.Text-fLXpl1I(msg, $composer, 0, 0, 65534);
}
// 2, end
// ↓
ScopeUpdateScope var10000 = $composer.endRestartGroup();
if (var10000 != null) {
var10000.updateScope((Function2)(new Function2() {
public final void invoke(@Nullable Composer $composer, int $force) {
// 3, Recursively call itself
// ↓
MainActivityKt.Greeting(msg, $composer, $changed | 1);
}
}));
}
}without doubt ,Greeting() After decompiling , The reason why it has become so complicated , The reasons behind it are all because Compose Compiler Plugin. There are too many details worth digging into in the above code , In order not to deviate from the theme , For the time being, we will only focus on 3 A note , Let's see one by one .
notes 1: composer.startRestartGroup, This is a Compose The compiler plug-in is Composable An auxiliary code for function insertion . Its function is to create a repeatable in memory Group, It often represents a Composable The function begins to execute ; meanwhile , It also creates a corresponding ScopeUpdateScope, And this ScopeUpdateScope It will be in the comments 2 Used at .
notes 2: composer.endRestartGroup(), It often represents a Composable End of function execution . And this Group, To a certain extent , It also describes UI The structure and hierarchy of . in addition , It also returns a ScopeUpdateScope, And it triggers 「Recompose」 The key to . For the specific logic, let's look at the notes 3.
notes 3: We went to ScopeUpdateScope.updateScope() Registered a listener , When our Greeting() When a function needs to be reorganized , This monitoring will be triggered , So as to recursively call itself . At this time you will find , aforementioned RestartGroup It also implies 「 restructuring 」 The mean of .
thus it can be seen ,Compose Among them, it looks very tall 「Recomposition」, In fact, that is : " Call the function again " nothing more .
that ,Greeting() Under what circumstances will it trigger 「 restructuring 」 Well ? Let's take a more complete example .
// Code segment 7
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MainScreen()
}
}
}
@Composable
fun MainScreen() {
log("MainScreen start")
val state = remember { mutableStateOf("Init") }
// 1
LaunchedEffect(key1 = Unit) {
delay(1000L)
state.value = "Modified"
}
Greeting(state.value)
log("MainScreen end")
}
private fun log(any: Any) {
Log.d("MainActivity", any.toString())
}
@Composable
fun Greeting(msg: String) {
log("Greeting start $msg")
Text(text = "Hello $msg!")
log("Greeting end $msg")
}
/* Output results
MainActivity: MainScreen start
MainActivity: Greeting start Init
MainActivity: Greeting end Init
MainActivity: MainScreen end
wait for 1 second
MainActivity: MainScreen start // restructuring
MainActivity: Greeting start Modified // restructuring
MainActivity: Greeting end Modified // restructuring
MainActivity: MainScreen end // restructuring
*/The above code logic is still very simple ,setContent {} Called MainScreen();MainScreen() Called Greeting(). The only thing that needs attention , It's a comment 1 Situated LaunchedEffect{}, Its function is to start a process , Delay 1 second , Also on state Assign a value .
Output from the code log , We can see , front 4 Log output , yes Compose Triggered by initial execution ; Back 4 Log output , It is from state Change leads to 「 restructuring 」. look ,Compose By some mechanism , Caught state Change of state , And then notified MainScreen() Has been restructured .
If you are careful enough , You will find ,state In fact, only Greeting() Yes , and state Changes , Has led to MainScreen()、Greeting() It all happened 「 restructuring 」,MainScreen() Of 「 restructuring 」 It seems superfluous . In fact, there is something hidden here Compose A key point of performance optimization .
Be careful : Similar to the above ,Compose Compiler In fact, enough optimizations have been made ,MainScreen() Of 「 restructuring 」 It seems superfluous , But it doesn't really have much impact on performance , We give this example just to make it clear 「 restructuring 」 Principle , Lead to optimization ideas .Compose Compiler Specific optimization ideas , We'll leave it for later analysis .
Let's change the code above :
// Code segment 8
class MainActivity : ComponentActivity() {
// unchanged
}
@Composable
fun MainScreen() {
log("MainScreen start")
val state = remember { mutableStateOf("Init") }
LaunchedEffect(key1 = Unit) {
delay(1000L)
state.value = "Modified"
}
Greeting { state.value } // 1, The change is here
log("MainScreen end")
}
private fun log(any: Any) {
Log.d("MainActivity", any.toString())
}
@Composable // 2, The change is here ↓
fun Greeting(msgProvider: () -> String) {
log("Greeting start ${msgProvider()}") // 3, change
Text(text = "Hello ${msgProvider()}!") // 3, change
log("Greeting end ${msgProvider()}") // 3, change
}
/*
MainActivity: MainScreen start
MainActivity: Greeting start Init
MainActivity: Greeting end Init
MainActivity: MainScreen end
wait for 1 second
MainActivity: Greeting start Modified // restructuring
MainActivity: Greeting end Modified // restructuring
*/I marked the code changes with comments , The main changes are : notes 2, We put the original String Change the parameter of type to function type : () -> String. notes 1、3 There are two changes , All follow the notes 2 Of .
Note the log output of the code , This time, ,「 restructuring 」 The scope of has changed ,MainScreen() No restructuring has occurred ! Why is that ? There are two knowledge points involved here : One is Kotlin In functional programming 「Laziness」; The other is Compose Restructured 「 Scope 」. Let's see one by one .
Laziness
Laziness It's a big topic in functional programming , To make this concept clear , I have to write several articles , Here I'll briefly explain , We'll have a further discussion later .
understand Laziness The most intuitive way , Is to write a piece of code for this comparison :
// Code segment 9
fun main() {
val value = 1 + 2
val lambda: () -> Int = { 1 + 2 }
println(value)
println(lambda)
println(lambda())
}Actually , If you are right about Kotlin Higher order function 、Lambda Well understood words , You can immediately understand the code snippet 8 In the middle of Laziness What do you mean .
The output of the above code is as follows :
3
Function0<java.lang.Integer>
3This output is also well understood .1 + 2 It's an expression , When we use it {} After wrapping , To a certain extent, it achieves Laziness, We visit lambda When It does not trigger the actual calculation behavior . Only a call lambda() When , Will trigger the actual calculation behavior .
Laziness Make it clear , Let's see Compose The reorganization of 「 Scope 」.
restructuring 「 Scope 」
Actually , In the previous code snippet 6 It's about , We have already touched it , That is to say ScopeUpdateScope. Through the previous analysis , Each of us Composable function , In fact, they all correspond to one ScopeUpdateScope,Compiler The bottom layer is monitoring through injection , To achieve 「 restructuring 」 Of .
actually ,Compose The bottom layer also provides a : State snapshot system (SnapShot).Compose The underlying principle of the snapshot system is still complex , We'll have a chance to discuss it further in the future .
in general ,SnapShot Can monitor Compose among State The reading of 、 Writing behavior .
// Code segment 10
@Stable
interface MutableState<T> : State<T> {
override var value: T
}
internal open class SnapshotMutableStateImpl<T>(
value: T,
override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {
override var value: T
get() = next.readable(this).value
set(value) = next.withCurrent {
if (!policy.equivalent(it.value, value)) {
next.overwritable(this, it) { this.value = value }
}
}
}Essentially , It is actually through customization Getter、Setter To achieve . When we define state Variable , Its value is from "Init" Turn into "Modified" When ,Compose Can be customized by Setter Capture this behavior , To call ScopeUpdateScope Listening in the middle , Trigger 「 restructuring 」.
that , Code segment 7、 Code segment 8, What is the difference between them ? The key lies in ScopeUpdateScope Different .
The connection , In fact, it can be summed up in one sentence : Where does the state read take place Scope, When the status is updated , Which one? Scope A reorganization occurs .
It doesn't matter if you don't understand this sentence , I drew a picture , Describes the code snippet 7、 Code segment 8 Differences between :

For code snippets 7, When state The read of occurs at MainScreen() Of ScopeUpdateScope, that , When state When things change , It will trigger MainScreen() Of Scope Conduct 「 restructuring 」.
Code segment 8 It's the same thing :

Now? , Looking back at this sentence , I believe you can understand : Where does the state read take place Scope, When the status is updated , Which one? Scope A reorganization occurs .
good , After all the foreshadowing , We can easily understand Android Three of the official performance optimization suggestions are .
1. Defer reads as long as possible
2. Use derivedStateOf to limit recompositions
3. Avoid backwards writes
Above this 3 Piece of advice , In essence, it is to avoid 「 restructuring 」, Or shrink 「 Restructuring scope 」. Due to space limitation , Let's pick the first one to explain in detail ~

Delay as much as possible State The act of reading
Actually , For our code snippet 7、 Code segment 8 Such a change ,Compose The performance improvement is not obvious , because Compiler The bottom layer has been optimized enough , One more level of function calls , There will be no obvious difference .Android Officials even suggest that we delay the reading and writing of certain states until Layout、Draw Stage .
It's like Compose The whole execution 、 Rendering process related . in general , For one Compose Page for , It will experience the following 4 A step :
First step ,Composition, This actually represents our Composable Function execution process .
The second step ,Layout, It's not about us View Systematic Layout similar , But there are some differences in the overall distribution process .
The third step ,Draw, That is to draw ,Compose Of UI The element will eventually be drawn in Android Of Canvas On . thus it can be seen ,Jetpack Compose Although it's brand new UI frame , But its bottom layer is not separated Android The category of .
Step four ,Recomposition, restructuring , And repeat 1、2、3 step .
The overall process is shown in the figure below :

Android The official recommendation is that we delay the status reading as much as possible , In fact, I hope we can skip directly in some scenarios Recomposition The stage of 、 even to the extent that Layout The stage of , Only affect Draw.
And the means to achieve this goal , In fact, it is what we mentioned earlier 「Laziness」 thought . Let's take the official code as an example :

First , What I want to say is ,Android The comments in the official documents actually have a Small flaws Of . It's friendly to beginners , But it is easy to trouble those of us who go deep into the bottom . As described in the code above Recomposition Scope Inaccurate , It's really Recomposition Scope, It should be the whole SnackDetail(), instead of Box(). Regarding this , I am already in Twitter Related to Google The engineer gave feedback , The other party also replied to me , This is a " intentionally " Of , Because it's easier to understand .
good , Let's get back to the point , Analyze this case in detail :
// Code segment 11
@Composable
fun SnackDetail() {
// Recomposition Scope
// ...
Box(Modifier.fillMaxSize()) { Start
val scroll = rememberScrollState(0)
// ...
Title(snack, scroll.value) // 1, State read
// ...
}
// Recomposition Scope End
}
@Composable
private fun Title(snack: Snack, scroll: Int) {
// ...
val offset = with(LocalDensity.current) { scroll.toDp() }
Column(
modifier = Modifier
.offset(y = offset) // 2, State use
) {
// ...
}
}The above code has two comments , notes 1, Represents the reading of the state ; notes 2, Represents the use of states . such " The state reading is inconsistent with the use position " The phenomenon of , In fact, for Compose Provides space for performance optimization .
that , How should we optimize it ? It's very simple , With the help of our previous Laziness Thought , Give Way : " The state reading is consistent with the use position ".
// Code segment 12
@Composable
fun SnackDetail() {
// Recomposition Scope
// ...
Box(Modifier.fillMaxSize()) { Start
val scroll = rememberScrollState(0)
// ...
Title(snack) { scroll.value } // 1,Laziness
// ...
}
// Recomposition Scope End
}
@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
// ...
val offset = with(LocalDensity.current) { scrollProvider().toDp() }
Column(
modifier = Modifier
.offset(y = offset) // 2, State read + Use
) {
// ...
}
}Please pay attention. notes 1 The changes here , Because we will scroll.value Turned into Lambda, therefore , It will not be in composition State read behavior generated during , such , When scroll.value When it changes , It won't trigger 「 restructuring 」, This is it. 「Laziness」 The meaning of .
Code segment 11、 Code segment 12 The difference is huge :

The former is triggered frequently during page sliding : 「 restructuring 」+「Layout」+「Draw」, The latter is completely bypassed 「 restructuring 」, Only 「Layout」+「Draw」, thus it can be seen , Its performance improvement is also very significant .

ending
OK, Come here , This article should be over . Let's summarize briefly :
First of all ,Composable The essence of , It's actually a function . Multiple Composable After functions are nested with each other , It naturally forms a UI Trees .Composable Function execution process , It's really just a DFS Ergodic process .
second ,@Composable Modified function , Will eventually be Compose Compiler plug-in modification , Not only does its function signature change , The logic of its function body will also change dramatically . Changes in function signature , As a result, ordinary functions cannot be called directly Composable function ; Changes in the body of a function , To better describe Compose Of UI structure , And realize 「 restructuring 」.
Third , restructuring , In essence, it means to be Compose When the state changes ,Runtime Yes Composable Functional Repeated calls to . This involves Compose Snapshot system of , also ScopeUpdateScope.
Fourth , because ScopeUpdateScope It's up to us to State Read location of , therefore , This determines that we can use Kotlin In functional programming Laziness thought , Yes Compose Conduct 「 performance optimization 」. Also is to let : The state reading is consistent with the use position , Minimize as much as possible 「 Reorganize scope 」, Try to avoid 「 restructuring 」 happen .
The fifth , This year's Google I/O At the conference ,Android The official team proposed : 5 Best practices for performance optimization , among 3 The essence of this recommendation , They are all practicing : The state reading is consistent with the use position Principles .
The sixth , We analyzed one of the suggestions in detail 「 Delay as much as possible State The act of reading 」. because Compose The implementation process of is divided into :「Composition」、「Layout」、「Draw」, adopt Laziness, We can get Compose skip 「 restructuring 」 The stage of , Greatly enhance Compose Performance of .

Conclusion
Actually ,Compose The principle of is quite complicated . In addition to UI Layer heel Android There is a strong correlation , Other parts Compiler、Runtime、Snapshot Can be independent of Android Exist outside . That's why JetBrains Can be based on Jetpack Compose build Compose-jb Why .

Long press the QR code on the right
See more developers share

" The developer said ·DTalk" oriented
Chinese developers solicit Google Mobile application (apps & games) Related products / Technical content . Welcome to share your insights or insights on the mobile application industry 、 Experience or new discovery in the process of mobile development 、 As well as the application of the sea experience summary and the use of related products feedback . We sincerely hope to provide these outstanding Chinese developers with better opportunities to show themselves 、 A platform to give full play to one's strong points . We will focus on selecting excellent cases for Google development through your technical content (GDE) The recommendation of .
Click at the end of the screen | Read the original | Sign up now " The developer said ·DTalk"

边栏推荐
- Maui introductory tutorial series (1. framework introduction)
- NielsenIQ宣布任命Tracey Massey为首席运营官
- 【愚公系列】2022年06月 .NET架构班 079-分布式中间件 ScheduleMaster的集群原理
- Overview and example analysis of opengauss database performance tuning
- MSDN download win11 method, simple and easy to operate
- 3000 words to teach you how to use mot
- AutoRunner自动化测试工具如何创建项目-Alltesting|泽众云测试
- 让快递快到来不及退款的,真的不是人
- It's really not human to let the express delivery arrive before the refund
- [Yugong series] June 2022 Net architecture class 076- execution principle of distributed middleware schedulemaster
猜你喜欢

Opengauss AI capability upgrade to create a new AI native database

From 0 to 1, master the mainstream technology of large factories steadily. Isn't it necessary to increase salary after one year?

Nat Common | le Modèle linguistique peut apprendre des distributions moléculaires complexes

使用Cloud DB构建APP 快速入门-快应用篇

Kill the swagger UI. This artifact is better and more efficient!

NielsenIQ宣布任命Tracey Massey为首席运营官

Using cloud DB to build apps quick start - quick games

泰雷兹云安全报告显示,云端数据泄露和复杂程度呈上升趋势
![[Yugong series] June 2022 Net architecture class 076- execution principle of distributed middleware schedulemaster](/img/c4/65babf7f51eaf1445ad6a02330975e.png)
[Yugong series] June 2022 Net architecture class 076- execution principle of distributed middleware schedulemaster

再聊数据中心网络
随机推荐
How to manage concurrent write operations? Get you started quickly
前沿科技探究之AI功能:慢SQL发现
3000 words to teach you how to use mot
Frontier technology exploration deepsql: in Library AI algorithm
Opengauss AI capability upgrade to create a new AI native database
AI4DB:人工智能之慢SQL根因分析
Nat Commun|语言模型可以学习复杂的分子分布
Streaking? Baa!
Hands on, how should selenium deal with pseudo elements?
从屡遭拒稿到90后助理教授,罗格斯大学王灏:好奇心驱使我不断探索
[daily question series]: how to test web forms?
Overview and example analysis of opengauss database performance tuning
前沿科技探究之AI工具:Anomaly-detection
Kill the swagger UI. This artifact is better and more efficient!
With an average annual salary of 20W, automated test engineers are so popular?
MSDN download win11 method, simple and easy to operate
Will you be punished for not wearing seat belts in the back row?
GO語言-值類型和引用類型
Deep separable convolution
Opengauss database JDBC environment connection configuration (eclipse)