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

攜程機票App KMM iOS工程配置實踐

移動開發 新聞
本文適合于對KMM有一定的了解的iOS開發者,KMM相關資料可參閱Kotlin Multiplatform官網介紹。

作者簡介

Derek,攜程資深研發經理,關注Native技術、跨平臺領域。

前言

KMM(Kotlin Multiplatform Mobile),2022年10月迎來了KMM的beta版,攜程機票也是從KMM開始出道的alpha版本就已在探索。

本文主要圍繞下面幾個方面展開說明:

  • 如何在KMM項目中配置iOS的依賴
  • KMM工程的CI/CD環境搭建和配置
  • 常見的集成問題的解決方法

本文適合于對KMM有一定的了解的iOS開發者,KMM相關資料可參閱Kotlin Multiplatform官網介紹

一、背景

攜程App已有很長的歷史了,在類似這樣一個龐大成熟的App中要引入一套新的跨端框架,最先考慮的就是接入成本。而歷史的跨端框架以及現存的RN、Flutter等,都需要大量的基建工作,最后才能利用上這個跨平臺框架。

通常對于大型的APP引用新的框架,通信本身的屬性肯定是沒問題的,那么最關鍵要解決的就是對現有依賴的處理,像RN和Flutter如果需要對iOS原生API調用,需要從RN和Flutter內部底層增加訪問API,而對于現有成型的一些API或者第三方SDK的API調用,將需要在iOS的工程中寫好對接的接口API才可以實現,而這個工作量是巨大的。而KMM這個跨端框架,正好可以規避這個問題,他只需要通過簡單的配置就可直接調用原有的API,甚至不需要寫額外的路由代碼就可以實現。

二、如何在KMM項目中配置iOS的依賴

針對不同的開發階段,工程的依賴環境也是不一樣的,大致可以分為下面幾種情況:

2.1 只依賴系統框架(項目剛起步、開發完全獨立的框架)

圖片

按照官方的介紹,直接進行邏輯開發,依賴于iOS平臺相關的,在引用API時,只需 import platform.xxx即可,更多內容可參見官方文檔。如:

import platform.UIKit.UIDevice


class IOSPlatform: Platform {
    override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

2.2 有部分API的依賴(一定的代碼積累,但又不想在KMM中重寫已有的API)

圖片

此種情況KMM可以直接依賴原始邏輯,只需要將依賴的文件聲明,做成一個def文件,通過官方提供的cinterop工具將其轉換為KMM內部能調用的API即可。

這里官網是在C interop中介紹的,而這其實也可以直接用到Objective-C中。

方法如下:xxx.def

language = Objective-C
headers = AAA.h BBB.h
compilerOpts = -I/xxx(/xxx為h文件所在目錄)

另外需要將def文件位置告知KMM工程,同時設置包名,具體如下:

compilations["main"].cinterops.create(name) {
    defFile = project.file("src/nativeInterop/cinterop/xxx.def")
    packageName = "com.xxx.ioscall"
}

最終,在KMM調用時,只需要按照正常的kotlin語法調用。(這里能正常import的前提是需要保證def能正常通過cinterop轉換為klib,并會被添加到KMM項目中的External Libraries中)

import com.xxx.ioscall.AAA

攜程機票最開始的做法也是這種方式,同時為了應對API的變更同步,將iOS工程作為KMM的git submodule,這樣def的配置中就可以引用相對路徑下的頭文件,同時也避免了不同的開發人員源文件路徑不同導致的尋址錯誤問題。

這里注意KMM項目中實際無法真實調用,只是做了編譯檢查,真實調用需要到iOS平臺上才可以。

2.3 依賴本地現有/第三方的framework/library

圖片

此種情況方法和上述類似,同樣需要依賴創建一個def,但需要添加一些對framework/library的link配置才可以。有了2中的方式后,還需要增加靜態庫的依賴配置項staticLibraries,如下:

language = Objective-C
package = com.yy.FA
headers = /xxx/TestLocalLibraryCinterop/extframework/FA.framework/Headers/AAA.h
libraryPaths = /xxx/TestLocalLibraryCinterop/extframework/
staticLibraries = FA.framework FB.framework

由于業務的逐漸增多,我們對基礎API也依賴的多了,因而此部分API也是在封裝好的Framework/Library中,故我們第二階段也增加諸如上面對靜態庫的配置。(這里同樣需要注意配置的路徑,最好是相對路徑)

2.4 依賴私有/公用的pods,攜程機票也在開發過程中遇到了基礎部門對iOS工程Cocoapods集成改造,現在也是用此種方式進行的依賴集成。

圖片

這種方式在iOS中是比較成熟的,也是比較方便的,但也是我們在集成時遇到問題較多的,特別是自定義的pods倉庫,而我們項目中依賴的pods比較復雜多樣,涵蓋了源碼、framework,library,swift多種依賴。

如官網上提及的AFNetworing,其實很簡單就可以添加到KMM中,但是用到自建的pods倉庫時,就會遇到一些問題。這里基礎步驟和官網一致,需要對cocoapods中的specRepos、pod等進行配置。如果是私有pods庫,并有依賴靜態庫,具體集成步驟如下:

1)添加cocoapods的相關配置,如下:

cocoapods {
        summary = "Some description for the Shared Module"
        homepage = "https://xxxx.com/xxxx"
        version = "1.0"
        ios.deploymentTarget = "13.0"
        framework {
            baseName = "shared"
        }
        specRepos {
            url("https://github.com/hxxyyangyong/yyspec.git")
        }
        pod("yytestpod"){
            version = "0.1.11"
        }
        useLibraries()
}

這里注意1.7.20 對靜態庫的Link的進行了修復

當低于1.7.20時,會遇到framework無法找到的錯誤 ld: framework not found XXXFrameworkName

2)針對cocoapods生成Def文件時添加配置。

當我們確定哪些pods中的class需要被引用,我們就需要在KMM插件創建def文件的時候進行配置。這一步其實就是前面我們自己創建def的那個過程,這里只不過是通過pods來確定def的文件,最終也都是通過cinterop來進行API的轉換。

這里和普通def的不同點是監聽了def的創建,def的名稱和個數和前面配置cocoapods中的pod是一致的。這個步驟主要配置的是引用的文件,以及引用文件的位置,如果沒有這些設置,如果是對靜態庫的pods,那么此處是不會有Class被轉換進klib的,也就無法在KMM項目中調用了。這里的引用頭文件的路徑,可依賴buildDir的相對目錄進行配置。

gradle.taskGraph.whenReady {
tasks.filter { it.name.startsWith("generateDef") }
    .forEach {
        tasks.named<org.jetbrains.kotlin.gradle.tasks.DefFileTask>(it.name).configure {
            doLast {
                val taskSuffix = this.name.replace("generateDef", "", false)
                val headers = when (taskSuffix) {
                    "Yytestpod" -> "TTDemo.h DebugLibrary.h NSString+librarykmm.h TTDemo+kmm.h NSString+kmm.h"
                    else -> ""
                }
                val compilerOpts = when (taskSuffix) {
                    "Yytestpod" -> "compilerOpts = -I${buildDir}/cocoapods/synthetic/IOS/Pods/yytestpod/yytestpod/Classes/DebugFramework.framework/Headers -I${buildDir}/cocoapods/synthetic/IOS/Pods/yytestpod/yytestpod/Classes/library/include/DebugLibrary\n"
                        else -> ""
                    }
                    outputFile.writeText(
                        """
            language = Objective-C
            headers = $headers
            $compilerOpts
            """.trimIndent()
                    )
                }
            }
        }
}

(這里配置時,需要注意不同版本的Android Studio和KMM插件以及IDEA,build中cocoapods子目錄有差異,低版本會多一層moduleName目錄層級)

當配置好這些之后,重新build,可以通過build/cocoapods/defs中的def文件check相關的配置是否正確。

3)build成功后,項目的External Libraries中就會出現對應的klib,如下:

調用API代碼,import包名為cocoapods.xxx.xxx,如下:

``` kotlin 
import cocoapods.yytestpod.TTDemo
class IosGreeting {
fun calctTwoDate() {
         println("Test1:" + TTDemo.callTTDemoCategoryMethod())
     }
 }
```

pods配置可參考我的Demo,pods和def方式可以混用,但需注意依賴的沖突。

2.5 依賴的發布

當解決了上面現有依賴之后,就可以直接調用依賴API了。但是如果有多個KMM項目需要用到這個依賴或者讓代碼和配置更簡潔,就可以把現有依賴做成個單獨依賴的KMM工程,自己有maven倉庫環境的前提下,可以將build的klib產物發布到自己的Maven倉庫。本身KMM就是一個gradle項目,所以這一點很容易做到。

首先只需要在KMM項目中增加Maven倉庫的配置:

publishing {
repositories {
    maven {
        credentials {
            username = "username"
            password = "password"
        }
        url = uri("http://maven.xxx.com/aaa/yy")
    }
}
}

然后可以在Gradle的tasks看到Publish項,執行publish的Task即可發布到Maven倉庫。

圖片

使用依賴時,這里和一般的kotlin項目的配置依賴一樣。(上面發布的klib,在配置時需要區分iosX64和iosArm64指令集,不區分會有klib缺失,實際maven看產物綜合目錄klib也是缺失)

配置如下:

val iosX64Main by getting {
dependencies{
    implementation("groupId:artifactId:iosx64-version:cinterop-packagename@klib")
}
}


val iosArm64Main by getting {
dependencies{
    implementation("groupId:artifactId:iosarm64-version:cinterop-packagename@klib")
}
}

三、KMM工程的CI/CD環境搭建和配置

當前面的流程完成之后,可以得到對應的Framework產物,如果沒有配置相關的CI/CD過程,則需要在本地手動將framework添加到iOS工程。所以我們這里做了一些CI/CD的配置,來簡化這里的Build、Test以及發布集成操作。

這里CI/CD主要分為下面幾個stage:

  • pre: 主要做一些環境的check操作
  • build: 執行KMM工程的build
  • test: 執行KMM工程中的UT
  • upload: 上傳UT的報告(手動執行)
  • deploy: 發布最終的集成產物(手動執行)

3.1 CI/CD環境的搭建

這里由于公司內部現階段無macOS鏡像的服務器,而KMM工程時需要依賴XCode的,故我們這里暫時使用自己的開發機器做成gitlab-runner,供CI/CD使用(使用gitlab-runner前提是工程為gitlab管理)。如果是gitlab環境,倉庫的Setting-CI/CD中有runner的安裝步驟。

安裝:

sudo curl --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64
sudo chmod +x /usr/local/bin/gitlab-runner
cd ~
gitlab-runner install
gitlab-runner start

注冊:

sudo gitlab-runner register --url http://xxx.com/ --registration-token xxx_token

注冊過程中需要注意的:

1. Enter tags for the runner (comma-separated):yy-runner
     此處需要填寫tag,后續設置yaml的tags需要保持一致


 2. Enter an executor: instance, kubernetes, docker-ssh, parallels, shell, docker-ssh+machine, docker+machine, custom, docker, ssh, virtualbox:shell
     此處我們只需要shell即可

最后會在磁盤下etc/gitlab-runner下生成一個config.toml。gitlab的需要識別,需要將此文件中的配配置copy到用戶目錄下的.gitlab-runner/config.toml中,如多個工程中用到直接添加到末尾即可,如:

圖片

最終在Setting-CI/CD-Runners下能看到runner得tag為active即可

3.2 Stage:pre

這里由于我們需要一些環境的依賴,因此我這里做了一下幾個環境的check,我們配置了對幾個依賴項的版本check,當然這里也可以增加一些校驗為安裝的情況下補充安裝的步驟等。

3.3 Stage:build

這個stage我們主要做build,并把build后的產物copy到臨時目錄,供后續stage使用。

這里還需要注意就是由于gradle的項目中存在的local.properties是本地生成的,git上不會存放,所以這里我們需要做一個創建local.properties,并且設置Android SDK DIR的操作,我這里使用的shell文件來做了操作。build的stage:

buildKMM:
    stage: build
    tags:
        - yy-runner
    script:
        - sh ci/createlocalfile.sh
        - ./gradlew shared:build
        - cp -r -f shared/build/fat-framework/release/ ../tempframework

createlocalfile.sh

#!/bin/sh
    scriptDir=$(cd "$(dirname "$0")"; pwd)
    echo $scriptDir
    cd ~
    rootpath=$(echo `pwd`)
    cd "$scriptDir/.."
    touch local.properties
    echo "sdk.dir=$rootpath/Library/Android/sdk" > local.properties

3.4 Stage:test

這一步我們將做的操作是執行UT,包括AndroidTest,CommonTest,iOSTest,并最終把執行Test后的產物copy到指定的臨時目錄,供后續stage使用。

具體腳本如下:

stage: test
tags:
    - yy-runner
script:
    - ./gradlew shared:iosX64Test
    - rm -rf ../reporttemp
    - mkdir ../reporttemp
    - cp -r -f shared/build/reports/ ../reporttemp/${CI_PIPELINE_ID}_${CI_JOB_STARTED_AT}

如果我們只有CommonTest對在CommonMain中寫了UT,沒有使用到平臺相關的API,那么這一步是相對輕松很多,只需要執行 ./gradlew shared:allTest 即可。在普通的iOS工程中,需要UT我們只需創建一個UT的Target,增加UTCase執行就很容易做到這一點。

但在實際在我們的KMM項目中,已經有依賴iOS平臺以及自己項目中的API,如果在iOSTest正常編寫了一些UTTestCase,當實際執行iOSX64Test時,是無法執行通過的,因為這里并不是在iOS系統環境下執行的。所以要先fix這個問題。

而這里要做到在KMM內部執行iOSTest中的TestCase,官方暫時沒有對外公布解決方法,所以只能自己探索。

搜索到了一個可行的方案,讓其Test的Task依賴iOS模擬器在iOS環境中來執行,那么就可以順利實現了KMM內部直接執行iOSTest。

官方也有考慮到UT執行,但是苦于沒有完整對iOSTest的配置的方法。通過文檔查看build目錄下的產物,在build/bin/iosX64/debugTest目錄下就有可執行UT的test.kexe文件,我們就是通過它來實現在KMM內部執行iOS的UTCase。

除了編寫UTCase外,當然還需要iOS的模擬器,借助iOS系統才可以完整的執行UTCase。

解決方案步驟如下:

1)在KMM項目共享代碼的module的同級目錄下增加一個module,并配置build.gradle.kts,如下:

plugins {
    `kotlin-dsl`
}


repositories {
    jcenter()
}

2)增加一個DefaultTask的子類,利用Task的TaskAction來執行iOSTest,內部能執行終端命令,獲取模擬器設備信息,并執行Test.

open class SimulatorTestsTask: DefaultTask() {

        @InputFile
        val testExecutable = project.objects.fileProperty()

        @Input
        val simulatorId = project.objects.property(String::class.java)

        @TaskAction
        fun runTests() {
            val device = simulatorId.get()
            val bootResult = project.exec { commandLine("xcrun", "simctl", "boot", device) }
            try {
                print(testExecutable.get())
                val spawnResult = project.exec { commandLine("xcrun", "simctl", "spawn", device, testExecutable.get()) }
                spawnResult.assertNormalExitValue()

            } finally {
                if (bootResult.exitValue == 0) {
                    project.exec { commandLine("xcrun", "simctl", "shutdown", device) }
                }
            }
        }
    }
    ```

3)將上述Task配置為shared工程中的check的dependsOn項。如下:

kotlin{
        ...
        val testBinary = targets.getByName<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>("iosX64").binaries.getTest("DEBUG")
        val runIosTests by project.tasks.creating(SimulatorTestsTask::class) {
            dependsOn(testBinary.linkTask)
            testExecutable.set(testBinary.outputFile)
            simulatorId.set(deviceName)
        }
        tasks["check"].dependsOn(runIosTests)
        ...
    }

如需單獨執行,可自行單獨配置。

val customIosTest by tasks.creating(Sync::class)
    group = "custom"
    val (deviceName,deviceUDID) = SimulatorHelp.getDeviceNameAndId()
    kotlin.targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetWithSimulatorTests::class.java) {
        testRuns["test"].deviceId = deviceUDID
    }


    val testBinary = kotlin.targets.getByName<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>("iosX64").binaries.getTest("DEBUG")
    val runIosTests by project.tasks.creating(SimulatorTestsTask::class) {
        dependsOn(testBinary.linkTask)
        testExecutable.set(testBinary.outputFile)
        simulatorId.set(deviceName)
    }

如上gradle配置中的testExecutable 和 simulatorId 都是來自外部傳值。

testExecutable這個獲取可從binaries中getTest獲取,如:

val testBinary = targets.getByName<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>("iosX64").binaries.getTest("DEBUG")

simulatorId 可通過如下命令查看。

xcrun simctl list runtimes --json
xcrun simctl list devices --json

為了減少手動查找和在其他人機器上執行的操作,我們可以利用同樣的原理,增加一個Task來獲取執行機器上可用的simulatorId,具體可參見我的Demo中的此文件。

遇到的小問題:如果直接執行,大概率會遇到一個默認模擬器為iPhone 12的問題。可以通過上面的SimulatorHelp輸出的deviceUDID來指定默認執行的模擬器。

val (deviceName,deviceUDID) = SimulatorHelp.getDeviceNameAndId()
    targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetWithSimulatorTests::class.java) {
        testRuns["test"].deviceId = deviceUDID
    }

執行完iOSTest的Task之后,可以在build的日志中看到一些Case的執行輸出。

圖片

3.5 Stage:upload

此步驟主要是上傳前面的測試產物,可以在線查看UT報告。

這里需要額外創建一個工程,用于存放Test的report產物,同時利用gitlab-pages上來查看UT的測試報告。通過前面執行stage:test后,我們已經把test的產物reports下面的全部文件Copy到了臨時目錄,我們這一步只需將臨時目錄下的內容上傳到testreport倉庫。

這里我們做了如下幾個操作:

1)首先將testreport倉庫,并配置開放成gitlab-pages,具體yaml配置如下:

pages:
    stage: build
    script:
        - yum -y install git
        - git status
    artifacts:
        paths:
        - public
    only:
        refs:
        - branches
        changes:
        - public/index.html
    tags:
        - official

2)上傳文件時以當次的pipelineid作為文件夾目錄名

3)創建一個index.html文件,內容為執行每次測試報告目錄下的index.html,每次上傳新的測試結果后,增加指向新傳測試報告的超鏈。

pages的首地址,效果如下:

圖片

通過鏈接即可查看實際測試結果,以及執行時間等信息。

圖片

圖片

圖片

3.6 Stage:deploy

此步驟我們主要是將fat-framework下的framework上傳為pods源代碼倉庫 & push spec到specrepo倉庫。

主要借鑒KMMBridge的思想,但其內部多處和github掛鉤,并不適合公司項目,如果本身就是在github上的項目,也可直接用kmmbridge的模版直接創建項目,也是非常方便,詳見kmmbridge創建的demo

需要創建2個倉庫:

  • pods源代碼倉庫,用于管理每次上傳的framework產物,做版本控制。

初始pods可以自己利用 pod lib create 命令創建。后續的上傳只需覆蓋s.vendored_frameworks中的shared.framework即可,如果有對其他pods的依賴需要添加s.dependency的配置

  • podspec倉庫,管理通過pods源碼倉庫中的spec的版本

其中最關鍵的是podspec的版本不能重復,這里需做自增處理,主要借鑒了KMMBridge中的邏輯,我這里是通過腳本處理,最終修改掉podlib中的.podspec文件中的version,并同步替換pods參考下的framework,進行上傳,然后添加給pods倉庫打上和podspec中version一樣的tag。

發布到單獨的specrepo,deploy可分為下面幾大步:

  1. 拉取pods源碼倉庫,替換framework
  2. 修改pods源碼倉庫中的spec文件的version字段
  3. 提交修改文件,給pods倉庫打上tag,和2中的version一致
  4. 將.podspec文件push到spec-repo

在攜程app中用的是自己內部的打包發布平臺,我們只需將framework提交統一的pods源碼倉庫即可,其他步驟只需借助內部打包發布平臺統一處理。最終的deploy流程目前可以做到如下效果:

圖片

四、常見集成問題的解決方法

4.1 配置了pods依賴,但是出現framework無法找到符號的問題

當依賴的pods中為靜態庫(.framework/.a)時,執行linkDebugTestIosX64時會遇到如下錯誤。

圖片

這個問題也是連接器的問題,需要增加framework的相關路徑才可以。pods是依賴Framework,需要的linkerOpts配置如下:

linkerOpts("-framework", "XXXFramework","-F${XXXFrameworkPath}")//.framework

pods是依賴Library,linkerOpts配置如下:

(如果.a前面本身是lib開頭,在這配置時需去除lib,如libAAA.a,只需配置-lAAA)

linkerOpts("-L${LibraryPath}","-lXXXLibrary")//.a

4.2 iOSTest中OC的Category無法找到的問題

不論直接調用Category中的方法,或者間接調用,只要調用堆棧中的方法內部有OC Category的方法,都會導致UT無法Pass。(此問題并不會影響build出fat-framework,同時LinkiOSX64Test也會成功,只牽涉到UTCase的通過率)

其實這個問題其實在正常的iOS項目中也會遇到,根本原因和OC Category的加載機制有關,Category本身是基于runtime的機制,在build期間不會將category中方法加到Class的方法列表中,如果我們需要支持這個調用,那么在iOS項目中我們只需要在Build Setting中的Others Link Flags中增加-ObjC、 -force_load xxx、-all_load的配置,來告知連接器,將OC Category一起加載進來。

同樣在KMM中,我們也需要配置這個屬性,只不過這里沒有顯式Others Link Flags的設置,需要在KotlinNativeTarget的binaries中增加linkerOpts的配置。

如果配置整個iOS Target都需要,可將此屬性配置到binaries.all中,具體如下:

kotlin {
...
targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
    binaries.all {
        linkerOpts("-ObjC")
    }
}
...
}

如果只需在Test中配置,那么將Test的target挑選出來進行設置,如下:

binaries{
getTest(org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG).apply{
    linkerOpts("-ObjC")
}
}

4.3 依賴中含有swift,出現ld: symbol(s) not found for architecture x86_64

如果KMM依賴的項目含有swift相關引用時,按照正常的配置,會遇到無法找到swift相關代碼的符號表,并伴隨出現一系列swift庫無法自動link的warning。具體如下:

圖片

這里主要是swift庫無法自動被Link,需要手動配置好swift的依賴runpath,即可解決類似問題。

getTest(org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG).apply {
    linkerOpts("-L/usr/lib/swift")
    linkerOpts("-rpath","/usr/lib/swift")
    linkerOpts("-L/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/${platform}")
    linkerOpts("-rpath","/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/${platform}")
}

除了上面提到的KMM邏輯層的共享代碼外,UI方面Jetbrains最近正在著力研發Compose Multiplatform,我們團隊已在調研探索中,歡迎有興趣的同學一起加入我們,一起探索,相信不久的將來就會迎來KMM的春天。

責任編輯:張燕妮 來源: 攜程技術
相關推薦

2022-05-13 09:27:55

Widget機票業務App

2022-06-17 09:42:20

開源MMKV攜程機票

2022-06-03 09:21:47

Svelte前端攜程

2020-12-04 14:32:33

AndroidJetpackKotlin

2017-04-11 15:11:52

ABtestABT變量法

2022-06-10 08:35:06

項目數據庫攜程機票

2023-08-25 09:51:21

前端開發

2023-11-13 11:27:58

攜程可視化

2025-06-24 09:51:47

2022-08-06 08:27:41

Trace系統機票前臺微服務架構

2025-06-24 09:44:41

2017-04-11 15:34:41

機票前臺埋點

2022-07-15 12:58:02

鴻蒙攜程華為

2017-03-15 17:38:19

互聯網

2022-09-09 15:49:03

攜程火車票組件化管理優化

2022-06-03 08:58:24

APP攜程流暢度

2022-08-12 08:34:32

攜程數據庫上云

2023-02-08 16:34:05

數據庫工具

2022-07-08 09:38:27

攜程酒店Flutter技術跨平臺整合

2022-07-15 09:20:17

性能優化方案
點贊
收藏

51CTO技術棧公眾號

亚洲欧美久久234| 国产日韩欧美影视| brazzers精品成人一区| 99热播精品免费| 亚洲精品老司机| 久久综合九色欧美狠狠| 国产乱子伦精品无码码专区| 亚洲免费影院| 欧美高清在线视频观看不卡| 国精产品一区二区三区| 97超碰人人草| 国产又爽又黄免费软件| 婷婷亚洲最大| 亚洲精品日韩欧美| 香蕉在线观看视频| 日韩专区视频| 色婷婷精品久久二区二区蜜臀av | 男人天堂综合| 国产伦精一区二区三区| 国产精品成人播放| 日本免费观看视| 亚洲精品国产成人影院| 在线精品高清中文字幕| 成人h动漫精品一区| 日韩av综合| 欧美巨大另类极品videosbest | 国产91在线|亚洲| 国产精品亚洲美女av网站| 国产成人无码精品久在线观看| 综合视频在线| 日韩性生活视频| 少妇无套高潮一二三区| 午夜精品影视国产一区在线麻豆| 精品欧美乱码久久久久久| 羞羞的视频在线| 午夜无码国产理论在线| 欧美午夜片在线免费观看| 人妻av中文系列| 黄色羞羞视频在线观看| 亚洲最大的成人av| 日韩中文字幕亚洲精品欧美| 成年人黄视频在线观看| 中文字幕一区二区三区视频| 亚洲精品白虎| 97超碰人人在线| 日本一区二区三区dvd视频在线| 欧美一区二区高清在线观看| 青青免费在线视频| 成人动漫一区二区在线| 国产美女精品久久久| 成人免费公开视频| 五月婷婷伊人网| 欧美成熟视频| 久久69精品久久久久久久电影好| 久久国产波多野结衣| 婷婷中文字幕一区| 理论片在线不卡免费观看| www深夜成人a√在线| 亚洲啊v在线观看| 久热精品视频在线观看一区| 国产免费无码一区二区视频| 亚洲一级黄色| 欧美孕妇毛茸茸xxxx| 久久人人爽人人爽人人片av免费| 日本视频一区二区三区| 成人黄色中文字幕| 亚洲精品网站在线| 久久综合久久综合亚洲| 图片区小说区区亚洲五月| 老司机在线永久免费观看| 亚洲精品日韩综合观看成人91| 国产青草视频在线观看| 九色porny自拍视频在线观看| 日韩欧美一区二区三区久久| 一区二区xxx| 一区二区三区国产好| 日韩电影中文字幕av| 日韩福利在线视频| 国色天香一区二区| 国产成人精品久久| 国产色视频在线| 波多野洁衣一区| 涩涩涩999| 久cao在线| 欧美色播在线播放| 999热精品视频| 校花撩起jk露出白色内裤国产精品 | 成人av手机在线| 久久久久9999亚洲精品| 四虎4hu永久免费入口| 丝袜老师在线| 337p亚洲精品色噜噜噜| 大地资源二中文在线影视观看 | 波多野结衣一区二区三区免费视频| 日韩国产精品亚洲а∨天堂免| 日韩欧美第一区| 91亚洲精品久久久蜜桃借种| 韩国精品福利一区二区三区| 国产一区二区黄| 久久精品久久精品久久| 另类调教123区 | 欧洲福利电影| 久久久视频免费观看| 91福利在线观看视频| 91色.com| 久久福利一区二区| 久草综合在线| 亚洲欧美日韩在线高清直播| 麻豆亚洲av成人无码久久精品| 人人爽香蕉精品| 精品免费日产一区一区三区免费| 成人在线app| 91成人网在线| 日韩精品一区二区三区高清免费| 一区二区三区在线电影| 国产91久久婷婷一区二区| 亚洲a视频在线| 综合在线观看色| 免费看污黄网站| 久久香蕉精品香蕉| 久久久久久久久91| 99国产精品久久久久99打野战| 久久精品亚洲乱码伦伦中文| 3d动漫一区二区三区| 亚洲精品黑牛一区二区三区| zzijzzij亚洲日本成熟少妇| 波多野结衣激情视频| 91麻豆swag| 免费一级特黄特色毛片久久看| 一区二区三区四区视频免费观看| 久久精品一本久久99精品| 国产精品成人久久久| 久久综合精品国产一区二区三区| 欧美二区在线视频| 国产厕拍一区| 亚州欧美日韩中文视频| 日韩在线视频免费| 亚洲成人www| 91精品又粗又猛又爽| 一区在线视频| 国产乱码精品一区二区三区不卡| 国模私拍视频在线播放| 精品国产1区2区3区| 国产精品1234区| 成人97人人超碰人人99| 在线观看一区二区精品视频| 婷婷六月天在线| 国产中文字幕一区二区三区| 国产精品成久久久久三级| 黄色在线网站| 欧美亚洲免费在线一区| 2019男人天堂| 久久av资源网| 青青草视频国产| 国产精品传媒| 欧美做爰性生交视频| 欧美老女人性开放| 欧美中文字幕一区二区三区| 日韩免费av一区| 国产精品一区二区x88av| 欧美中日韩在线| 精品网站aaa| 欧洲永久精品大片ww免费漫画| 国产精品秘入口| 欧美精品在线视频| 欧美成人一二三区| av毛片久久久久**hd| 青青青国产在线视频| 日韩欧美视频| wwwxx欧美| 中国字幕a在线看韩国电影| 在线日韩中文字幕| www.五月婷| 精品久久久久久久久久| 波多野结衣一二三四区| 国产一区二区三区美女| 精品国产免费av| 欧美综合视频| 91在线在线观看| 345成人影院| 久久精品中文字幕免费mv| 国产 欧美 自拍| 日本福利一区二区| 久久精品一区二区三| 久久免费午夜影院| 五月天婷婷在线观看视频| 99热精品在线观看| 伊人色综合久久天天五月婷| 国产精伦一区二区三区| 国产精品久久久久久网站| 污视频网站免费在线观看| 亚洲人高潮女人毛茸茸| 亚洲第一成人av| 在线观看日韩毛片| 久热精品在线观看| 国产精品毛片a∨一区二区三区| 波多野结衣办公室双飞| 另类综合日韩欧美亚洲| 狠狠爱免费视频| 午夜精品影院| 在线观看日韩av| 992tv成人免费观看| 亚洲一二av| 国产精品免费在线免费| 欧美一级鲁丝片| 欧美日韩第一页| 日本a在线播放| 亚洲欧洲午夜一线一品| 欧美视频一二区| 欧美一区二区三区性视频| 国产一卡二卡三卡| 性做久久久久久久免费看| 精品自拍偷拍视频| 中文乱码免费一区二区| 精品夜夜澡人妻无码av| 成人午夜电影网站| 欧美污在线观看| 精品一区二区三区免费观看| 国产视频一区二区视频| 亚洲综合社区| 九色在线视频观看| 136国产福利精品导航网址| 中文字幕精品在线播放| 欧美岛国激情| 一区二区在线观| 日本不卡二三区| 日韩av电影在线观看| 国产91精品对白在线播放| 精品网站在线看| 久久久久久久久久久久久久久久久久久久| 成人区精品一区二区| 欧美黄色一级| 99在线视频免费观看| 日韩视频一区二区三区四区| 成人写真福利网| 九九99久久精品在免费线bt| 成人免费网视频| 年轻的保姆91精品| 91青青草免费在线看| 国产一区二区三区国产精品| 91精品免费视频| 国产精品18| 亚洲xxx自由成熟| 日本成人精品| 成人91视频| 国产成人一二片| 久久资源av| 精品一区av| 一本一道久久a久久综合精品| 欧美xxxxx视频| 粉嫩av一区二区三区天美传媒 | 国产一区二区av| 91精品国产91久久久久游泳池| 在线视频欧美日韩精品| 欧美a免费在线| 欧美成人精品不卡视频在线观看| 激情网站在线| 欧美在线视频网站| 国产精品久久久久77777丨| 91精品国产综合久久香蕉最新版 | 久久男人天堂| 欧美一二三视频| 国产精品亚洲成在人线| 亚洲xxx大片| 色88888久久久久久影院| 欧美在线3区| 围产精品久久久久久久| 无码 制服 丝袜 国产 另类| 久久av在线| 在线能看的av网站| 成人综合激情网| 国产激情在线免费观看| 自拍偷拍国产精品| 自拍偷拍欧美亚洲| 精品视频在线视频| 亚洲成人黄色片| 亚洲色图美腿丝袜| 含羞草www国产在线视频| 欧美极品少妇xxxxx| 香蕉成人av| 成人黄色在线免费观看| 国产a久久精品一区二区三区| 警花观音坐莲激情销魂小说| 中文一区在线| 九九九九九九九九| 99这里只有久久精品视频| 在线观看天堂av| 精品福利一区二区| 92久久精品一区二区| 日韩精品视频在线观看免费| 国产精品一卡二卡三卡| 青青草99啪国产免费| 日韩第一区第二区| 日韩欧美三级一区二区| 欧美激情视频一区二区三区在线播放| 少妇人妻在线视频| 国内精品久久久久影院色| 国产免费一区二区三区最新6| 国产精品卡一卡二卡三| 精品国产乱码一区二区| 欧美一级黄色片| 91在线导航| 欧美一级视频一区二区| 136国产福利精品导航网址应用| 台湾成人av| 亚洲欧美春色| 深田咏美中文字幕| 亚洲日穴在线视频| 国产情侣小视频| 日韩精品极品视频| 亚洲性图自拍| 成人在线观看视频网站| 精品国产一区二区三区久久久樱花| 隔壁人妻偷人bd中字| 国产在线视视频有精品| 成人免费视频入口| 色婷婷国产精品| 天天干天天摸天天操| 欧美激情久久久| 国产亚洲字幕| 中国老女人av| 国内外成人在线| 国产一区二区三区视频播放| 日韩精品成人一区二区三区| 久久久视频精品| 亚洲成人看片| 久久这里精品国产99丫e6| 一区二区三区四区五区精品视频| 中文字幕99页| 亚洲一卡二卡三卡四卡五卡| 国产伦一区二区| 久久九九国产精品怡红院 | 日本视频在线免费| 欧美午夜理伦三级在线观看| 国产日韩精品在线看| 日本精品视频在线| 狠狠色狠狠色综合婷婷tag| 国产aaa一级片| 久久久午夜精品理论片中文字幕| 亚洲免费激情视频| 精品网站999www| 中文在线а√天堂| 欧美亚洲另类在线一区二区三区| 欧美亚洲三级| www.99热| 欧美日韩国产片| av在线free| 国产精品一区而去| 99综合在线| 午夜时刻免费入口| 欧美三级蜜桃2在线观看| 欧美日本高清| 96久久精品| 99综合精品| 国产精品久久久久久久av| 在线不卡欧美精品一区二区三区| 国产视频中文字幕在线观看| 国产精品久久久久久久免费大片| 亚洲精品综合| 免费一级做a爰片久久毛片潮| 欧美日韩中字一区| 曰本三级在线| 精品久久久久久一区| 日韩高清不卡一区二区三区| 欧美丰满熟妇bbbbbb| 亚洲精品一线二线三线| 欧亚一区二区| 在线观看福利一区| 成人午夜又粗又硬又大| 久久久久久久久久久影院| 少妇av一区二区三区| 午夜免费欧美电影| 97成人在线观看视频| 国产精品国产三级国产普通话三级 | av一区二区三区在线| 日韩黄色片网站| 久久国产精品久久久久| 日韩av三区| 欧美激情国内自拍| 黄色一区二区在线| 91精品专区| 国产伦精品一区二区三区四区免费 | 国产精品综合不卡av| 在线免费av网站| 欧美色图在线观看| 视频一区二区三区不卡| 国产精品加勒比| 蜜桃视频第一区免费观看| 久久精品视频9| 日韩视频中文字幕| 欧美交a欧美精品喷水| 香蕉视频999| 欧美日韩国产在线| 中文字幕在线观看网站| 日韩av在线电影观看| 成人av在线电影| 国产乱码精品一区二区三区精东| 91黑丝高跟在线| 午夜精品久久久久99热蜜桃导演| 国产三级在线观看完整版|