如何在 iOS 项目中添加两个或多个 kotlin 原生模块

Posted

技术标签:

【中文标题】如何在 iOS 项目中添加两个或多个 kotlin 原生模块【英文标题】:How to add two or more kotlin native modules on an iOS project 【发布时间】:2020-08-11 15:20:35 【问题描述】:

TL;DR;

而不会出现 duplicate symbols 错误?

详细问题

让我们假设一个多模块 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 
        
    

它看起来非常简单,如果我将 android build gradle 依赖项配置如下,它甚至可以在 android 上运行:

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

但是,这似乎不是在 iOS 上组织多模块的正确方法,因为运行 ./gradlew podspec 我得到了 BUILD SUCCESSFUL 与以下 pod 的预期一样:

pod 'moduleA', :path => '…/HelloWorld/common/moduleA'
pod 'moduleB', :path => '…/HelloWorld/common/moduleB'

即使运行 pod install,一旦 Xcode 在 Pods 部分显示模块 A 和模块 B,我也会得到成功输出 Pod installation complete! There are 2 dependencies from the Podfile and 2 total pods installed.

但是,如果我尝试构建 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 代码可以按预期工作和运行,所以我知道代码本身是正确的,问题是如何管理多个模块,所以问题是,如何添加两者(模块 A和模块 B) 在 iOS 上并让事情正常运行?

附言

我确实尽可能地减少了代码,试图只保留我认为是问题根源的部分,但是,如果您想检查 sn 中缺少的任何内容,可以使用完整的代码 here -ps,或者如果你想运行并尝试解决问题......

【问题讨论】:

【参考方案1】:

多个 Kotlin 框架可能会很棘手,但应该可以在 1.3.70 开始工作,我看到你有。

问题似乎是这两个框架都是静态的,这目前是 1.3.70 中的一个问题,因此它无法正常工作。 (这应该由 1.40 更新)。看起来默认情况下 cocoapods 插件将框架设置为静态的,这将不起作用。我不知道有一种方法可以更改 cocoapods 以将其设置为动态,但我已经测试了没有 cocoapods 并在 gradle 任务中使用 isStatic 变量的构建,并且已经获得了一个 iOS 项目来编译。比如:

binaries 
    framework("moduleA")
        isStatic = false
    

现在您可以通过使用上面的代码并创建一个任务来构建框架来解决此问题(here's an example)

另一件值得注意的事情是,在 iOS 端,HelloWorld 类将显示为两个独立的类,尽管它们都来自 moduleA。这是多个 Kotlin 框架的另一种奇怪情况,但我认为扩展在这种情况下仍然可以工作,因为您要返回一个字符串。

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

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

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

【讨论】:

非常感谢,您的回答和补充材料正是我想要的。您提出的问题是相关的,考虑到这一点,我正在遵循以下策略:保持模块化(问题的目标)对于 Android,我保留了普通的 gradle 模块对于本机模块,我会添加一个“胖”模块,没有代码和 gradle 配置一起使用所有其他模块代码并生成一个解决类前缀问题和重复符号的单个框架。 希望未来的 kotlin 版本能够更好地支持原生的多模块,这样我就可以摆脱胖模块并使用已经组织好的模块。现在让我们使用它:D @ademar111190 在您的案例中,“胖”模块方法到底有什么问题? 感谢您的回答。作为一名从 iOS 开始的 Android 开发人员,我遇到了同样的问题。使用静态/动态解决方案,您必须决定是否可以按照文章建议的那样处理名称空间问题。我个人不喜欢将模块的名称作为前缀,因此我最终保留了用于 android 的模块,但让其中一个模块可传递地导出第二个模块的依赖项。 framework baseName = "moduleA" export("moduleB") transitiveExport = true 【参考方案2】:

但是,如果我尝试构建 iOS 项目,我会收到以下错误:

此特定错误是一个已知问题。多个调试静态框架与编译器缓存不兼容。

因此,要解决此问题,您可以通过将以下行放入您的 gradle.properties 来禁用编译器缓存:

kotlin.native.cacheKind=none

或通过将以下 sn-p 添加到 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。

【讨论】:

以上是关于如何在 iOS 项目中添加两个或多个 kotlin 原生模块的主要内容,如果未能解决你的问题,请参考以下文章

如何在多平台多项目 Kotlin 构建中向另一个项目的测试添加依赖项

Kotlin singleton:如何从singleton复制对象

如何根据 android studio 中的 listview 项目点击更改活动图像和文本? java 或 kotlin

如何确定 kotlin-multiplatform 项目中的构建类型

Simple Intellij Kotlin项目无法识别LinkedHashMap或其他集合

Kotlin 通用库可在多个 MPP 中重用