如何在模块化/组件化项目中实现ObjCSwift混编?...这里我们重点关注的是如何实现Swift代码和O

在正式开始实践Swift-ObjC混编之前,我们有一些问题是绕不过去的,比如:

如果是在一个AppTarget内部混编的话,当我们在ObjC项目中新建Swift文件时或者在Swift项目中新建ObjC文件时,Xcode都会自动帮你新建一个Objective-Cbridgingheaderfile(当然我们也可以手动创建),我们可以在这个文件中导入需要暴露给Swift代码调用的ObjC头文件,这样我们就能在Swift中调用ObjC的代码了。

如果我们想在ObjC代码中调用Swift的代码,只需要写上一行import"ProductModuleName-Swift.h"(这里的ProductModuleName表示target的名字)就可以了,因为在编译时,编译器会自动给项目中的Swift代码生成一个ProductModuleName-Swift.h的头文件(这个文件是编译产物,我们在build目录可以看到它),暴露给ObjC使用。

除了在一个AppTarget内部混编之外,还有一种情况是当我们要写一个Library或者Framework给别人用时,这个时候如果有ObjC和Swift的混编,Objective-Cbridgingheader的方式已经不适用了,如果我们用了这个头文件,Xcode在预编译时也会警告我们。

先来看看Swift怎么调用ObjC,正确的做法是将BuildSettings中的DefinesModule选项设置为YES,然后新建一个umbrellaheader,再将需要暴露给(内部的)Swift调用的ObjC的头文件在这个umbrellaheader中导入(LLVMModule和umbrellaheader是两个新概念,后面会做具体介绍)。

--------------------------------------------------Hotel|HotelOrder|...业务层--------------------------------------------------HotelFoundation|HotelModel|...业务基础层--------------------------------------------------Network|Foundation|MapKit|RNKit|...基础框架层--------------------------------------------------图4笔者所在公司iOS客户端架构示意图

目前笔者所在公司的项目整体架构是采用模块化设计的,而且整个项目完全都是使用ObjC/C实现的,在实际开发时,各模块既可以以源码的形式使用,也可以以.a+.h+资源bundle的形式使用,简而言之,既可以源码依赖,也可以是静态库依赖。那么我们可以直接在项目中使用Swift静态库吗?

图5项目结构示意图(简化模型)

我们都知道,从Xcode9开始,Apple就开始支持Swift静态库的使用了,所以我们现有的项目架构并不需要调整,引入Swift代码的话是可以以静态库的形式出现的。

我们要做的第一步,就是创建一个Swift静态库工程,然后再把它作为子工程集成到ObjC主工程中去。

大概的步骤如下:

示例代码:

@objcMemberspublicclassSwiftLibA:NSObject{publicfuncsayHello(){print("Hello,thisisSwiftworld!")}}@implementationViewController-(void)viewDidLoad{[superviewDidLoad];[[SwiftLibAnew]sayHello];}@end问题:

1.为什么需要设置Dependencies?

设置Dependencies是为了告诉Xcodebuildsystem在编译主工程之前,需要先编译哪些其他的target,简而言之,就是编译依赖。

2.为什么需要设置LinkBinaryWithLibraries?

Xcode在build主工程时,会先编译好各个子工程,最后再链接成一个可执行文件,通过这个LinkBinaryWithLibraries设置,我们可以指定需要参与链接的静态库。

3.为什么需要复制xxx-Swift头文件到build目录下?

因为编译时自动生成的头文件是在Intermediates目录中各子工程所属的DerivedSources中,比如在我的电脑上就是/Users/ShannonChen/Library/Developer/Xcode/DerivedData/MainProject-aptbbpsumoitdlhbzjckyglkspoi/Build/Intermediates.noindex/SwiftLibA.build/Debug-iphonesimulator/SwiftLibA.build/DerivedSources/SwiftLibA-Swift.h,而主工程在编译时会到Build目录下的Products目录去找头文件,在我的电脑上就是/Users/ShannonChen/Library/Developer/Xcode/DerivedData/MainProject-aptbbpsumoitdlhbzjckyglkspoi/Build/Products/Debug-iphonesimulator/include,所以主工程或者其他子工程在编译时就找不到这个头文件了。

因此,我们就需要把这个xxx-Swift头文件复制到build目录下,具体脚本内容如下:

#将编译器生成的xxx-Swift头文件拷贝到build目录下的include目录中include_dir=${BUILT_PRODUCTS_DIR}/include/${PRODUCT_MODULE_NAME}/mkdir-p${include_dir}cp${DERIVED_SOURCES_DIR}/*-Swift.h${include_dir}参考:

集成好Swift静态库之后,我们再build一下,发现在链接时仍然会报错。

根据报错信息来看,是因为找不到swiftFoundation这些动态库,这是由于我们的主工程是纯ObjC项目,所以我们需要告诉Xcodebuildsystem这些Swift动态库的路径。

在BuildSettingstab下找到LibrarySearchPaths,添加上$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME),另外还需要添加Swift5.0的动态库所在的路径$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)。

这两个目录都可以在我们的电脑上看到:

参考:

静态链接的问题已经解决了,此时按下+R,模拟器启动后发生崩溃。控制台上的日志信息显示dyld:Librarynotloaded:@rpath/libswiftCore.dylib,这是因为程序启动时Swift动态库加载失败了。

为了解决这个问题,我们需要设置两个地方(只要你的项目iOSDeploymentTarget是12.2以下,这两个就都需要设置):

为什么我们要分别针对iOS12.2之前和之后的系统做不同的设置呢?将AlwaysEmbedSwiftStandardLibraries设置为YES是不是意味着每次打包时都会把Swift标准库打进去呢?

2019年对iOS开发者来说,最大的新闻莫过于SwiftABI终于稳定了。ABIStability意味着什么呢?ABIStability也就是binary接口稳定,在运行的时候只要是用Swift5.0或更高版本的编译器(Swift5.0对应Xcode10.2)构建出来的app,就可以跑在任意的Swift5.0或更高版本的Swiftruntime上了。这样,我们就不需要像以往那样每次打一个新的app时都要带上一套Swiftruntime和standardlibrary了,iOS和macOS系统里就会内置一套Swiftruntime和standardlibrary。

图11在这个例子中,基于Swift5.0构建出来的app可以直接在内置了Swift5或者Swift5.1,甚至Swift6标准库的系统上运行

但是如果你用的是Swift5.0以前版本的编译器,那么打包时还是会带上一套Swiftruntime和standardlibrary。

另外,对于用Swift5.0或更高版本的编译器构建出来的app,在发布app时,Apple将根据iOS系统创建不同的下载包。对于iOS12.2及以上的系统,因为系统内置了Swift5的runtime和standardlibrary,所以app中不再需要嵌入Swift的库,它们会被从appbundle中删掉。但是对于iOS12.2以下的系统,因为系统中没有内置Swift5的runtime和standardlibrary,所以打包时仍然需要带上。

理解了什么是ABIStability,就好理解我们前面在BuildSettings所做的两个设置了。

app在启动/运行时,会先看appbundle中有没有Swiftruntime,如果找不到,动态链接器dyld会到runpath路径下查找dylib(这个runpath路径是一个系统目录路径)。所以我们针对iOS12.2及以后的系统添加了RunpathSearchPath:/usr/lib/swift,针对iOS12.2以前的系统设置了AlwaysEmbedSwiftStandardLibrary。

AlwaysEmbedSwiftStandardLibrary曾经叫做EmbeddedContentContainsSwiftCode,字面上看上去像是“总是嵌入Swift标准库”,但是实际上这里只是告诉buildsystem要做的事,并不代表用户手机上下载的app是这样的,因为在发布app时,appthinning会自动根据目标系统来决定是否将appbundle中的Swift标准库删掉。

那么这个AlwaysEmbedSwiftStandardLibrary是用来告诉buildsystem做什么的呢?只要你的target会引用到Swift文件或者库,就需要把它设置为YES,比如我们这里的主工程用到了Swift静态库,所以就需要设置为YES,还有一种情况是你的target是一个测试工程,但是引用了Swift代码,那么也需要设置为YES。另外,笔者试验了一下,如果给一个纯ObjC的项目中添加了一个Swift文件,Xcode会自动将这个选项设置为YES。

前面提到过,笔者所在公司的iOS项目是采用的是模块化架构,而模块之间是有依赖关系的。一般是上层模块依赖于下层的模块,如图4所示。

这里先说明一下我们这里所说的模块的概念,在我们的项目中,一个ObjC模块就是.a静态库+.h头文件+bundle资源文件的组合。

如前面所说,ObjC调用Swift代码时,只需要导入编译Swift模块时自动生成的头文件xxx-Swift.h就可以了。

比如,模块ObjCLibA调用模块SwiftLibA:

这是因为SwiftLibA-Swift.h文件是编译模块SwiftLibA时的产物,是生成在build目录中,而不是工程代码所在的目录中。这一点我们在前面已经讨论过,这里不再赘述。

所以要想解决这个问题,我们可以换个思路,这个SwiftLibA-Swift.h文件是根据我们写的Swift代码公有API生成的,那么我们每次修改Swift代码的公有API时,它就会更新一次,所以,我们可以在每次build这个模块时把最新生成的拷贝到源码所在目录下(这个文件需要加入到版本控制中和其他代码一起提交),然后再把新的路径添加到ObjC模块的HeaderSearchPath中,另外,ObjC模块中头文件导入的方式也要改成双引号的形式。

完整脚本如下:

ObjC模块调用Swift模块的问题解决了,那么如果Swift模块调用Swift模块呢?会不会也存在类似的问题?

先来看一个例子,还是前面的那个示例项目,只不过多了一个模块SwiftLibB:

-MainProject-ObjCLibA-SwiftLibA-SwiftLibB然后我们在模块SwiftLibA中调用了模块SwiftLibB中的API:

importFoundationimportSwiftLibB@objcMemberspublicclassSwiftLibA:NSObject{publicfuncsayHello(name:String){SwiftLibB().sayHello(name:name)print("Hello,thisis"+name+"!")print("--PrintedbySwiftLibA")}}这个时候如果编译主工程是没问题的,但是如果单独编译模块SwiftLibA就会报错:Nosuchmodule'SwiftLibB'。

这个问题看上去跟前面遇到的ObjC模块调用Swift模块的问题是一样的,但是我们要知道Swift中是没有头文件的概念的,那么Swift是通过什么方式暴露公开API的呢?

不同于C-based语言使用manually-written头文件来提供公开接口,Swift是通过一个叫做swiftmodule的文件来描述一个library的interface,这个swiftmodule文件是编译器自动生成的。我们打开SwiftLibB模块的build目录,可以看到编译器自动生成的SwiftLibB.swiftmodule,这个SwiftLibB.swiftmodule目录下有两种文件:swiftmodule文件和swiftdoc文件。swiftmodule文件和swiftdoc文件都是二进制文件,我们可以用反编译工具查看其中的内容,swiftmodule文件里面保存了模块的信息,而swiftdoc文件则保存了源代码中的注释内容。

看到这里,你可能会想我们只要像导出xxx-Swift.h文件一样,把这几个swiftmodule文件导出到源代码目录,然后再设置SwiftLibA的importpath,另外再把这几个文件加入git版本控制中就解决了。

是的,我一开始也是这么想的,然后我就这么去做了,单独编译SwiftLibA确实问题,但是提交到git远程仓库之后,持续交付平台上的SwiftLibA模块却编译报错了:

...error:ModulecompiledwithSwift5.1cannotbeimportedbytheSwift5.1.2compiler...ModuleStability上面的方法之所以行不通,是因为swiftmodule文件跟编译器版本是绑定的,在Swift5.1之前,Apple官方没有提供解决办法,在发布Swift5.1的时候,除了ABIStability之外,Apple还解决了一个重要的,就是ModuleStability,也就是我们这里遇到的问题。

针对ModuleStability,Apple提供的解决方案是swiftinterface文件,swiftinterface文件是作为swiftmodule的一个补充,它是一个描述module公开接口的文本文件,不受编译器版本限制。比如,你用Swift5.0的编译器编译出了一个library,它的swiftinterface文件可以在Swift5.1的编译器上使用。

我们现在打开SwiftLibB的BuildSetting,找到BuildOptions->BuildLibrariesforDistribution,把它设置为YES,重新编译一下,再看看build目录中生成的SwiftLibB.swiftmodule,里面多了几个swiftinterface文件。

我们可以打开swiftinterface文件跟源代码对一下,它其实就是一个swift头文件。

源代码:

importFoundation@objcMemberspublicclassSwiftLibB:NSObject{publicfuncsayHello(name:String){print("Hello,thisis"+name+"!")print("--PrintedbySwiftLibB")}}swiftinterface文件中的内容:

//swift-interface-format-version:1.0//swift-compiler-version:AppleSwiftversion5.1(swiftlang-1100.0.270.13clang-1100.0.33.7)//swift-module-flags:-targetx86_64-apple-ios13.0-simulator-enable-objc-interop-enable-library-evolution-swift-version5-enforce-exclusivity=checked-Onone-module-nameSwiftLibBimportFoundationimportSwift@objc@objcMemberspublicclassSwiftLibB:ObjectiveC.NSObject{@objcpublicfuncsayHello(name:Swift.String)@objcoverridedynamicpublicinit()@objcdeinit}为了能够满足模块SwiftLibA的单独编译,跟前面对xx-Swift.h文件的操作一样,我们用脚本把SwiftLibB.swiftmodule拷贝到源代码目录中,然后再把这个新路径添加到SwiftLibA的BuildSetting->SwiftCompiler-SearchPaths->ImportPaths中。

这个方案对于模块化/组件化有个缺点就是,每次编译Swift模块时需要考虑多种不同的CPU架构。

除了这个方案之外,还有其他两个方案可以解决Swift模块之间依赖的问题:

参考

如果是在同一个apptarget里,Swift调用ObjC可以通过Objective-Cbridgingheader来实现,但是如果是跨模块的调用呢?Swift模块怎么调用ObjC模块?

根据Apple官方文档中的介绍,在Library或者Framework中不能使用bridgingheader的,而应该使用umbrellaheader。

在ObjC中可以通过@import指令导入module,在Swift中通过import关键字导入module。

Module机制中一个很重要的文件就是modulemap文件,modulemap文件是用来描述头文件和module结构的在逻辑上的对应关系的。

Thecruciallinkbetweenmodulesandheadersisdescribedbyamodulemap,whichdescribeshowacollectionofexistingheadersmapsontothe(logical)structureofamodule.Forexample,onecouldimagineamodulestdcoveringtheCstandardlibrary.EachoftheCstandardlibraryheaders(stdio.h,stdlib.h,math.h,etc.)wouldcontributetothestdmodule,byplacingtheirrespectiveAPIsintothecorrespondingsubmodule(std.io,std.lib,std.math,etc.).Havingalistoftheheadersthatarepartofthestdmoduleallowsthecompilertobuildthestdmoduleasastandaloneentity,andhavingthemappingfromheadernamesto(sub)modulesallowstheautomatictranslationof#includedirectivestomoduleimports.

Modulemapsarespecifiedasseparatefiles(eachnamedmodule.modulemap)alongsidetheheaderstheydescribe,whichallowsthemtobeaddedtoexistingsoftwarelibrarieswithouthavingtochangethelibraryheadersthemselves(inmostcases[2]).

Themodulemaplanguagedescribesthemappingfromheaderfilestothelogicalstructureofmodules.Toenablesupportforusingalibraryasamodule,onemustwriteamodule.modulemapfileforthatlibrary.Themodule.modulemapfileisplacedalongsidetheheaderfilesthemselves,andiswritteninthemodulemaplanguagedescribedbelow.

一个C标准库的modulemap文件可能就是这样的:

Aheaderwiththeumbrellaspecifieriscalledanumbrellaheader.Anumbrellaheaderincludesalloftheheaderswithinitsdirectory(andanysubdirectories),andistypicallyused(inthe#includeworld)toeasilyaccessthefullAPIprovidedbyaparticularlibrary.Withmodules,anumbrellaheaderisaconvenientshortcutthateliminatestheneedtowriteoutheaderdeclarationsforeverylibraryheader.Agivendirectorycanonlycontainasingleumbrellaheader.

如果你创建的是Framework,在创建这个Framework时,definesmodule默认会设置为YES,编译这个Framework之后,可以在build目录下看到自动生成的Module目录,这个Module目录下有自动创建的modulemap文件,其中引用了自动创建的umbrellaheader。但是如果你创建的是staticlibrary,那就需要开发者手动为这个module创建modulemap文件和要引用的umbrellaheader。

接下来我们创建一个ObjCLibB模块,然后让SwiftLibA模块来调用它。

首先要做的是给模块ObjCLibB新建一个umbrellaheader文件和一个modulemap文件,然后再把modulemap文件的路径添加到SwiftLibA的importpaths,把umbrellaheader文件的路径添加到SwiftLibA的headersearchpaths,这样就大功告成了。

如果你的Swift模块要调用的模块是ObjC-Swift混编的,也可用同样的方式来实现,核心点就在于将C-based语言的头文件用modulemap和umbrellaheader封装起来。

如果你的主工程是纯ObjC实现的,那么当你在断点调试Swift模块中的代码时,会无法看到变量值,即便在console上使用LLDB命令也打印不出来。

Swift5的到来终于让我们看到了期待已久的ABI稳定,相信更现代、更安全的Swift会变得越来越流行。另外,在模块化/组件化项目中落地Swift时,LLVMModule是一个绕不过去的话题,LLVMModule改变了传统C-Based语言的头文件机制,取而代之的是Module的思维。技术的发展会带来更先进的生产力,我们期待Swift在未来能够进一步提升我们的开发效率和编程体验。

THE END
1.不写代码,也能开发小程序和复杂网站了电脑软件编程知识电脑技巧02:23 5个封神AI工具,引爆你的创造力! 01:23 即梦AI,让短视频进入大片时代 02:17 一句话开发软件,干翻“秒哒”的产品出现了 03:08 AI应用复活漂流瓶,你离青春只有20分钟 00:59 一句话做个App:开发神器v0.dev做到了 01:04 炸裂!国产AI一键生成MV、短片、绘本!网易https://www.163.com/v/video/VYI1JQABJ.html
2.代码生成工具1——项目简介和基椽发代码生成项目需要提前在数据库建好表,然后执行代码生成工具,会生成简单的Java文件,避免重复编写增删改查代码。类似的工具网上有很多,本人开发这个工具属于自娱自乐。这个专栏会记录开发的过程。 2 项目搭建 数据库使用MySQL:8.1.0 JDK使用1.8 1、新建一个普通的Java项目。GeneraJava项目是实际开发工具。GeneraJavaDemo是生成之后的模https://blog.csdn.net/geminigoth/article/details/138556453
3.java如何自动生成代码java自动生成代码git项目java如何自动生成代码 java自动生成代码 git项目 目录 0、把自己的项目共享到Git上 1、在Git上新建仓库 2、输入仓库名称 3、创建成功,得到git地址 4、在Eclipse中创建一个java项目 5、Share Project 6、配置仓库 7、创建仓库 8、提交项目 9、设置提交信息https://blog.51cto.com/u_16213690/6976509
4.在Eclipse中生成代码可以使用 Eclipse 工具来检查模型、生成代码以及编辑所生成的代码。 关于本任务 有关描述Rhapsody?代码生成的详细指示信息,请参阅从 Rhapsody 模型生成代码。 Rhapsody使用 Eclipse IDE 项目进行代码生成。 在可以生成代码之前,您必须执行下列任务: 创建一个 Eclipse IDE 项目 https://www.ibm.com/docs/zh/engineering-lifecycle-management-suite/design-rhapsody/9.0.1?topic=eclipse-generating-code-in
5.如何生成固定资产投资项目代码?如何生成固定资产投资项目代码? 固定资产投资项目代码制度是《中共中央国务院关于深化投融资体制改革的意见》(中发〔2016〕18号)、《政府投资条例》(国务院令第712号)、《企业投资项目核准和备案管理条例》(国务院令第673号)明确的投资管理基本制度。全国投资项目在线审批监管平台生成的项目代码是项目整个建设周期的唯一http://www.xinyu.gov.cn/xinyu/sfzggw/2022-11/24/content_37c5dcd448ef43f19d30b4e6048b6ccb.shtml
6.如何:通过现有代码创建C++项目MicrosoftLearn在Visual Studio 中,你可以使用“从现有代码文件创建新项目”向导将现有代码文件移植到 C++ 项目中。 此向导创建使用 MSBuild 系统来管理源文件和生成配置的项目解决方案。 它最适用于没有复杂文件夹层次结构的相对简单的项目。 Visual Studio 的较旧 Express 版本中不提供该向导。 https://docs.microsoft.com/zh-cn/cpp/ide/specify-debug-configuration-settings
7.使用MyBatis快速生成代码的几种方法java3.2 选择package和path,会将生成的文件放在你选择的路径下,需要什么类型的文件在template中选中,然后开始生成。 3.3 生成成功 二、mybatis-generator快速生成代码 将mybatis-generator配置到项目里,将文件直接生成到指定的目录。 1. 配置generatorConfig.xml 在项目src/main/resources 下新建generatorConfig.xml,具体配置如https://www.jb51.net/program/307171csc.htm
8.Windows如何配置和迁移深度学习环境,以及使用Pycharm调试源码⑤如何生成项目文件夹的requirements.txt,以及根据.txt文件安装对应的库环境? 1.生成requirements.txt 推荐使用pip来安装pipreqs,安装命令: 代码语言:javascript 复制 pip install pipreqs 注意,这里的操作也是在torch14的conda环境下执行该命令! 然后使用cd命令定位到项目文件的根目录: https://cloud.tencent.com/developer/article/2117794
9.常见问题—JEECG低代码开发平台问题: 导入项目代码很多错误,例如 实体没有get/set方法 解决办法: IDEA或者Eclipse中安装Lombok插件 2.提示表不存在问题Table 'jeecg-boot.QRTZ_LOCKS' doesn't exist 解决方案 : 设置mysql数据库不区分大小写 错误截图: 3. 最新版本提示白名单校验未通过 http://www.jeecg.com/doc/qa
10.使用Sonar进行项目代码扫描5、对项目代码进行扫描 注意:若打算将最终扫描结果导出为PDF文档,建议在扫描之前先按照第三章的内容安装好PDF插件,再进行扫描操作!!!如果只是本地看代码扫描结果即可的话,则可忽略这段提示~ 扫描方式一:直接在项目根路径下打开cmd窗口,执行第4步生成的maven命令 https://www.jianshu.com/p/d51463cc462f
11.GTIN条码生成器,四种代码结构和11种码制–来福智条码完整的标识代码可以保证在相关的应用领域内全球唯一。 对贸易项目进行编码和符号表示,能够实现商品零售(POS)、进货、存补货、销售分析及其他业务运作的自动化。 下载GTIN条码生成器 GTIN的四种代码结构: 参考:《GTIN管理规则》 下载 条形码生成器 条码标签代制作 留言提问https://www.laivz.com/barcode-generate/gtin-barcode-generator/