如何在模块化/组件化项目中实现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.代码生成工具1——项目简介和基椽发代码生成项目需要提前在数据库建好表,然后执行代码生成工具,会生成简单的Java文件,避免重复编写增删改查代码。类似的工具网上有很多,本人开发这个工具属于自娱自乐。这个专栏会记录开发的过程。 2 项目搭建 数据库使用MySQL:8.1.0 JDK使用1.8 1、新建一个普通的Java项目。GeneraJava项目是实际开发工具。GeneraJavaDemo是生成之后的模https://blog.csdn.net/geminigoth/article/details/138556453
2.java指定类中自动生成代码mob64ca12f8a724的技术博客Lombok: 借助注解可以自动生成 getters、setters 和其他常用的方法。 Spring Roo: 一个快速应用开发框架,能通过命令行工具生成项目代码。 JHipster: 用于创建现代 Web 应用程序的开发平台,它支持自动生成 RESTful API 和前端代码。 3. 示例:使用 Lombok 生成 Getter 和 Setter https://blog.51cto.com/u_16213466/11711883
3.Java小程序代码:提升开发效率的关键技术服务器源代码命令提示符Java小程序的基本框架构建是一个涉及多个方面的过程,包括项目结构、代码组织、以及运行环境的搭建。项目结构是构建Java小程序的基础,通常包括源代码目录、资源目录和输出目录。源代码目录用于存放所有的Java类文件,资源目录则存放配置文件和静态资源,输出目录则存放编译后的字节码文件。 https://www.163.com/dy/article/JJ0RQCM50556AMAG.html
4.源代码生成APP软件应该怎么操作?步骤3:创建项目 一旦软件安装和配置完成,您可以开始创建新项目或打开现有项目。通常,源代码生成APP软件会提供一个用户友好的界面,让您轻松管理项目文件和设置。您可以选择创建一个空白项目或导入已有的项目文件。 步骤4:选择生成代码的模板 源代码生成APP软件通常会提供各种模板,用于生成不同类型的代码片段。在创建项目http://www.apppark.cn/t-47428.html
5.从0到1,如何搭建一个好用的springboot开源项目代码生成,提高基本功能的开发效率 等等 所以,通常我们从0开始设计一个项目,一般也不会真正从0开始写代码,而是先选择脚手架,然后在基础上添加业务代码,这样可以大大提高项目的开发效率,从而减少成本。 好了,写了一堆的废话,下面我们去分析一下,一个脚手架项目具体需要什么功能模块,然后要做哪些封装,用什么技术能事https://cloud.tencent.com/developer/article/1521095
6.开票申请流程操作指南1.非科研开票的“其他依据编号”怎么填写? 答:点击“其他依据编号”后箭头,选择“辅助依据补录”,根据开票项目实际内容填写并保存,系统将自动生成依据编号。相同事项可使用同一个辅助依据,无需重复录入。 2.如何选择票据种类? 答:提供应税劳务及从事其他经营活动取得应税收入时,需申请开具税票,即电子发票(普通发票)https://tjcwc.tongji.edu.cn/index.php?classid=9727&newsid=19561&t=show
7.codeMaker:代码生成器项目不依赖任何代码生成工具 基于mysql + mybatis + spring boot生成项目增删改查等功能 项目工具目前主要是为了构建可复用的代码生成服务,后续会继续沉淀其他代码生成服务 总体目标是为构建大规模springboot应用的技术底座,提高开发效率,专注业务领域,数据模型。 https://gitee.com/nullindex/code-maker
8.renrensecurity开发文档renren-generator为代码生成器模块,只需在MySQL数据库里,创建好表结构,就可以生成新增、修改、删除、查询、导出等操作的代码,包括entity、mapper、dao、service、controller、页面等所有代码,项目开发神器。 1.4.本地部署 环境要求JDK1.8、Tomcat8.5+、MySQL5.5+ https://www.renren.io/guide/
9.使用IntelliJIDEA快速生成项目代码前几年写了篇文章:Java Web开发时快速生成模版代码,是通过”git patch”文件方式来生成项目代码,虽然比复制粘贴的方式方便不少,但是还有比较繁琐,最近发现”IntelliJ IDEA (Ultimate Edition)”可根据数据库快速生成”POJO”代码,如下图所示: 既然”POJO”代码可以生成,那”Controller”, “Service”之类的代码也应该https://cofcool.github.io/tech/2019/06/13/idea-generate-pojos
10.vue项目代码覆盖率报告生成在本地代码运行时 通过点击触发代码覆盖率报告的生成 在代码覆盖率报告中显示自己的各个代码文件执行代码的百分比 首先使用 vue-cli 创建一个新项目 项目名称为 istanbul vue create istanbul 进入项目 并下载包 cd istanbul yarn 下载插件 yarn add--dev babel-plugin-istanbul chalk concurrently live-server nodemonhttps://www.jianshu.com/p/9ebeb1f14ccf
11.智能AI代码生成工具Cursor安装和使用超详细教程java授权成功后即可生成代码,也可以使用左下角的 Java 插件创建 Maven 项目等。 创建maven 项目 创建完毕后可以在项目中创建文件并使用 cursor 进行编码。 3.4 和代码“对话” (基于老版,新版的功能也类似) 可以选择生成的部分代码,去问任何你想问题的问题,让它对代码进行优化。 https://www.jb51.net/article/283984.htm
12.TouchGFX快速移植教程? 切换至Project Mananger,根据下图进心项目配置 ? 配置完成后,点击右上角“Generate code”生成代码 ? 等待代码生成完毕,点击关闭对话框 将项目导入至STM32 CubeIDE ? 启动STM32 CubeIDE,注意工作空间(Workspace)一定是CubeMX中设置的Project Location, https://oshwhub.com/article/touchgfx-quickstart
13.GTIN条码生成器,四种代码结构和11种码制–来福智条码完整的标识代码可以保证在相关的应用领域内全球唯一。 对贸易项目进行编码和符号表示,能够实现商品零售(POS)、进货、存补货、销售分析及其他业务运作的自动化。 下载GTIN条码生成器 GTIN的四种代码结构: 参考:《GTIN管理规则》 下载 条形码生成器 条码标签代制作 留言提问https://www.laivz.com/barcode-generate/gtin-barcode-generator/
14.OnlineCoding>代码生成「企业级低代码平台」前后端分离架构SpringBoot 2.x/3.x,SpringCloud,Ant Design&Vue3,Mybatis,Shiro,JWT。强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领新的开发模式,引入AI模型能力 OnlineCoding->代码生成->手工MERGE,帮助Java项目解决70%重复工作,让开发更关注业务,既能快速提高效率,帮助公https://github.com/zhangdaiscott/jeecg-boot