暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

干货 | 携程机票App KMM iOS工程配置实践

携程技术 2023-05-11
326

作者简介

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的春天。

                                                            【参考文献】


                                                            【推荐阅读】


                                                             “携程技术”公众号

                                                              分享,交流,成长



                                                            文章转载自携程技术,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                                            评论