攜程機(jī)票App KMM iOS工程配置實(shí)踐
作者簡(jiǎn)介
Derek,攜程資深研發(fā)經(jīng)理,關(guān)注Native技術(shù)、跨平臺(tái)領(lǐng)域。
前言
KMM(Kotlin Multiplatform Mobile),2022年10月迎來(lái)了KMM的beta版,攜程機(jī)票也是從KMM開(kāi)始出道的alpha版本就已在探索。
本文主要圍繞下面幾個(gè)方面展開(kāi)說(shuō)明:
- 如何在KMM項(xiàng)目中配置iOS的依賴
- KMM工程的CI/CD環(huán)境搭建和配置
- 常見(jiàn)的集成問(wèn)題的解決方法
本文適合于對(duì)KMM有一定的了解的iOS開(kāi)發(fā)者,KMM相關(guān)資料可參閱Kotlin Multiplatform官網(wǎng)介紹。
一、背景
攜程App已有很長(zhǎng)的歷史了,在類似這樣一個(gè)龐大成熟的App中要引入一套新的跨端框架,最先考慮的就是接入成本。而歷史的跨端框架以及現(xiàn)存的RN、Flutter等,都需要大量的基建工作,最后才能利用上這個(gè)跨平臺(tái)框架。
通常對(duì)于大型的APP引用新的框架,通信本身的屬性肯定是沒(méi)問(wèn)題的,那么最關(guān)鍵要解決的就是對(duì)現(xiàn)有依賴的處理,像RN和Flutter如果需要對(duì)iOS原生API調(diào)用,需要從RN和Flutter內(nèi)部底層增加訪問(wèn)API,而對(duì)于現(xiàn)有成型的一些API或者第三方SDK的API調(diào)用,將需要在iOS的工程中寫(xiě)好對(duì)接的接口API才可以實(shí)現(xiàn),而這個(gè)工作量是巨大的。而KMM這個(gè)跨端框架,正好可以規(guī)避這個(gè)問(wèn)題,他只需要通過(guò)簡(jiǎn)單的配置就可直接調(diào)用原有的API,甚至不需要寫(xiě)額外的路由代碼就可以實(shí)現(xiàn)。
二、如何在KMM項(xiàng)目中配置iOS的依賴
針對(duì)不同的開(kāi)發(fā)階段,工程的依賴環(huán)境也是不一樣的,大致可以分為下面幾種情況:
2.1 只依賴系統(tǒng)框架(項(xiàng)目剛起步、開(kāi)發(fā)完全獨(dú)立的框架)
按照官方的介紹,直接進(jìn)行邏輯開(kāi)發(fā),依賴于iOS平臺(tái)相關(guān)的,在引用API時(shí),只需 import platform.xxx即可,更多內(nèi)容可參見(jiàn)官方文檔。如:
import platform.UIKit.UIDevice
class IOSPlatform: Platform {
override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}
2.2 有部分API的依賴(一定的代碼積累,但又不想在KMM中重寫(xiě)已有的API)
此種情況KMM可以直接依賴原始邏輯,只需要將依賴的文件聲明,做成一個(gè)def文件,通過(guò)官方提供的cinterop工具將其轉(zhuǎn)換為KMM內(nèi)部能調(diào)用的API即可。
這里官網(wǎng)是在C interop中介紹的,而這其實(shí)也可以直接用到Objective-C中。
方法如下:xxx.def
language = Objective-C
headers = AAA.h BBB.h
compilerOpts = -I/xxx(/xxx為h文件所在目錄)
另外需要將def文件位置告知KMM工程,同時(shí)設(shè)置包名,具體如下:
compilations["main"].cinterops.create(name) {
defFile = project.file("src/nativeInterop/cinterop/xxx.def")
packageName = "com.xxx.ioscall"
}
最終,在KMM調(diào)用時(shí),只需要按照正常的kotlin語(yǔ)法調(diào)用。(這里能正常import的前提是需要保證def能正常通過(guò)cinterop轉(zhuǎn)換為klib,并會(huì)被添加到KMM項(xiàng)目中的External Libraries中)
import com.xxx.ioscall.AAA
攜程機(jī)票最開(kāi)始的做法也是這種方式,同時(shí)為了應(yīng)對(duì)API的變更同步,將iOS工程作為KMM的git submodule,這樣def的配置中就可以引用相對(duì)路徑下的頭文件,同時(shí)也避免了不同的開(kāi)發(fā)人員源文件路徑不同導(dǎo)致的尋址錯(cuò)誤問(wèn)題。
這里注意KMM項(xiàng)目中實(shí)際無(wú)法真實(shí)調(diào)用,只是做了編譯檢查,真實(shí)調(diào)用需要到iOS平臺(tái)上才可以。
2.3 依賴本地現(xiàn)有/第三方的framework/library
此種情況方法和上述類似,同樣需要依賴創(chuàng)建一個(gè)def,但需要添加一些對(duì)framework/library的link配置才可以。有了2中的方式后,還需要增加靜態(tài)庫(kù)的依賴配置項(xiàng)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
由于業(yè)務(wù)的逐漸增多,我們對(duì)基礎(chǔ)API也依賴的多了,因而此部分API也是在封裝好的Framework/Library中,故我們第二階段也增加諸如上面對(duì)靜態(tài)庫(kù)的配置。(這里同樣需要注意配置的路徑,最好是相對(duì)路徑)
2.4 依賴私有/公用的pods,攜程機(jī)票也在開(kāi)發(fā)過(guò)程中遇到了基礎(chǔ)部門(mén)對(duì)iOS工程Cocoapods集成改造,現(xiàn)在也是用此種方式進(jìn)行的依賴集成。
這種方式在iOS中是比較成熟的,也是比較方便的,但也是我們?cè)诩蓵r(shí)遇到問(wèn)題較多的,特別是自定義的pods倉(cāng)庫(kù),而我們項(xiàng)目中依賴的pods比較復(fù)雜多樣,涵蓋了源碼、framework,library,swift多種依賴。
如官網(wǎng)上提及的AFNetworing,其實(shí)很簡(jiǎn)單就可以添加到KMM中,但是用到自建的pods倉(cāng)庫(kù)時(shí),就會(huì)遇到一些問(wèn)題。這里基礎(chǔ)步驟和官網(wǎng)一致,需要對(duì)cocoapods中的specRepos、pod等進(jìn)行配置。如果是私有pods庫(kù),并有依賴靜態(tài)庫(kù),具體集成步驟如下:
1)添加cocoapods的相關(guān)配置,如下:
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 對(duì)靜態(tài)庫(kù)的Link的進(jìn)行了修復(fù)。
當(dāng)?shù)陀?.7.20時(shí),會(huì)遇到framework無(wú)法找到的錯(cuò)誤 ld: framework not found XXXFrameworkName
2)針對(duì)cocoapods生成Def文件時(shí)添加配置。
當(dāng)我們確定哪些pods中的class需要被引用,我們就需要在KMM插件創(chuàng)建def文件的時(shí)候進(jìn)行配置。這一步其實(shí)就是前面我們自己創(chuàng)建def的那個(gè)過(guò)程,這里只不過(guò)是通過(guò)pods來(lái)確定def的文件,最終也都是通過(guò)cinterop來(lái)進(jìn)行API的轉(zhuǎn)換。
這里和普通def的不同點(diǎn)是監(jiān)聽(tīng)了def的創(chuàng)建,def的名稱和個(gè)數(shù)和前面配置cocoapods中的pod是一致的。這個(gè)步驟主要配置的是引用的文件,以及引用文件的位置,如果沒(méi)有這些設(shè)置,如果是對(duì)靜態(tài)庫(kù)的pods,那么此處是不會(huì)有Class被轉(zhuǎn)換進(jìn)klib的,也就無(wú)法在KMM項(xiàng)目中調(diào)用了。這里的引用頭文件的路徑,可依賴buildDir的相對(duì)目錄進(jìn)行配置。
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()
)
}
}
}
}
(這里配置時(shí),需要注意不同版本的Android Studio和KMM插件以及IDEA,build中cocoapods子目錄有差異,低版本會(huì)多一層moduleName目錄層級(jí))
當(dāng)配置好這些之后,重新build,可以通過(guò)build/cocoapods/defs中的def文件check相關(guān)的配置是否正確。
3)build成功后,項(xiàng)目的External Libraries中就會(huì)出現(xiàn)對(duì)應(yīng)的klib,如下:
調(diào)用API代碼,import包名為cocoapods.xxx.xxx,如下:
``` kotlin
import cocoapods.yytestpod.TTDemo
class IosGreeting {
fun calctTwoDate() {
println("Test1:" + TTDemo.callTTDemoCategoryMethod())
}
}
```
pods配置可參考我的Demo,pods和def方式可以混用,但需注意依賴的沖突。
2.5 依賴的發(fā)布
當(dāng)解決了上面現(xiàn)有依賴之后,就可以直接調(diào)用依賴API了。但是如果有多個(gè)KMM項(xiàng)目需要用到這個(gè)依賴或者讓代碼和配置更簡(jiǎn)潔,就可以把現(xiàn)有依賴做成個(gè)單獨(dú)依賴的KMM工程,自己有maven倉(cāng)庫(kù)環(huán)境的前提下,可以將build的klib產(chǎn)物發(fā)布到自己的Maven倉(cāng)庫(kù)。本身KMM就是一個(gè)gradle項(xiàng)目,所以這一點(diǎn)很容易做到。
首先只需要在KMM項(xiàng)目中增加Maven倉(cāng)庫(kù)的配置:
publishing {
repositories {
maven {
credentials {
username = "username"
password = "password"
}
url = uri("http://maven.xxx.com/aaa/yy")
}
}
}
然后可以在Gradle的tasks看到Publish項(xiàng),執(zhí)行publish的Task即可發(fā)布到Maven倉(cāng)庫(kù)。
使用依賴時(shí),這里和一般的kotlin項(xiàng)目的配置依賴一樣。(上面發(fā)布的klib,在配置時(shí)需要區(qū)分iosX64和iosArm64指令集,不區(qū)分會(huì)有klib缺失,實(shí)際maven看產(chǎn)物綜合目錄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環(huán)境搭建和配置
當(dāng)前面的流程完成之后,可以得到對(duì)應(yīng)的Framework產(chǎn)物,如果沒(méi)有配置相關(guān)的CI/CD過(guò)程,則需要在本地手動(dòng)將framework添加到iOS工程。所以我們這里做了一些CI/CD的配置,來(lái)簡(jiǎn)化這里的Build、Test以及發(fā)布集成操作。
這里CI/CD主要分為下面幾個(gè)stage:
- pre: 主要做一些環(huán)境的check操作
- build: 執(zhí)行KMM工程的build
- test: 執(zhí)行KMM工程中的UT
- upload: 上傳UT的報(bào)告(手動(dòng)執(zhí)行)
- deploy: 發(fā)布最終的集成產(chǎn)物(手動(dòng)執(zhí)行)
3.1 CI/CD環(huán)境的搭建
這里由于公司內(nèi)部現(xiàn)階段無(wú)macOS鏡像的服務(wù)器,而KMM工程時(shí)需要依賴XCode的,故我們這里暫時(shí)使用自己的開(kāi)發(fā)機(jī)器做成gitlab-runner,供CI/CD使用(使用gitlab-runner前提是工程為gitlab管理)。如果是gitlab環(huán)境,倉(cāng)庫(kù)的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
注冊(cè):
sudo gitlab-runner register --url http://xxx.com/ --registration-token xxx_token
注冊(cè)過(guò)程中需要注意的:
1. Enter tags for the runner (comma-separated):yy-runner
此處需要填寫(xiě)tag,后續(xù)設(shè)置yaml的tags需要保持一致
2. Enter an executor: instance, kubernetes, docker-ssh, parallels, shell, docker-ssh+machine, docker+machine, custom, docker, ssh, virtualbox:shell
此處我們只需要shell即可
最后會(huì)在磁盤(pán)下etc/gitlab-runner下生成一個(gè)config.toml。gitlab的需要識(shí)別,需要將此文件中的配配置copy到用戶目錄下的.gitlab-runner/config.toml中,如多個(gè)工程中用到直接添加到末尾即可,如:
最終在Setting-CI/CD-Runners下能看到runner得tag為active即可
3.2 Stage:pre
這里由于我們需要一些環(huán)境的依賴,因此我這里做了一下幾個(gè)環(huán)境的check,我們配置了對(duì)幾個(gè)依賴項(xiàng)的版本check,當(dāng)然這里也可以增加一些校驗(yàn)為安裝的情況下補(bǔ)充安裝的步驟等。
3.3 Stage:build
這個(gè)stage我們主要做build,并把build后的產(chǎn)物copy到臨時(shí)目錄,供后續(xù)stage使用。
這里還需要注意就是由于gradle的項(xiàng)目中存在的local.properties是本地生成的,git上不會(huì)存放,所以這里我們需要做一個(gè)創(chuàng)建local.properties,并且設(shè)置Android SDK DIR的操作,我這里使用的shell文件來(lái)做了操作。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
這一步我們將做的操作是執(zhí)行UT,包括AndroidTest,CommonTest,iOSTest,并最終把執(zhí)行Test后的產(chǎn)物copy到指定的臨時(shí)目錄,供后續(xù)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對(duì)在CommonMain中寫(xiě)了UT,沒(méi)有使用到平臺(tái)相關(guān)的API,那么這一步是相對(duì)輕松很多,只需要執(zhí)行 ./gradlew shared:allTest 即可。在普通的iOS工程中,需要UT我們只需創(chuàng)建一個(gè)UT的Target,增加UTCase執(zhí)行就很容易做到這一點(diǎn)。
但在實(shí)際在我們的KMM項(xiàng)目中,已經(jīng)有依賴iOS平臺(tái)以及自己項(xiàng)目中的API,如果在iOSTest正常編寫(xiě)了一些UTTestCase,當(dāng)實(shí)際執(zhí)行iOSX64Test時(shí),是無(wú)法執(zhí)行通過(guò)的,因?yàn)檫@里并不是在iOS系統(tǒng)環(huán)境下執(zhí)行的。所以要先f(wàn)ix這個(gè)問(wèn)題。
而這里要做到在KMM內(nèi)部執(zhí)行iOSTest中的TestCase,官方暫時(shí)沒(méi)有對(duì)外公布解決方法,所以只能自己探索。
搜索到了一個(gè)可行的方案,讓其Test的Task依賴iOS模擬器在iOS環(huán)境中來(lái)執(zhí)行,那么就可以順利實(shí)現(xiàn)了KMM內(nèi)部直接執(zhí)行iOSTest。
官方也有考慮到UT執(zhí)行,但是苦于沒(méi)有完整對(duì)iOSTest的配置的方法。通過(guò)文檔查看build目錄下的產(chǎn)物,在build/bin/iosX64/debugTest目錄下就有可執(zhí)行UT的test.kexe文件,我們就是通過(guò)它來(lái)實(shí)現(xiàn)在KMM內(nèi)部執(zhí)行iOS的UTCase。
除了編寫(xiě)UTCase外,當(dāng)然還需要iOS的模擬器,借助iOS系統(tǒng)才可以完整的執(zhí)行UTCase。
解決方案步驟如下:
1)在KMM項(xiàng)目共享代碼的module的同級(jí)目錄下增加一個(gè)module,并配置build.gradle.kts,如下:
plugins {
`kotlin-dsl`
}
repositories {
jcenter()
}
2)增加一個(gè)DefaultTask的子類,利用Task的TaskAction來(lái)執(zhí)行iOSTest,內(nèi)部能執(zhí)行終端命令,獲取模擬器設(shè)備信息,并執(zhí)行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項(xiàng)。如下:
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)
...
}
如需單獨(dú)執(zhí)行,可自行單獨(dú)配置。
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 都是來(lái)自外部傳值。
testExecutable這個(gè)獲取可從binaries中g(shù)etTest獲取,如:
val testBinary = targets.getByName<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>("iosX64").binaries.getTest("DEBUG")
simulatorId 可通過(guò)如下命令查看。
xcrun simctl list runtimes --json
xcrun simctl list devices --json
為了減少手動(dòng)查找和在其他人機(jī)器上執(zhí)行的操作,我們可以利用同樣的原理,增加一個(gè)Task來(lái)獲取執(zhí)行機(jī)器上可用的simulatorId,具體可參見(jiàn)我的Demo中的此文件。
遇到的小問(wèn)題:如果直接執(zhí)行,大概率會(huì)遇到一個(gè)默認(rèn)模擬器為iPhone 12的問(wèn)題??梢酝ㄟ^(guò)上面的SimulatorHelp輸出的deviceUDID來(lái)指定默認(rèn)執(zhí)行的模擬器。
val (deviceName,deviceUDID) = SimulatorHelp.getDeviceNameAndId()
targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetWithSimulatorTests::class.java) {
testRuns["test"].deviceId = deviceUDID
}
執(zhí)行完iOSTest的Task之后,可以在build的日志中看到一些Case的執(zhí)行輸出。
3.5 Stage:upload
此步驟主要是上傳前面的測(cè)試產(chǎn)物,可以在線查看UT報(bào)告。
這里需要額外創(chuàng)建一個(gè)工程,用于存放Test的report產(chǎn)物,同時(shí)利用gitlab-pages上來(lái)查看UT的測(cè)試報(bào)告。通過(guò)前面執(zhí)行stage:test后,我們已經(jīng)把test的產(chǎn)物reports下面的全部文件Copy到了臨時(shí)目錄,我們這一步只需將臨時(shí)目錄下的內(nèi)容上傳到testreport倉(cāng)庫(kù)。
這里我們做了如下幾個(gè)操作:
1)首先將testreport倉(cāng)庫(kù),并配置開(kāi)放成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)上傳文件時(shí)以當(dāng)次的pipelineid作為文件夾目錄名
3)創(chuàng)建一個(gè)index.html文件,內(nèi)容為執(zhí)行每次測(cè)試報(bào)告目錄下的index.html,每次上傳新的測(cè)試結(jié)果后,增加指向新傳測(cè)試報(bào)告的超鏈。
pages的首地址,效果如下:
通過(guò)鏈接即可查看實(shí)際測(cè)試結(jié)果,以及執(zhí)行時(shí)間等信息。
3.6 Stage:deploy
此步驟我們主要是將fat-framework下的framework上傳為pods源代碼倉(cāng)庫(kù) & push spec到specrepo倉(cāng)庫(kù)。
主要借鑒KMMBridge的思想,但其內(nèi)部多處和github掛鉤,并不適合公司項(xiàng)目,如果本身就是在github上的項(xiàng)目,也可直接用kmmbridge的模版直接創(chuàng)建項(xiàng)目,也是非常方便,詳見(jiàn)kmmbridge創(chuàng)建的demo。
需要?jiǎng)?chuàng)建2個(gè)倉(cāng)庫(kù):
- pods源代碼倉(cāng)庫(kù),用于管理每次上傳的framework產(chǎn)物,做版本控制。
初始pods可以自己利用 pod lib create 命令創(chuàng)建。后續(xù)的上傳只需覆蓋s.vendored_frameworks中的shared.framework即可,如果有對(duì)其他pods的依賴需要添加s.dependency的配置
- podspec倉(cāng)庫(kù),管理通過(guò)pods源碼倉(cāng)庫(kù)中的spec的版本
其中最關(guān)鍵的是podspec的版本不能重復(fù),這里需做自增處理,主要借鑒了KMMBridge中的邏輯,我這里是通過(guò)腳本處理,最終修改掉podlib中的.podspec文件中的version,并同步替換pods參考下的framework,進(jìn)行上傳,然后添加給pods倉(cāng)庫(kù)打上和podspec中version一樣的tag。
發(fā)布到單獨(dú)的specrepo,deploy可分為下面幾大步:
- 拉取pods源碼倉(cāng)庫(kù),替換framework
- 修改pods源碼倉(cāng)庫(kù)中的spec文件的version字段
- 提交修改文件,給pods倉(cāng)庫(kù)打上tag,和2中的version一致
- 將.podspec文件push到spec-repo
在攜程app中用的是自己內(nèi)部的打包發(fā)布平臺(tái),我們只需將framework提交統(tǒng)一的pods源碼倉(cāng)庫(kù)即可,其他步驟只需借助內(nèi)部打包發(fā)布平臺(tái)統(tǒng)一處理。最終的deploy流程目前可以做到如下效果:
四、常見(jiàn)集成問(wèn)題的解決方法
4.1 配置了pods依賴,但是出現(xiàn)framework無(wú)法找到符號(hào)的問(wèn)題
當(dāng)依賴的pods中為靜態(tài)庫(kù)(.framework/.a)時(shí),執(zhí)行l(wèi)inkDebugTestIosX64時(shí)會(huì)遇到如下錯(cuò)誤。
這個(gè)問(wèn)題也是連接器的問(wèn)題,需要增加framework的相關(guān)路徑才可以。pods是依賴Framework,需要的linkerOpts配置如下:
linkerOpts("-framework", "XXXFramework","-F${XXXFrameworkPath}")//.framework
pods是依賴Library,linkerOpts配置如下:
(如果.a前面本身是lib開(kāi)頭,在這配置時(shí)需去除lib,如libAAA.a,只需配置-lAAA)
linkerOpts("-L${LibraryPath}","-lXXXLibrary")//.a
4.2 iOSTest中OC的Category無(wú)法找到的問(wèn)題
不論直接調(diào)用Category中的方法,或者間接調(diào)用,只要調(diào)用堆棧中的方法內(nèi)部有OC Category的方法,都會(huì)導(dǎo)致UT無(wú)法Pass。(此問(wèn)題并不會(huì)影響build出fat-framework,同時(shí)LinkiOSX64Test也會(huì)成功,只牽涉到UTCase的通過(guò)率)
其實(shí)這個(gè)問(wèn)題其實(shí)在正常的iOS項(xiàng)目中也會(huì)遇到,根本原因和OC Category的加載機(jī)制有關(guān),Category本身是基于runtime的機(jī)制,在build期間不會(huì)將category中方法加到Class的方法列表中,如果我們需要支持這個(gè)調(diào)用,那么在iOS項(xiàng)目中我們只需要在Build Setting中的Others Link Flags中增加-ObjC、 -force_load xxx、-all_load的配置,來(lái)告知連接器,將OC Category一起加載進(jìn)來(lái)。
同樣在KMM中,我們也需要配置這個(gè)屬性,只不過(guò)這里沒(méi)有顯式Others Link Flags的設(shè)置,需要在KotlinNativeTarget的binaries中增加linkerOpts的配置。
如果配置整個(gè)iOS Target都需要,可將此屬性配置到binaries.all中,具體如下:
kotlin {
...
targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
binaries.all {
linkerOpts("-ObjC")
}
}
...
}
如果只需在Test中配置,那么將Test的target挑選出來(lái)進(jìn)行設(shè)置,如下:
binaries{
getTest(org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG).apply{
linkerOpts("-ObjC")
}
}
4.3 依賴中含有swift,出現(xiàn)ld: symbol(s) not found for architecture x86_64
如果KMM依賴的項(xiàng)目含有swift相關(guān)引用時(shí),按照正常的配置,會(huì)遇到無(wú)法找到swift相關(guān)代碼的符號(hào)表,并伴隨出現(xiàn)一系列swift庫(kù)無(wú)法自動(dòng)link的warning。具體如下:
這里主要是swift庫(kù)無(wú)法自動(dòng)被Link,需要手動(dòng)配置好swift的依賴runpath,即可解決類似問(wèn)題。
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最近正在著力研發(fā)Compose Multiplatform,我們團(tuán)隊(duì)已在調(diào)研探索中,歡迎有興趣的同學(xué)一起加入我們,一起探索,相信不久的將來(lái)就會(huì)迎來(lái)KMM的春天。