如何在iOS项目中添加两个或多个Kotlin Native模块

9

简洁版

如何在iOS项目中添加两个或多个Kotlin Native模块,而不会出现“重复符号”错误?

详细问题

假设一个多模块KMP项目,其中存在一个Android本地应用程序和一个iOS本地应用程序以及两个通用模块来保存共享的Kotlin代码。

.
├── android
│   └── app
├── common
│   ├── moduleA
│   └── moduleB
├── ios
│   └── app

模块 A 包含一个数据类 HelloWorld,并且没有依赖任何其他模块:

package hello.world.modulea

data class HelloWorld(
    val message: String
)

模块B包含HelloWorld类的扩展函数,因此它依赖于模块A:

package hello.world.moduleb

import hello.world.modulea.HelloWorld

fun HelloWorld.egassem() = message.reversed()

这些模块的 build.gradle 配置如下:

  • 模块 A
apply plugin: "org.jetbrains.kotlin.multiplatform"
apply plugin: "org.jetbrains.kotlin.native.cocoapods"

…

kotlin {
    targets {
        jvm("android")

        def iosClosure = {
            binaries {
                framework("moduleA")
            }
        }
        if (System.getenv("SDK_NAME")?.startsWith("iphoneos")) {…}
    }

    cocoapods {…}

    sourceSets {
        commonMain.dependencies {
            implementation "org.jetbrains.kotlin:kotlin-stdlib-common:1.3.72"
        }
        androidMain.dependencies {
            implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.72"
        }
        iosMain.dependencies {
        }
    }
}
  • 模块 B
apply plugin: "org.jetbrains.kotlin.multiplatform"
apply plugin: "org.jetbrains.kotlin.native.cocoapods"
…

kotlin {
    targets {
        jvm("android")

        def iosClosure = {
            binaries {
                framework("moduleB")
            }
        }
        if (System.getenv("SDK_NAME")?.startsWith("iphoneos")) {…}
    }

    cocoapods {…}

    sourceSets {
        commonMain.dependencies {
            implementation "org.jetbrains.kotlin:kotlin-stdlib-common:1.3.72"
            implementation project(":common:moduleA")
        }
        androidMain.dependencies {
            implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.72"
        }
        iosMain.dependencies {
        }
    }
}

看起来非常简单,如果我将安卓的gradle构建依赖配置如下,它甚至能够在安卓上运行:

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.72"
    implementation project(":common:moduleA")
    implementation project(":common:moduleB")
}

然而,这似乎不是在iOS上组织多个模块的正确方法,因为运行./gradlew podspec后,我得到了BUILD SUCCESSFUL的预期结果,并且下面列出了相应的pods:
pod 'moduleA', :path => '…/HelloWorld/common/moduleA'
pod 'moduleB', :path => '…/HelloWorld/common/moduleB'

即使我运行了 pod install 命令,也会输出成功信息 Pod installation complete! There are 2 dependencies from the Podfile and 2 total pods installed.,在 Xcode 的 Pods 部分中正确显示了模块 A 和模块 B。

但是,如果我尝试构建 iOS 项目,就会出现以下错误:

Ld …/Hello_World-…/Build/Products/Debug-iphonesimulator/Hello\ World.app/Hello\ World normal x86_64 (in target 'Hello World' from project 'Hello World')
    cd …/HelloWorld/ios/app
…
duplicate symbol '_ktypew:kotlin.Any' in:
    …/HelloWorld/common/moduleA/build/cocoapods/framework/moduleA.framework/moduleA(result.o)
    …/HelloWorld/common/moduleB/build/cocoapods/framework/moduleB.framework/moduleB(result.o)
… a lot of duplicate symbol more …
duplicate symbol '_kfun:kotlin.throwOnFailure$stdlib@kotlin.Result<#STAR>.()' in:
    …/HelloWorld/common/moduleA/build/cocoapods/framework/moduleA.framework/moduleA(result.o)
    …/HelloWorld/common/moduleB/build/cocoapods/framework/moduleB.framework/moduleB(result.o)
ld: 9928 duplicate symbols for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

我对iOS的了解不多,所以在我的生疏眼里,每个模块似乎都添加了自己版本的内容,而没有使用某种分辨率策略来共享它。

如果我仅使用模块A,则代码可以正常工作和运行,因此我知道代码本身是正确的,问题在于如何管理多个模块,那么问题是如何在iOS上同时添加两个模块(模块A和模块B)并使其正常工作?

附言

我尽可能地缩减了代码,只保留我认为是问题源头的部分,但是完整的代码可在这里获得,如果您想检查片段中是否有遗漏的任何内容或者想运行并尝试解决问题...

2个回答

5

多个Kotlin框架可能会比较棘手,但应该可以在1.3.70版本中正常工作,我看到您正在使用该版本。

问题似乎是两个框架都是静态的,在目前的1.3.70版本中这是一个问题,所以它无法工作。(在1.4.0版本中应该已经更新了)默认情况下,Cocoapods插件将框架设置为静态的,这将无法工作。我不知道如何更改Cocoapods以将其设置为动态,但我已经测试过在没有Cocoapods的情况下构建,并在Gradle任务中使用isStatic变量,已经成功编译了一个iOS项目。像这样:

binaries {
    framework("moduleA"){
        isStatic = false
    }
}

目前,您可以使用此方法绕过问题,使用上面的代码并创建一个任务来构建框架(这里提供了示例)。

另一个值得注意的事情是,在iOS端,HelloWorld类将显示为两个单独的类,尽管两者都来自moduleA。这是另一种使用多个Kotlin框架的奇怪情况,但我认为在这种情况下扩展仍然有效,因为您正在返回一个字符串。

我实际上刚刚写了一篇关于多个Kotlin框架的博客文章,如果您想了解更多其他问题,它可能会有所帮助。https://touchlab.co/multiple-kotlin-frameworks-in-application/

编辑: 看起来还有一个isStatic变量,所以将其设置为isStatic = false

总之,您目前不能在同一个iOS项目中拥有多个静态Kotlin框架。使用isStatic = false将它们设置为非静态。


非常感谢,您的回答和补充材料正是我所需要的。 您提出的问题很相关,因此我正在遵循以下策略: 保持模块化(问题的目标) 对于Android,我保留了普通的gradle模块 对于本地模块,我会添加一个“fat”模块,没有代码,并且使用gradle配置将所有其他模块的代码一起使用并生成单个框架,解决类前缀问题和重复符号问题。 - ademar111190
希望 Kotlin 的未来版本能够提供更好的本地多模块支持,这样我就可以摆脱臃肿的模块并使用已经组织好的模块。现在让我们先使用它吧 :D - ademar111190
@ademar111190,在你的情况下,“fat”模块方法有什么问题? - Svyatoslav Scherbina
谢谢你的回答。作为一名开始接触iOS的Android开发人员,我也遇到了同样的问题。对于静态/动态解决方案,您必须像文章建议的那样决定是否可以容忍名称空间问题。我个人不喜欢将模块名作为前缀,因此我最终保留了Android的模块,但使其中一个模块传递性地导出第二个模块的依赖项。 framework { baseName = "moduleA" export("moduleB") transitiveExport = true } - Michael Bakogiannis
使用 Kotlin 1.7.20,几乎没有任何变化,我们仍然会为一个 Kotlin 类生成 2 个 Swift 类。 - Farid
@MichaelBakogiannis,您能详细说明一下吗?即使使用您的实现方式,Kotlin类在不同的框架中仍会被解释为两个不同的Swift类。您真的测试过您的实现方式还是只是理论? - Farid

2
然而,如果我尝试构建iOS项目,我会遇到以下错误:
这个特定的错误是已知问题。多个调试静态框架与编译器缓存不兼容。
因此,为了解决问题,您可以通过将以下行放入您的gradle.properties来禁用编译器缓存:
kotlin.native.cacheKind=none

或者通过将以下片段添加到您的Gradle构建脚本中,使框架动态化:
kotlin {
    targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
        binaries.withType<org.jetbrains.kotlin.gradle.plugin.mpp.Framework> {
            isStatic = false
        }
    }
}

请参阅https://youtrack.jetbrains.com/issue/KT-42254了解更多详情。 对于多个框架的当前行为,可能对于原始主题发起人来说并没有太多意义,我在此仅为任何可能遇到相同问题的人提供答案。

我的iOS知识不是很丰富,所以在我未经训练的眼中,看起来每个模块都添加了自己版本的内容,而不是使用某些分辨率策略进行共享。

这正是目前的工作方式。但是,每个框架中的“内容版本”都放置在单独的独立命名空间中,因此不应该存在链接错误,你所遇到的问题是一个bug。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接