精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Compose-Multiplatform在A(yíng)ndroid和iOS上的實(shí)踐

移動(dòng)開(kāi)發(fā) iOS Android
Compose-Multiplatform目前雖然還不成熟,但通過(guò)對(duì)其原理的分析,我們可以預(yù)見(jiàn)的是,結(jié)合KMM,未來(lái)將成為跨平臺(tái)的有力競(jìng)爭(zhēng)者。特別對(duì)于A(yíng)ndroid開(kāi)發(fā)同學(xué)來(lái)說(shuō),可以把KMM先用起來(lái),結(jié)合Compose去實(shí)現(xiàn)一些低耦合的業(yè)務(wù),待未來(lái)Compose-iOS發(fā)布穩(wěn)定版后,可以愉快的進(jìn)行雙端開(kāi)發(fā),節(jié)約開(kāi)發(fā)成本。

01簡(jiǎn)介

之前我們探討過(guò)KMM,即Kotlin Multiplatform Mobile,是Kotlin發(fā)布的移動(dòng)端跨平臺(tái)框架。當(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)端的跨平臺(tái)絕對(duì)不是想止于邏輯層的共享,隨著Compose的日漸成熟,JetBrains推出了Compose-Multiplatform,從UI層面上實(shí)現(xiàn)移動(dòng)端,Web端,桌面端的跨平臺(tái)。考慮到屏幕大小與交互方式的不同,Android和iOS之間的共享會(huì)極大的促進(jìn)開(kāi)發(fā)效率。比如現(xiàn)在已經(jīng)非常成熟的Flutter。令人興奮的是,Compose-Multiplatform目前已經(jīng)發(fā)布了支持iOS系統(tǒng)的alpha版本,雖然還在開(kāi)發(fā)實(shí)驗(yàn)階段,但我們已經(jīng)開(kāi)始嘗試用起來(lái)了。

02Jetpack-Compose與Compose-Multiplatform

作為Android開(kāi)發(fā),Jetpack-Compose我們?cè)偈煜げ贿^(guò)了,是Google針對(duì)Android推出的新一代聲明式UI工具包,完全基于Kotlin打造,天然具備了跨平臺(tái)的使用基礎(chǔ)。JetBrains以Jetpack-Compose為基礎(chǔ),相繼發(fā)布了compose-desktop,compose-web和compose-iOS ,使Compose可以運(yùn)行在更多不同平臺(tái),也就是我們今天要講的Compose-Multiplatform。在通用的API上Compose-Multiplatform與Jetpack-Compose時(shí)刻保持一致,不同的只是包名發(fā)生了變化。因此作為Android開(kāi)發(fā),我們?cè)谑褂肅ompose-Multiplatform時(shí),可以將Jetpack-Compose代碼低成本地遷移到Compose-Multiplatform:

圖片圖片

03使用

既然是UI框架,那么我們就來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的在移動(dòng)端非常常規(guī)的業(yè)務(wù)需求:

從服務(wù)器請(qǐng)求數(shù)據(jù),并以列表形式展現(xiàn)在UI上。

在此我們要說(shuō)明的是,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è)平臺(tái)的主工程模塊,shared為共享邏輯模塊,供androidApp和iosApp調(diào)用。shared模塊中:

  • commonMain為公共模塊,該模塊的代碼與平臺(tái)無(wú)關(guān),是通過(guò)expected關(guān)鍵字對(duì)一些api的聲明(聲明的實(shí)現(xiàn)在platform module中);
  • androidMain和iosMain分別Android和ios這兩個(gè)平臺(tái),通過(guò)actual關(guān)鍵字在平臺(tái)模塊進(jìn)行具體的實(shí)現(xiàn)。

關(guān)于kmm工程的配置與使用方式,運(yùn)行方式,編譯過(guò)程原理還是請(qǐng)回顧一下之前的文章,在此不做贅述。(2)

接下來(lái)我們看Compose-Multiplatform是怎么基于kmm工程進(jìn)行的實(shí)現(xiàn)。 

1、添加配置

在settings.gradle文件中聲明compose插件:

plugins{
//...
        val composeVersion = extra["compose.version"] as String
        id("org.jetbrains.compose").version(composeVersion)
    }

其中compose.version在gradle.properties進(jìn)行了聲明。需要注意的是目前Compose-Multiplatform的版本有要求,目前可以參考官方的具體配置。(3)

#Versions
kotlin.versinotallow=1.8.20
agp.versinotallow=7.4.2
compose.versinotallow=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文件中添加如下代碼開(kāi)啟UIKit:

org.jetbrains.compose.experimental.uikit.enabled=true

最后我們需要在為commonMain添加compose依賴(lài):

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)

            }
        }

好了到此為止我們的配置就完成了,接下來(lái)開(kāi)始寫(xiě)業(yè)務(wù)代碼了。既然是從服務(wù)器獲取數(shù)據(jù),我們肯定得封裝一個(gè)網(wǎng)絡(luò)模塊,下面我們將使用ktor封裝一個(gè)簡(jiǎn)單的網(wǎng)絡(luò)模塊。 

2、網(wǎng)絡(luò)模塊

先我們先在shared模塊的build.gradle文件中添加依賴(lài)如下:

val commonMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-core:$ktor_version")//core
                implementation("io.ktor:ktor-client-cio:$ktor_version")//CIO
                implementation("io.ktor:ktor-client-logging:$ktor_version")//Logging
                implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
                implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")//Json格式化
//...
            }
        }

接下來(lái)我們封裝一個(gè)最簡(jiǎn)單的HttpUtil,包含post和get請(qǐng)求;

package com.example.sharesample

import 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.Json

class HttpUtil{
    companion object{
        val client: HttpClient = HttpClient(CIO) {
            expectSuccess = true
            engine {
                maxConnectionsCount = 1000
                requestTimeout = 30000
                endpoint {
                    maxConnectionsPerRoute = 100
                    pipelineMaxSize = 20
                    keepAliveTime = 30000
                    connectTimeout = 30000
                }
            }
            install(Logging) {
                logger = Logger.DEFAULT
                level = LogLevel.HEADERS
            }

            install(ContentNegotiation) {
                json(Json {
                    ignoreUnknownKeys = true
                    isLenient = true
                    encodeDefaults = false
                })
            }
        }

        suspend inline fun <reified T> get(
            url: String,//請(qǐng)求地址
        ): T?  {
            return try {
                val response: HttpResponse = client.get(url) {//GET請(qǐng)求
                    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線(xiàn)程
            return try {
                val response: HttpResponse = client.post(url) {//POST請(qǐng)求
                    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
            }
        }
    }
}

代碼非常直觀(guān),定義了HttpClient對(duì)象,進(jìn)行了基礎(chǔ)的設(shè)置來(lái)實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求。我們來(lái)定義一下接口請(qǐng)求返回的數(shù)據(jù)結(jié)構(gòu)。

3、返回的數(shù)據(jù)結(jié)構(gòu)

package com.example.sharesample.bean

@kotlinx.serialization.Serializable
class SearchResult {
    var count: Int? = null
    var resInfos: List<ResInfoBean>? = null
}
package com.example.sharesample.bean

@kotlinx.serialization.Serializable
class ResInfoBean {
    var name: String? = null
    var desc: String? = null
}

接下來(lái)我們看看是怎么發(fā)送的請(qǐng)求。

4、發(fā)送請(qǐng)求

然后我們定義個(gè)SearchApi:

package com.example.sharesample

import 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()方法。接著我們來(lái)看view層的實(shí)現(xiàn)與數(shù)據(jù)的綁定是如何實(shí)現(xiàn)的。

5、View層的實(shí)現(xiàn)

我們創(chuàng)建一個(gè)SearchCompose:

package com.example.sharesample

import 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.resource

class SearchCompose {
    private val searchApi = SearchApi()
    private var isInit = false

    @OptIn(ExperimentalResourceApi::class)
    @Composable
    fun 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ā)送請(qǐng)求時(shí)開(kāi)啟了一個(gè)協(xié)程,scope()方法指定了作用域,除此之外,我們還定義了ioDispatcher在不同平臺(tái)下的實(shí)現(xiàn),具體的聲明如下:

expect val ioDispatcher: CoroutineDispatcher

在A(yíng)ndroid上的實(shí)現(xiàn):

actual val ioDispatcher = Dispatchers.IO

在ios上的實(shí)現(xiàn):

actual val ioDispatcher = Dispatchers.IO

需要注意的是,Android平臺(tái),Dispatchers.IO在jvmMain/Dispatchers,ios平臺(tái),Dispatchers.IO在nativeMain/Dispatchers下。兩者是不一樣的。在獲取了服務(wù)端數(shù)據(jù)后,我們使用LazyColumn對(duì)列表進(jìn)行實(shí)現(xiàn)。其中有圖片和文本的展示。為了方便進(jìn)行說(shuō)明,圖片數(shù)據(jù)我們使用本地resources目錄下的圖片,文本展示的是服務(wù)端返回的數(shù)據(jù)。下面我來(lái)說(shuō)明一下圖片的加載。

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對(duì)象,由于resource(picture).readBytes()是個(gè)掛起函數(shù),我們需要用LaunchedEffect來(lái)執(zhí)行。這段代碼的作用是從resources目錄下讀取資源到內(nèi)存中,然后我們?cè)诓煌脚_(tá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()

好了通過(guò)以上的方式我們就可以實(shí)現(xiàn)對(duì)本地圖片的加載,到此為止,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)。我們先來(lái)看一下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 = mainViewController
        window?.makeKeyAndVisible()
        return true
    }
}

關(guān)鍵代碼是這兩行:

let mainViewController = Main_iosKt.MainViewController()
        window?.rootViewController = mainViewController

創(chuàng)建了一個(gè)MainViewController對(duì)象,然后賦給window的rootViewController。這個(gè)MainViewController是在哪兒怎么定義的呢?我們回到shared模塊,定義一個(gè)main.ios的文件,它會(huì)在framework編譯成Main_iosKt文件。main.ios的實(shí)現(xiàn)如下:

package com.example.sharesample

import 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對(duì)象MainViewController。這個(gè)便是ios端和Compose鏈接的橋梁。接下來(lái)我們來(lái)看看在A(yíng)ndroid和ios上的效果。

  • Android端:

圖片圖片

  • iOS端:

圖片圖片

好了到此為止,我們看到了一個(gè)簡(jiǎn)單的列表業(yè)務(wù)邏輯是怎樣實(shí)現(xiàn)的了。由于Compose-Multiplatform還未成熟,在業(yè)務(wù)實(shí)現(xiàn)上勢(shì)必有很多內(nèi)容需要自己造輪子。 

04Android端的compose繪制原理

由于網(wǎng)上已經(jīng)有很多Compose的相關(guān)繪制原理,下一章我們只是進(jìn)行簡(jiǎn)單的源碼解析,來(lái)說(shuō)明它是如何生成UI樹(shù)并進(jìn)行自繪的。

1、Android端的compose繪制原理

Android端是在從onCreate()里實(shí)現(xiàn)setContent()開(kāi)始的:

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? ComposeView

    if (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 attach
        setParentCompositionContext(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 present
        setOwners()
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}

我們看到它主要是生成了ComposeView然后通過(guò)setContent(content)將compose的內(nèi)容注冊(cè)到ComposeView里,其中ComposeView繼承ViewGroup,然后調(diào)用ComponentActivity的setContentView()方法將ComposeView添加到DecorView中相應(yīng)的子View中。通過(guò)追蹤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對(duì)象,傳入U(xiǎn)iApplier
    val 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)主要做了兩件事情:

  • 創(chuàng)建Composition對(duì)象,傳入U(xiǎn)iApplier
  • 傳入content函數(shù)

其中UiApplier的定義如下:

internal class UiApplier(
    root: LayoutNode
) : AbstractApplier<LayoutNode>(root)

持有一個(gè)LayoutNode對(duì)象,它的說(shuō)明如下:

An element in the layout hierarchy, built with compose UI

可以看到LayoutNode就是在Compose渲染的時(shí)候,每一個(gè)組件就是一個(gè)LayoutNode,最終組成一個(gè)LayoutNode樹(shù),來(lái)描述UI界面。LayoutNode是怎么創(chuàng)建的呢?

1)LayoutNode

我們假設(shè)創(chuàng)建一個(gè)Image,來(lái)看看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.current
    val layoutDirection = LocalLayoutDirection.current
    val viewConfiguration = LocalViewConfiguration.current
    ReusableComposeNode<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對(duì)象,而LayoutNode就是ComposeUiNode的實(shí)現(xiàn)類(lèi)。我們?cè)賮?lái)看看Composition。

2)Composition

從命名來(lái)看,Composition的作用就是將LayoutNode組合起來(lái)。其中WrappedComposition繼承Composition:

private class WrappedComposition(
    val owner: AndroidComposeView,
    val original: Composition
) : Composition, LifecycleEventObserver

我們來(lái)追蹤一下它的setContent()的實(shí)現(xiàn):

override fun setContent(content: @Composable () -> Unit) {
        owner.setOnViewTreeOwnersAvailable {
            if (!disposed) {
                val lifecycle = it.lifecycleOwner.lifecycle
                lastContent = content
                if (addedToLifecycle == null) {
                    addedToLifecycle = lifecycle
                    // this will call ON_CREATE synchronously if we already created
                    lifecycle.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)
                        }
                    }
                }
            }
        }
    }

在頁(yè)面的生命周期是CREATED的狀態(tài)下,執(zhí)行original.setContent():

override fun setContent(content: @Composable () -> Unit) {
        check(!disposed) { "The composition is disposed" }
        this.composable = content
        parent.composeInitial(this, composable)
    }

調(diào)用parent的composeInitial()方法,這段代碼我們就不再繼續(xù)追蹤下去了,它最終的作用就是對(duì)布局進(jìn)行組合,創(chuàng)建父子依賴(lài)關(guān)系。 

3)Measure和Layout

在A(yíng)ndroidComposeView中的dispatchDraw()實(shí)現(xiàn)了measureAndLayout()方法:

override fun measureAndLayout(sendPointerUpdate: Boolean) {
        trace("AndroidOwner:measureAndLayout") {
            val resend = if (sendPointerUpdate) resendMotionEventOnLayout else null
            val rootNodeResized = measureAndLayoutDelegate.measureAndLayout(resend)
            if (rootNodeResized) {
                requestLayout()
            }
            measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
        }
    }

    fun measureAndLayout(onLayout: (() -> Unit)? = null): Boolean {
        var rootNodeResized = false
        performMeasureAndLayout {
            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)行了繪制。通過(guò)以上的分析,我們了解到Compose并不是和原生控件一一映射的關(guān)系,而是像Flutter一樣,有自己的UI組織方式,并最終調(diào)用自繪引擎直接在Canvas上進(jìn)行繪制的。在A(yíng)ndroid和iOS端使用的自繪引擎是skiko。這個(gè)skiko是什么呢?它其實(shí)是Skia for Kotlin的縮寫(xiě)(Flutter在移動(dòng)端也是用的Skia引擎進(jìn)行的繪制)。事實(shí)上不止是在移動(dòng)端,我們可以通過(guò)以下的截圖看到,Compose的桌面端和Web端的繪制實(shí)際上也是用了skiko:

圖片圖片

關(guān)于skiko的更多信息,還請(qǐng)查閱文末的官方鏈接。(4)

好了到此為止,Compose的在A(yíng)ndroid端的繪制原理我們就講完了。對(duì)其他端繪制感興趣的同學(xué)可自行查看相應(yīng)的源碼,細(xì)節(jié)有不同,但理念都是一致的:創(chuàng)建自己的Compose樹(shù),并最終調(diào)用自繪引擎在Canvas上進(jìn)行繪制。

05Compose-Multiplatform與Flutter 

為什么要單拿它倆出來(lái)說(shuō)一下呢?是因?yàn)樵谡{(diào)研Compose-Multiplatform的過(guò)程中,我們發(fā)現(xiàn)它跟Flutter的原理類(lèi)似,那未來(lái)可能就會(huì)有競(jìng)爭(zhēng),有競(jìng)爭(zhēng)就意味著開(kāi)發(fā)同學(xué)若在自己的項(xiàng)目中使用跨平臺(tái)框架需要選擇。那么我們來(lái)對(duì)比一下這兩個(gè)框架:在之前KMM的文章中,我們比較過(guò)KMM和Flutter,結(jié)論是:

  • KMM主要實(shí)現(xiàn)的是共享邏輯,UI層的實(shí)現(xiàn)還是建議平臺(tái)各自去處理;
  • Flutter是UI層的共享。

當(dāng)時(shí)看來(lái)兩者雖然都是跨平臺(tái),但目標(biāo)不同,看上去并沒(méi)有形成競(jìng)爭(zhēng)。而在Compose-Multiplatform加入之后,結(jié)合KMM,成為了邏輯和UI都可以實(shí)現(xiàn)共享的結(jié)果。而且從繪制原理上來(lái)說(shuō),Compose和Flutter都是創(chuàng)建自己的View樹(shù),在通過(guò)自繪引擎進(jìn)行渲染,原理上差異不大。再加上Kotlin和Compose作為Android的官方推薦,對(duì)于A(yíng)ndroid同學(xué)來(lái)說(shuō),基本上是沒(méi)有什么學(xué)習(xí)成本的。個(gè)人認(rèn)為若Compose-Multiplatform更加成熟,發(fā)布穩(wěn)定版后與Flutter的競(jìng)爭(zhēng)會(huì)非常大。 

06總結(jié)

Compose-Multiplatform目前雖然還不成熟,但通過(guò)對(duì)其原理的分析,我們可以預(yù)見(jiàn)的是,結(jié)合KMM,未來(lái)將成為跨平臺(tái)的有力競(jìng)爭(zhēng)者。特別對(duì)于A(yíng)ndroid開(kāi)發(fā)同學(xué)來(lái)說(shuō),可以把KMM先用起來(lái),結(jié)合Compose去實(shí)現(xiàn)一些低耦合的業(yè)務(wù),待未來(lái)Compose-iOS發(fā)布穩(wěn)定版后,可以愉快的進(jìn)行雙端開(kāi)發(fā),節(jié)約開(kāi)發(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

責(zé)任編輯:武曉燕 來(lái)源: 搜狐技術(shù)產(chǎn)品
相關(guān)推薦

2020-11-26 18:30:33

機(jī)器學(xué)習(xí)Kubernetes開(kāi)發(fā)

2023-09-03 18:55:51

2021-06-05 06:52:16

Kubernetes

2021-08-09 10:21:42

云原生Dubbo3.0 服務(wù)治理

2016-12-23 09:09:54

TensorFlowKubernetes框架

2019-08-19 08:14:52

深度鏈接iOSAndroid

2011-11-02 13:56:13

2023-08-25 08:06:04

項(xiàng)目布局LazyRow?

2018-10-09 14:31:32

SparkCI灰度

2020-09-28 10:05:57

數(shù)據(jù)工具技術(shù)

2012-03-06 09:46:25

iOSHTML5Android

2015-10-14 10:02:33

ClojureScri Android

2017-08-15 19:20:51

AndroidHttpServer

2015-05-12 09:40:11

WindowsAndroidiOS

2021-09-07 10:17:35

iOS多語(yǔ)言適配設(shè)計(jì)

2016-09-07 13:49:11

AppiumAndroid UI應(yīng)用

2011-06-28 09:19:40

C#XNAiOS

2023-04-25 10:04:25

ZadigDigest追蹤

2012-11-30 08:56:51

IDCiOSAndroid

2011-05-26 17:02:19

金山快盤(pán)android
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

91国内精品野花午夜精品 | 老司机午夜精品99久久| 日韩少妇与小伙激情| 无人码人妻一区二区三区免费| 成人免费一区二区三区牛牛| 国产无一区二区| 97se视频在线观看| αv一区二区三区| 天堂资源在线视频| 一区二区三区视频免费视频观看网站| 无码av免费一区二区三区试看| 色狠狠久久av五月综合|| 成人h动漫精品一区二区无码 | 亚洲福利精品视频| 神马午夜伦理不卡| 亚洲国产精品传媒在线观看| 不卡日韩av| 中文字幕视频二区| 亚洲黄色影院| 久久99久久亚洲国产| 国产精品成人无码免费| 五月天婷婷网站| 综合中文字幕| 欧美人体做爰大胆视频| 国产免费毛卡片| 欧美xxxx免费虐| 亚洲欧洲日韩av| 免费看成人片| 亚洲a视频在线| 精品一区二区三区欧美| 日本久久久久久久| 国产网友自拍视频| 欧美国产另类| 日韩视频精品在线| 大吊一区二区三区| 国内精品久久久久久久影视简单 | 黄色成人av网| 中文字幕在线看视频国产欧美在线看完整 | www.一区二区三区| 国产男男gay体育生网站| 日韩va欧美va亚洲va久久| 98精品在线视频| 久草网视频在线观看| 亚洲五月综合| 日韩在线视频播放| 丰满少妇高潮一区二区| 妖精视频一区二区三区免费观看| 亚洲成人精品在线| 日韩精品人妻中文字幕有码| 成人爽a毛片免费啪啪红桃视频| 成人短视频app| 久久久精品国产免大香伊| 亚洲欧美激情视频| 精品国产一区二区三区麻豆免费观看完整版 | 欧美有码在线| 精品国免费一区二区三区| 欧美一级特黄aaa| 欧美国产日韩在线视频| 成人国产综合| 欧美性极品少妇| 日本激情视频在线| 深夜福利影院在线观看| 神马电影在线观看| 国产98色在线|日韩| 97神马电影| 精品国产亚洲一区二区麻豆| 九色一区二区| 日本美女一级视频| 91亚洲大成网污www| 精品一区二区国产| 十九岁完整版在线观看好看云免费| 91啪九色porn原创视频在线观看| 欧美成人第一区| 国产福利在线| 亚洲婷婷国产精品电影人久久| 国产福利片一区二区| 先锋成人av| 亚洲免费婷婷| 国产精品无码永久免费888| 国产伦精品一区二区三| 你懂的免费在线观看| 国产精品久久久久久久久久免费看 | 国产亚洲一区二区在线观看| 日韩欧美一区二区三区四区| 免费a级在线播放| 一区二区三区在线影院| 国产大屁股喷水视频在线观看| 成a人片在线观看| 亚洲精品国产视频| 性视频1819p久久| 国产手机视频在线观看| 中文字幕伦理免费在线视频 | 久久国产精品第一页| 亚洲a区在线视频| 日韩一级中文字幕| 亚洲国产激情av| 日韩精品免费一区| 综合日韩av| 91精品久久久久久久99蜜桃| 中文字幕免费高清视频| 日韩www.| 91国产美女视频| 一二区在线观看| www.一区二区| 中文字幕乱码一区二区三区| 亚洲精品久久久久久久蜜桃臀| 亚洲午夜久久久久久久国产| 亚洲+变态+欧美+另类+精品| www.日韩系列| 久草手机在线视频| 国产精品99久久久久久久vr| 日本成人看片网址| а√天堂中文在线资源8| 欧美日免费三级在线| 国产成人av无码精品| 99久久九九| 国产91在线播放精品91| 懂色av一区二区三区四区| 日本一区二区三区dvd视频在线| 狠狠噜天天噜日日噜| 国产精品伊人| 亚洲欧美国产日韩天堂区| 免费在线观看日韩| 国产在线国偷精品免费看| 美女视频网站久久| 成人欧美一区二区三区在线湿哒哒| 天天操天天操天天干| 亚洲蜜臀av乱码久久精品蜜桃| 黄色a级片免费| 国产乱人伦丫前精品视频| 久久色在线播放| 这里只有精品国产| 国产亚洲午夜高清国产拍精品 | 日韩天天综合| 成人蜜桃视频| 天堂va在线| 宅男噜噜噜66一区二区66| 免费看裸体网站| 亚洲资源av| 精品国产一区二区三区四区精华| 黑人极品ⅴideos精品欧美棵| 欧美一区二区三区在| 69成人精品免费视频| 女王人厕视频2ⅴk| 亚洲精品97| 91在线国产电影| 日本精品在线| 9191成人精品久久| 免费成人美女女在线观看| 青青草97国产精品免费观看无弹窗版 | 9人人澡人人爽人人精品| 精品久久久无码人妻字幂| 精品精品视频| 色中色综合影院手机版在线观看| 亚洲av无码国产精品永久一区| 亚洲欧美日韩成人高清在线一区| 欧美视频亚洲图片| 欧美精品首页| 欧美群妇大交群的观看方式| 久久久精品动漫| 国产成人l区| 欧美一区二区视频在线观看2020 | 欧美日高清视频| 国产喷水在线观看| 国产酒店精品激情| 欧美一区二区视频在线播放| 欧美巨大xxxx| 日韩av男人的天堂| a中文在线播放| 欧美日韩高清一区| 欧美成人精品欧美一级私黄| 粉嫩欧美一区二区三区高清影视 | 91吃瓜在线观看| 色豆豆成人网| 欧美成人乱码一区二区三区| 久久综合成人网| 99久久精品情趣| 熟女人妇 成熟妇女系列视频| 日韩精品网站| 成人91视频| 欧美片第一页| 久久成人这里只有精品| 色欲久久久天天天综合网| 色综合久久综合网欧美综合网 | 国产精品草莓在线免费观看| 蜜芽在线免费观看| 亚洲黄页视频免费观看| 亚洲精品一区二区二区| 亚洲激情五月婷婷| 91中文字幕永久在线| 国内精品免费在线观看| 偷拍与自拍一区| 日本wwwwwww| 日韩av一二三| 精品一二三四五区| 国产精品探花在线观看| 97久久夜色精品国产九色| 亚洲女同av| 欧美超级免费视 在线| 三级做a全过程在线观看| 制服丝袜一区二区三区| 西西44rtwww国产精品| 日韩美女视频一区| 性久久久久久久久久| 国产一区在线不卡| 久久久久狠狠高潮亚洲精品| 在线精品视频在线观看高清| 蜜桃传媒一区二区| 亚洲小说春色综合另类电影| 国产极品精品在线观看| free性护士videos欧美| 久久视频精品在线| 国产精品综合网站| 69av在线| 国产视频亚洲视频| 性生交生活影碟片| 欧美欧美欧美欧美| 乱子伦一区二区三区| 午夜视频在线观看一区二区| 国产一区二区播放| 国产精品日日摸夜夜摸av| 一本色道综合久久欧美日韩精品 | av福利在线播放| 日韩黄色高清视频| 狠狠躁夜夜躁av无码中文幕| 91精品国产色综合久久| 一级一级黄色片| 日韩欧美国产激情| 综合激情网五月| 婷婷国产在线综合| 日本亚洲色大成网站www久久| av在线天堂| 9191精品国产综合久久久久久| 波多野结衣在线观看视频| 欧美日韩亚洲精品一区二区三区 | 国产高清免费av在线| 日韩高清a**址| 男人天堂综合网| 亚洲福利视频专区| 全国男人的天堂网| 日韩av影视在线| 亚洲av片一区二区三区| 日韩成人xxxx| 欧美精品少妇| 亚洲男人av电影| 国产主播福利在线| 亚洲日本成人女熟在线观看| 欧美挠脚心网站| 亚洲精品网站在线播放gif| 日本啊v在线| 国产香蕉一区二区三区在线视频| 午夜剧场免费在线观看| 美女主播精品视频一二三四| 97超级碰碰碰| 中文不卡1区2区3区| 国产mv久久久| 成人黄色在线| 国产日韩欧美视频在线| 自拍偷拍亚洲图片| 96成人在线视频| 国产伦理久久久久久妇女| 精品麻豆av| 伊人春色精品| 亚洲国产一区二区三区在线播| 欧美r级电影| 日韩不卡视频一区二区| 99在线热播精品免费99热| 18禁男女爽爽爽午夜网站免费| 日韩电影在线一区二区| 红桃视频 国产| 国产精品一二一区| 亚洲精品女人久久久| 国产亚洲欧美日韩俺去了| 青青青视频在线播放| 欧美破处大片在线视频| 中文在线不卡视频| 幼a在线观看| 欧美激情视频一区二区| 在线天堂中文资源最新版| 国产精品视频26uuu| 国产精品视频一区二区三区| 国产精品一区二| 精品国产91| 台湾无码一区二区| 肉丝袜脚交视频一区二区| 日韩成人精品视频在线观看| av在线不卡免费看| 国产又粗又猛又爽又黄的视频四季| 亚洲欧美激情在线| 亚洲s码欧洲m码国产av| 欧美一区二区三区视频免费| 飘雪影院手机免费高清版在线观看| 色偷偷91综合久久噜噜| 国产在线观看www| 91嫩草在线视频| 久久av免费| 精品国偷自产一区二区三区| 奇米精品一区二区三区四区 | 国产黄色片免费在线观看| 日韩电影网1区2区| 久久精品国产欧美激情| 午夜在线播放| 91精品国产网站| 久久久久毛片免费观看| 欧美日韩国产综合视频在线| 欧美aa国产视频| 冲田杏梨av在线| 97精品国产露脸对白| 成人在线观看小视频| 色婷婷综合久色| 韩国av免费在线| 久久成人人人人精品欧| 日韩毛片一区| 久久伊人一区二区| 国色天香一区二区| 国产精品久久久久久9999| 国产日产精品一区| 欧美一区二区激情视频| 欧美精品一区二区三区视频| www久久日com| 国产欧美日韩精品在线观看| 蜜臀91精品国产高清在线观看| 久久久久久久香蕉| 国产精品夜夜嗨| 午夜黄色福利视频| 欧美日韩一区国产| 800av在线播放| 国产精品videossex久久发布| 九九视频精品在线观看| 91在线国产观看| 日韩欧美亚洲国产| 精品精品欲导航| 亚洲第一图区| 91麻豆蜜桃| 女人天堂亚洲aⅴ在线观看| 免费精品99久久国产综合精品应用| 久久久国产精华| 亚洲s码欧洲m码国产av| 亚洲欧美国产一本综合首页| 麻豆网站免费在线观看| 精品国产电影| 国产美女精品| 粉嫩av蜜桃av蜜臀av| 91久久精品一区二区三| 黄色片视频在线观看| 国产va免费精品高清在线观看| 亚洲资源网你懂的| 粗暴91大变态调教| 亚洲国产高清在线观看视频| 亚洲影视一区二区| 成年人精品视频| www.成人网| 国产视频一视频二| 久久久噜噜噜久噜久久综合| 国产精品久久亚洲| 亚洲成人中文| 日本免费福利视频| 在线观看成人免费视频| 在线播放毛片| 2014亚洲精品| 国产亚洲一级| 亚洲欧美va天堂人熟伦| 欧美日韩视频在线观看一区二区三区 | 成人性生交大片免费看中文网站| 日韩成人高清视频| 精品亚洲va在线va天堂资源站| 欧美最新精品| 中文字幕在线亚洲精品| 国产盗摄一区二区三区| 久久综合成人网| 亚洲视频免费一区| av一级久久| 激情小视频网站| 国产三级精品在线| 国产乱色精品成人免费视频 | 国内精品伊人| 奇米777四色影视在线看| 99国产精品99久久久久久| 最近国语视频在线观看免费播放| 国产精品一区二区在线观看网站 | 国产精品资源在线观看| 国产精品免费av一区二区| 中文字幕成人在线| 超碰成人在线观看| 天天操天天摸天天爽| 一区二区三区四区中文字幕| 欧美新色视频| 亚洲精品欧美日韩专区| 免费在线欧美黄色| 亚洲二区在线播放| 日韩电影在线观看中文字幕| 日本电影久久久| 国产人妻777人伦精品hd| 国产精品嫩草影院av蜜臀| 特黄视频在线观看| 国产在线精品自拍| 免费亚洲视频| 欧美另类视频在线观看| 中文字幕欧美国内| 久9re热视频这里只有精品|