施工企業(yè)資質(zhì)證書延期搜索引擎優(yōu)化寶典
本文字?jǐn)?shù):4680字
預(yù)計(jì)閱讀時(shí)間:30分鐘
01
簡介
之前我們探討過KMM,即Kotlin Multiplatform Mobile,是Kotlin發(fā)布的移動(dòng)端跨平臺框架。當(dāng)時(shí)的結(jié)論是KMM提倡將共有的邏輯部分抽出,由KMM封裝成Android(Kotlin/JVM)的aar和iOS(Kotlin/Native)的framework,再提供給View層進(jìn)行調(diào)用,從而節(jié)約一部分的工作量。共享的是邏輯而不是UI。(1)
其實(shí)在這個(gè)時(shí)候我們就知道Kotlin在移動(dòng)端的跨平臺絕對不是想止于邏輯層的共享,隨著Compose的日漸成熟,JetBrains推出了Compose-Multiplatform,從UI層面上實(shí)現(xiàn)移動(dòng)端,Web端,桌面端的跨平臺??紤]到屏幕大小與交互方式的不同,Android和iOS之間的共享會(huì)極大的促進(jìn)開發(fā)效率。比如現(xiàn)在已經(jīng)非常成熟的Flutter。令人興奮的是,Compose-Multiplatform目前已經(jīng)發(fā)布了支持iOS系統(tǒng)的alpha版本,雖然還在開發(fā)實(shí)驗(yàn)階段,但我們已經(jīng)開始嘗試用起來了。
02
Jetpack-Compose與
Compose-Multiplatform
作為Android開發(fā),Jetpack-Compose我們再熟悉不過了,是Google針對Android推出的新一代聲明式UI工具包,完全基于Kotlin打造,天然具備了跨平臺的使用基礎(chǔ)。JetBrains以Jetpack-Compose為基礎(chǔ),相繼發(fā)布了compose-desktop,compose-web和compose-iOS ,使Compose可以運(yùn)行在更多不同平臺,也就是我們今天要講的Compose-Multiplatform。在通用的API上Compose-Multiplatform與Jetpack-Compose時(shí)刻保持一致,不同的只是包名發(fā)生了變化。因此作為Android開發(fā),我們在使用Compose-Multiplatform時(shí),可以將Jetpack-Compose代碼低成本地遷移到Compose-Multiplatform:
03
使用
既然是UI框架,那么我們就來實(shí)現(xiàn)一個(gè)簡單的在移動(dòng)端非常常規(guī)的業(yè)務(wù)需求:
從服務(wù)器請求數(shù)據(jù),并以列表形式展現(xiàn)在UI上。
在此我們要說明的是,Compose-Multiplatform是要與KMM配合使用的,其中KMM負(fù)責(zé)把shared模塊編譯成Android的aar和iOS的framework,Compose-Multiplatform負(fù)責(zé)UI層面的交互與繪制的實(shí)現(xiàn)。
首先我們先回顧一下KMM工程的組織架構(gòu):
其中androidApp和iosApp分別為Android和iOS這兩個(gè)平臺的主工程模塊,shared為共享邏輯模塊,供androidApp和iosApp調(diào)用。shared模塊中:
commonMain為公共模塊,該模塊的代碼與平臺無關(guān),是通過expected關(guān)鍵字對一些api的聲明(聲明的實(shí)現(xiàn)在platform module中);
androidMain和iosMain分別Android和ios這兩個(gè)平臺,通過actual關(guān)鍵字在平臺模塊進(jìn)行具體的實(shí)現(xiàn)。
關(guān)于kmm工程的配置與使用方式,運(yùn)行方式,編譯過程原理還是請回顧一下之前的文章,在此不做贅述。(2)
接下來我們看Compose-Multiplatform是怎么基于kmm工程進(jìn)行的實(shí)現(xiàn)。?
1、添加配置
在settings.gradle文件中聲明compose插件:
plugins{
//...val composeVersion?=?extra["compose.version"]?as Stringid("org.jetbrains.compose").version(composeVersion)}
其中compose.version在gradle.properties進(jìn)行了聲明。需要注意的是目前Compose-Multiplatform的版本有要求,目前可以參考官方的具體配置。(3)
#Versions
kotlin.version=1.8.20
agp.version=7.4.2
compose.version=1.4.0
之后在shared模塊的build.gradle文件中引用聲明好的插件如下:
plugins?{
//...id("org.jetbrains.compose")
}
同時(shí)我們需要在build.gradle文件中配置compose靜態(tài)資源文件的目錄,方式如下:
Android:
android?{
//...sourceSets["main"].resources.srcDirs("src/commonMain/resources")
}
iOS:
cocoapods?{
//...extraSpecAttributes["resources"]?="['src/commonMain/resources/**',?'src/iosMain/resources/**']"}
這意味著在尋找如圖片等資源文件時(shí),將從src/commonMain/resources/這個(gè)目錄下尋找,如下圖所示:
由于目前compose-iOS還處于實(shí)驗(yàn)階段,我們需要在gradle.properties文件中添加如下代碼開啟UIKit:
org.jetbrains.compose.experimental.uikit.enabled=true
最后我們需要在為commonMain添加compose依賴:
val commonMain by getting?{dependencies?{
//...implementation(compose.runtime)implementation(compose.foundation)implementation(compose.material)
//????????????????//implementation(compose.materialIconsExtended)?//?TODO not working on iOS?for?now@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)implementation(compose.components.resources)implementation(compose.ui)}}
好了到此為止我們的配置就完成了,接下來開始寫業(yè)務(wù)代碼了。既然是從服務(wù)器獲取數(shù)據(jù),我們肯定得封裝一個(gè)網(wǎng)絡(luò)模塊,下面我們將使用ktor封裝一個(gè)簡單的網(wǎng)絡(luò)模塊。?
2、網(wǎng)絡(luò)模塊
先我們先在shared模塊的build.gradle文件中添加依賴如下:
val commonMain by getting?{dependencies?{implementation("io.ktor:ktor-client-core:$ktor_version")//coreimplementation("io.ktor:ktor-client-cio:$ktor_version")//CIOimplementation("io.ktor:ktor-client-logging:$ktor_version")//Loggingimplementation("io.ktor:ktor-client-content-negotiation:$ktor_version")implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")//Json格式化
//...}}
接下來我們封裝一個(gè)最簡單的HttpUtil,包含post和get請求;
package com.example.sharesampleimport io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.*
import kotlinx.serialization.json.Jsonclass HttpUtil{companion object{val client:?HttpClient?=?HttpClient(CIO)?{expectSuccess?=?trueengine?{maxConnectionsCount?=?1000requestTimeout?=?30000endpoint?{maxConnectionsPerRoute?=?100pipelineMaxSize?=?20keepAliveTime?=?30000connectTimeout?=?30000}}install(Logging)?{logger?=?Logger.DEFAULTlevel?=?LogLevel.HEADERS}install(ContentNegotiation)?{json(Json?{ignoreUnknownKeys?=?trueisLenient?=?trueencodeDefaults?=?false})}}suspend?inline fun?<reified T>?get(url:?String,//請求地址):?T???{return?try?{val response:?HttpResponse?=?client.get(url)?{//GET請求contentType(ContentType.Application.Json)//content-type}val data:?T?=?response.body()data}?catch?(e:?ResponseException)?{print(e.response)null}?catch?(e:?Exception)?{print(e.message)null}}suspend?inline fun?<reified T>?post(url:?String,):?T???{//coroutines?中的IO線程return?try?{val response:?HttpResponse?=?client.post(url)?{//POST請求contentType(ContentType.Application.Json)//content-type}val data:?T?=?response.body()data}?catch?(e:?ResponseException)?{print(e.response)null}?catch?(e:?Exception)?{print(e.message)null}}}
}
代碼非常直觀,定義了HttpClient對象,進(jìn)行了基礎(chǔ)的設(shè)置來實(shí)現(xiàn)網(wǎng)絡(luò)請求。我們來定義一下接口請求返回的數(shù)據(jù)結(jié)構(gòu)。
3、返回的數(shù)據(jù)結(jié)構(gòu)
package com.example.sharesample.bean@kotlinx.serialization.Serializable
class SearchResult?{var count:?Int??=?nullvar resInfos:?List<ResInfoBean>??=?null
}
package com.example.sharesample.bean@kotlinx.serialization.Serializable
class ResInfoBean?{var name:?String??=?nullvar desc:?String??=?null
}
接下來我們看看是怎么發(fā)送的請求。
4、發(fā)送請求
然后我們定義個(gè)SearchApi:
package com.example.sharesampleimport androidx.compose.material.Text
import androidx.compose.runtime.*
import com.example.sharesample.bean.SearchResult
import io.ktor.client.plugins.logging.*
import kotlinx.coroutines.*class SearchApi?{suspend?fun search():?SearchResult?{Logger.SIMPLE.log("search2")var result:?SearchResult??=HttpUtil.get(url?=?"http://h5-yapi.sns.sohuno.com/mock/229/api/v1/resInfo/search")if?(result?==?null)?{result?=?SearchResult()}return?result}
}
實(shí)現(xiàn)了search()方法。接著我們來看view層的實(shí)現(xiàn)與數(shù)據(jù)的綁定是如何實(shí)現(xiàn)的。
5、View層的實(shí)現(xiàn)
我們創(chuàng)建一個(gè)SearchCompose:
package com.example.sharesampleimport androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.sharesample.bean.SearchResult
import io.ktor.client.plugins.logging.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.resourceclass SearchCompose?{private val searchApi?=?SearchApi()private var isInit?=?false@OptIn(ExperimentalResourceApi::class)@Composablefun?searchCompose()?{var searchResult by remember?{?mutableStateOf<SearchResult>(SearchResult())?}if?(!isInit)?{scope().launch?{val result?=?async?{searchApi.search()}searchResult?=?result.await()}isInit?=?true}Column?{Text("Total:?${searchResult.count??:?0}",style?=?TextStyle(fontSize?=?20.sp),modifier?=?Modifier.padding(start?=?20.dp,?top?=?20.dp))val scrollState?=?rememberLazyListState()if?(searchResult.resInfos?!=?null)?{LazyColumn(state?=?scrollState,modifier?=?Modifier.padding(top?=?14.dp,bottom?=?50.dp,end?=?14.dp,start?=?14.dp))?{items(searchResult.resInfos!!)?{?item?->Box(modifier?=?Modifier.padding(top?=?20.dp).fillMaxWidth().background(color?=?Color.LightGray,?shape?=?RoundedCornerShape(10.dp)).padding(all?=?20.dp))?{Column?{Row(verticalAlignment?=?Alignment.CenterVertically)?{val picture?=?"1.jpg"var imageBitmap:?ImageBitmap??by remember(picture)?{mutableStateOf(null)}LaunchedEffect(picture)?{try?{imageBitmap?=resource(picture).readBytes().toImageBitmap()}?catch?(e:?Exception)?{}}if?(imageBitmap?!=?null)?{Image(bitmap?=?imageBitmap!!,?"",?modifier?=?Modifier.size(60.dp).clip(RoundedCornerShape(10.dp)))}Text(item.name??:?"name",style?=?TextStyle(color?=?Color.Yellow),modifier?=?Modifier.padding(start?=?10.dp))}Text(item.desc??:?"desc",?style?=?TextStyle(color?=?Color.White))}}}}}}}
}@Composable
fun scope():?CoroutineScope?{var viewScope?=?rememberCoroutineScope()return?remember?{CoroutineScope(SupervisorJob(viewScope.coroutineContext.job)?+?ioDispatcher)}
}
在searchCompose()里我們看到了在發(fā)送請求時(shí)開啟了一個(gè)協(xié)程,scope()方法指定了作用域,除此之外,我們還定義了ioDispatcher在不同平臺下的實(shí)現(xiàn),具體的聲明如下:
expect val ioDispatcher:?CoroutineDispatcher
在Android上的實(shí)現(xiàn):
actual val ioDispatcher?=?Dispatchers.IO
在ios上的實(shí)現(xiàn):
actual val ioDispatcher?=?Dispatchers.IO
需要注意的是,Android平臺,Dispatchers.IO在jvmMain/Dispatchers,ios平臺,Dispatchers.IO在nativeMain/Dispatchers下。兩者是不一樣的。在獲取了服務(wù)端數(shù)據(jù)后,我們使用LazyColumn對列表進(jìn)行實(shí)現(xiàn)。其中有圖片和文本的展示。為了方便進(jìn)行說明,圖片數(shù)據(jù)我們使用本地resources目錄下的圖片,文本展示的是服務(wù)端返回的數(shù)據(jù)。下面我來說明一下圖片的加載。
6、圖片加載
具體的實(shí)現(xiàn)如下:
val picture?=?"1.jpg"
var imageBitmap:?ImageBitmap??by remember(picture)?{mutableStateOf(null)
}
LaunchedEffect(picture)?{try?{imageBitmap?=resource(picture).readBytes().toImageBitmap()}?catch?(e:?Exception)?{}}
if?(imageBitmap?!=?null)?{Image(bitmap?=?imageBitmap!!,?"",?modifier?=?Modifier.size(60.dp).clip(RoundedCornerShape(10.dp)))
}
先創(chuàng)建了一個(gè)ImageBitmap的remember對象,由于resource(picture).readBytes()是個(gè)掛起函數(shù),我們需要用LaunchedEffect來執(zhí)行。這段代碼的作用是從resources目錄下讀取資源到內(nèi)存中,然后我們在不同平臺實(shí)現(xiàn)了toImageBitmap()將它轉(zhuǎn)換成Bitmap。
toImageBitmap()的聲明:
expect fun ByteArray.toImageBitmap():?ImageBitmap
Android端的實(shí)現(xiàn):
fun ByteArray.toAndroidBitmap():?Bitmap?{return?BitmapFactory.decodeByteArray(this,?0,?size)
}
iOS端的實(shí)現(xiàn):
actual fun ByteArray.toImageBitmap():?ImageBitmap?=Image.makeFromEncoded(this).toComposeImageBitmap()
好了通過以上的方式我們就可以實(shí)現(xiàn)對本地圖片的加載,到此為止,Compose的相應(yīng)實(shí)現(xiàn)就完成了。那么它是怎么被Android和ios的view引用的呢?Android端我們已經(jīng)非常熟悉了,和Jetpack-Compose的調(diào)用方式一樣,在MainActivity中直接調(diào)用即可:
class MainActivity?:?ComponentActivity()?{override fun onCreate(savedInstanceState:?Bundle?)?{super.onCreate(savedInstanceState)setContent?{MyApplicationTheme?{Surface(modifier?=?Modifier.fillMaxSize(),color?=?MaterialTheme.colors.background)?{SearchCompose().searchCompose()}}}}
}
ios端會(huì)稍微麻煩一點(diǎn)。我們先來看一下iosApp模塊下iOSApp.swift的實(shí)現(xiàn):
import UIKit
import shared@UIApplicationMain
class AppDelegate:?UIResponder,?UIApplicationDelegate?{var window:?UIWindow?func application(_ application:?UIApplication,?didFinishLaunchingWithOptions launchOptions:?[UIApplication.LaunchOptionsKey:?Any]?)?->?Bool?{window?=?UIWindow(frame:?UIScreen.main.bounds)let?mainViewController?=?Main_iosKt.MainViewController()window?.rootViewController?=?mainViewControllerwindow?.makeKeyAndVisible()return?true}
}
關(guān)鍵代碼是這兩行:
let?mainViewController?=?Main_iosKt.MainViewController()window?.rootViewController?=?mainViewController
創(chuàng)建了一個(gè)MainViewController對象,然后賦給window的rootViewController。這個(gè)MainViewController是在哪兒怎么定義的呢?我們回到shared模塊,定義一個(gè)main.ios的文件,它會(huì)在framework編譯成Main_iosKt文件。main.ios的實(shí)現(xiàn)如下:
package com.example.sharesampleimport androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.ComposeUIViewController
import platform.UIKit.UIViewController@Suppress("FunctionName",?"unused")
fun MainViewController():?UIViewController?=ComposeUIViewController?{MaterialTheme?{Surface(modifier?=?Modifier.fillMaxSize(),color?=?MaterialTheme.colors.background)?{SearchCompose().searchCompose()}}}
我們看到在這兒會(huì)創(chuàng)建一個(gè)UIViewController對象MainViewController。這個(gè)便是ios端和Compose鏈接的橋梁。接下來我們來看看在Android和ios上的效果。
Android端:
iOS端:
好了到此為止,我們看到了一個(gè)簡單的列表業(yè)務(wù)邏輯是怎樣實(shí)現(xiàn)的了。由于Compose-Multiplatform還未成熟,在業(yè)務(wù)實(shí)現(xiàn)上勢必有很多內(nèi)容需要自己造輪子。?
04
Android端的compose繪制原理
由于網(wǎng)上已經(jīng)有很多Compose的相關(guān)繪制原理,下一章我們只是進(jìn)行簡單的源碼解析,來說明它是如何生成UI樹并進(jìn)行自繪的。
1、Android端的compose繪制原理
Android端是在從onCreate()里實(shí)現(xiàn)setContent()開始的:
setContent?{MyApplicationTheme?{Surface(modifier?=?Modifier.fillMaxSize(),color?=?MaterialTheme.colors.background)?{SearchCompose().searchCompose()}}}
setContent()的實(shí)現(xiàn)如下:
public fun ComponentActivity.setContent(parent:?CompositionContext??=?null,content:?@Composable?()?->?Unit
)?{val existingComposeView?=?window.decorView.findViewById<ViewGroup>(android.R.id.content).getChildAt(0)?as??ComposeViewif?(existingComposeView?!=?null)?with(existingComposeView)?{setParentCompositionContext(parent)setContent(content)}?else?ComposeView(this).apply?{//?Set content and parent?**before**?setContentView//?to have ComposeView create the composition on attachsetParentCompositionContext(parent)setContent(content)//?Set the view tree owners before setting the content view so that the inflation process//?and attach listeners will see them already presentsetOwners()setContentView(this,?DefaultActivityContentLayoutParams)}
}
我們看到它主要是生成了ComposeView然后通過setContent(content)將compose的內(nèi)容注冊到ComposeView里,其中ComposeView繼承ViewGroup,然后調(diào)用ComponentActivity的setContentView()方法將ComposeView添加到DecorView中相應(yīng)的子View中。通過追蹤C(jī)omposeView的setContent方法:
private fun doSetContent(owner:?AndroidComposeView,parent:?CompositionContext,content:?@Composable?()?->?Unit
):?Composition?{if?(inspectionWanted(owner))?{owner.setTag(R.id.inspection_slot_table_set,Collections.newSetFromMap(WeakHashMap<CompositionData,?Boolean>()))enableDebugInspectorInfo()}//?創(chuàng)建Composition對象,傳入U(xiǎn)iApplierval original?=?Composition(UiApplier(owner.root),?parent)val wrapped?=?owner.view.getTag(R.id.wrapped_composition_tag)as??WrappedComposition?:?WrappedComposition(owner,?original).also?{owner.view.setTag(R.id.wrapped_composition_tag,?it)}//?傳入content函數(shù)wrapped.setContent(content)return?wrapped
}
我們發(fā)現(xiàn)主要做了兩件事情:
1.創(chuàng)建Composition對象,傳入U(xiǎn)iApplier
2.傳入content函數(shù)
其中UiApplier的定義如下:
internal class UiApplier(root:?LayoutNode
)?:?AbstractApplier<LayoutNode>(root)
持有一個(gè)LayoutNode對象,它的說明如下:
An element in the layout hierarchy, built with compose UI
可以看到LayoutNode就是在Compose渲染的時(shí)候,每一個(gè)組件就是一個(gè)LayoutNode,最終組成一個(gè)LayoutNode樹,來描述UI界面。LayoutNode是怎么創(chuàng)建的呢?
1)LayoutNode
我們假設(shè)創(chuàng)建一個(gè)Image,來看看Image的實(shí)現(xiàn):
fun Image(painter:?Painter,contentDescription:?String?,modifier:?Modifier?=?Modifier,alignment:?Alignment?=?Alignment.Center,contentScale:?ContentScale?=?ContentScale.Fit,alpha:?Float?=?DefaultAlpha,colorFilter:?ColorFilter??=?null
)?{
//...Layout({},modifier.then(semantics).clipToBounds().paint(painter,alignment?=?alignment,contentScale?=?contentScale,alpha?=?alpha,colorFilter?=?colorFilter))?{?_,?constraints?->layout(constraints.minWidth,?constraints.minHeight)?{}}
}
繼續(xù)追蹤Layout()的實(shí)現(xiàn):
@Composable inline fun Layout(content:?@Composable?@UiComposable?()?->?Unit,modifier:?Modifier?=?Modifier,measurePolicy:?MeasurePolicy
)?{val density?=?LocalDensity.currentval layoutDirection?=?LocalLayoutDirection.currentval viewConfiguration?=?LocalViewConfiguration.currentReusableComposeNode<ComposeUiNode,?Applier<Any>>(factory?=?ComposeUiNode.Constructor,update?=?{set(measurePolicy,?ComposeUiNode.SetMeasurePolicy)set(density,?ComposeUiNode.SetDensity)set(layoutDirection,?ComposeUiNode.SetLayoutDirection)set(viewConfiguration,?ComposeUiNode.SetViewConfiguration)},skippableUpdate?=?materializerOf(modifier),content?=?content)
}@Composable?@ExplicitGroupsComposable
inline fun?<T,?reified E?:?Applier<*>>?ReusableComposeNode(noinline factory:?()?->?T,update:?@DisallowComposableCalls Updater<T>.()?->?Unit,noinline skippableUpdate:?@Composable SkippableUpdater<T>.()?->?Unit,content:?@Composable?()?->?Unit
)?{if?(currentComposer.applier?!is E)?invalidApplier()currentComposer.startReusableNode()if?(currentComposer.inserting)?{currentComposer.createNode(factory)}?else?{currentComposer.useNode()}Updater<T>(currentComposer).update()SkippableUpdater<T>(currentComposer).skippableUpdate()currentComposer.startReplaceableGroup(0x7ab4aae9)content()currentComposer.endReplaceableGroup()currentComposer.endNode()
}
在這里創(chuàng)建了ComposeUiNode對象,而LayoutNode就是ComposeUiNode的實(shí)現(xiàn)類。我們再來看看Composition。
2)Composition
從命名來看,Composition的作用就是將LayoutNode組合起來。其中WrappedComposition繼承Composition:
private class WrappedComposition(val owner:?AndroidComposeView,val original:?Composition
)?:?Composition,?LifecycleEventObserver
我們來追蹤一下它的setContent()的實(shí)現(xiàn):
override fun setContent(content:?@Composable?()?->?Unit)?{owner.setOnViewTreeOwnersAvailable?{if?(!disposed)?{val lifecycle?=?it.lifecycleOwner.lifecyclelastContent?=?contentif?(addedToLifecycle?==?null)?{addedToLifecycle?=?lifecycle//?this will call ON_CREATE synchronously?if?we already createdlifecycle.addObserver(this)}?else?if?(lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED))?{original.setContent?{@Suppress("UNCHECKED_CAST")val inspectionTable?=owner.getTag(R.id.inspection_slot_table_set)?as?MutableSet<CompositionData>?:?(owner.parent as??View)?.getTag(R.id.inspection_slot_table_set)as??MutableSet<CompositionData>if?(inspectionTable?!=?null)?{inspectionTable.add(currentComposer.compositionData)currentComposer.collectParameterInformation()}LaunchedEffect(owner)?{?owner.boundsUpdatesEventLoop()?}CompositionLocalProvider(LocalInspectionTables provides inspectionTable)?{ProvideAndroidCompositionLocals(owner,?content)}}}}}}
在頁面的生命周期是CREATED的狀態(tài)下,執(zhí)行original.setContent():
override fun setContent(content:?@Composable?()?->?Unit)?{check(!disposed)?{?"The composition is disposed"?}this.composable?=?contentparent.composeInitial(this,?composable)}
調(diào)用parent的composeInitial()方法,這段代碼我們就不再繼續(xù)追蹤下去了,它最終的作用就是對布局進(jìn)行組合,創(chuàng)建父子依賴關(guān)系。?
3)Measure和Layout
在AndroidComposeView中的dispatchDraw()實(shí)現(xiàn)了measureAndLayout()方法:
override fun measureAndLayout(sendPointerUpdate:?Boolean)?{trace("AndroidOwner:measureAndLayout")?{val resend?=?if?(sendPointerUpdate)?resendMotionEventOnLayout?else?nullval rootNodeResized?=?measureAndLayoutDelegate.measureAndLayout(resend)if?(rootNodeResized)?{requestLayout()}measureAndLayoutDelegate.dispatchOnPositionedCallbacks()}}fun measureAndLayout(onLayout:?(()?->?Unit)??=?null):?Boolean?{var rootNodeResized?=?falseperformMeasureAndLayout?{if?(relayoutNodes.isNotEmpty())?{relayoutNodes.popEach?{?layoutNode?->val sizeChanged?=?remeasureAndRelayoutIfNeeded(layoutNode)if?(layoutNode?===?root?&&?sizeChanged)?{rootNodeResized?=?true}}onLayout?.invoke()}}callOnLayoutCompletedListeners()return?rootNodeResized}
調(diào)用remeasureAndRelayoutIfNeeded,遍歷relayoutNodes,為每一個(gè)LayoutNode去進(jìn)行measure和layout。具體的實(shí)現(xiàn)不分析了。
4)繪制
我們還是以Image舉例:
fun Image(bitmap:?ImageBitmap,contentDescription:?String?,modifier:?Modifier?=?Modifier,alignment:?Alignment?=?Alignment.Center,contentScale:?ContentScale?=?ContentScale.Fit,alpha:?Float?=?DefaultAlpha,colorFilter:?ColorFilter??=?null,filterQuality:?FilterQuality?=?DefaultFilterQuality
)?{val bitmapPainter?=?remember(bitmap)?{?BitmapPainter(bitmap,?filterQuality?=?filterQuality)?}Image(painter?=?bitmapPainter,contentDescription?=?contentDescription,modifier?=?modifier,alignment?=?alignment,contentScale?=?contentScale,alpha?=?alpha,colorFilter?=?colorFilter)
}
主要的繪制工作是由BitmapPainter完成的,它繼承自Painter。
override fun DrawScope.onDraw()?{drawImage(image,srcOffset,srcSize,dstSize?=?IntSize(this@onDraw.size.width.roundToInt(),this@onDraw.size.height.roundToInt()),alpha?=?alpha,colorFilter?=?colorFilter,filterQuality?=?filterQuality)}
在onDraw()方法里實(shí)現(xiàn)了drawImage():
override fun drawImage(image:?ImageBitmap,srcOffset:?IntOffset,srcSize:?IntSize,dstOffset:?IntOffset,dstSize:?IntSize,/*FloatRange(from?=?0.0,?to?=?1.0)*/alpha:?Float,style:?DrawStyle,colorFilter:?ColorFilter?,blendMode:?BlendMode,filterQuality:?FilterQuality)?=?drawParams.canvas.drawImageRect(image,srcOffset,srcSize,dstOffset,dstSize,configurePaint(null,?style,?alpha,?colorFilter,?blendMode,?filterQuality))
而最終也是在Canvas上進(jìn)行了繪制。通過以上的分析,我們了解到Compose并不是和原生控件一一映射的關(guān)系,而是像Flutter一樣,有自己的UI組織方式,并最終調(diào)用自繪引擎直接在Canvas上進(jìn)行繪制的。在Android和iOS端使用的自繪引擎是skiko。這個(gè)skiko是什么呢?它其實(shí)是Skia for Kotlin的縮寫(Flutter在移動(dòng)端也是用的Skia引擎進(jìn)行的繪制)。事實(shí)上不止是在移動(dòng)端,我們可以通過以下的截圖看到,Compose的桌面端和Web端的繪制實(shí)際上也是用了skiko:

關(guān)于skiko的更多信息,還請查閱文末的官方鏈接。(4)
好了到此為止,Compose的在Android端的繪制原理我們就講完了。對其他端繪制感興趣的同學(xué)可自行查看相應(yīng)的源碼,細(xì)節(jié)有不同,但理念都是一致的:創(chuàng)建自己的Compose樹,并最終調(diào)用自繪引擎在Canvas上進(jìn)行繪制。
05
Compose-Multiplatform與Flutter?
為什么要單拿它倆出來說一下呢?是因?yàn)樵谡{(diào)研Compose-Multiplatform的過程中,我們發(fā)現(xiàn)它跟Flutter的原理類似,那未來可能就會(huì)有競爭,有競爭就意味著開發(fā)同學(xué)若在自己的項(xiàng)目中使用跨平臺框架需要選擇。那么我們來對比一下這兩個(gè)框架:在之前KMM的文章中,我們比較過KMM和Flutter,結(jié)論是:
KMM主要實(shí)現(xiàn)的是共享邏輯,UI層的實(shí)現(xiàn)還是建議平臺各自去處理;
Flutter是UI層的共享。
當(dāng)時(shí)看來兩者雖然都是跨平臺,但目標(biāo)不同,看上去并沒有形成競爭。而在Compose-Multiplatform加入之后,結(jié)合KMM,成為了邏輯和UI都可以實(shí)現(xiàn)共享的結(jié)果。而且從繪制原理上來說,Compose和Flutter都是創(chuàng)建自己的View樹,在通過自繪引擎進(jìn)行渲染,原理上差異不大。再加上Kotlin和Compose作為Android的官方推薦,對于Android同學(xué)來說,基本上是沒有什么學(xué)習(xí)成本的。個(gè)人認(rèn)為若Compose-Multiplatform更加成熟,發(fā)布穩(wěn)定版后與Flutter的競爭會(huì)非常大。?
06
總結(jié)
Compose-Multiplatform目前雖然還不成熟,但通過對其原理的分析,我們可以預(yù)見的是,結(jié)合KMM,未來將成為跨平臺的有力競爭者。特別對于Android開發(fā)同學(xué)來說,可以把KMM先用起來,結(jié)合Compose去實(shí)現(xiàn)一些低耦合的業(yè)務(wù),待未來Compose-iOS發(fā)布穩(wěn)定版后,可以愉快的進(jìn)行雙端開發(fā),節(jié)約開發(fā)成本。
參考:
(1)https://www.jianshu.com/p/e1ae5eaa894e
(2)https://www.jianshu.com/p/e1ae5eaa894e?
(3)https://github.com/JetBrains/compose-multiplatform-ios-android-template
(4)https://github.com/JetBrains/skiko