iOS开发-使用OC/Swift开发静态库/动态库(SDK开发)

近期工作主要是使用OC/Swift来开发各种SDK,静态库和动态库都有用到,于是想写篇文章记录一下SDK开发的一些内容,以及我在开发中遇到的坑和解决办法,希望能为大家提供帮助。

目前使用开发环境为xcode910.x,Swift版本是4.04.2。

一、iOS库的区别

常见的自建iOS库分为两种形式,一种是xxx.a,还有一种是xxx.framework。这两种库有什么区别呢?

.a库只能是静态库,只支持OC,只能使用静态链接的方式来引入库,调用时需要.h头文件,资源文件通常自建.bundle来管理,也就是最后交付使用的SDK通常包含3个文件,.a文件,.h文件和.bundle文件。

.framework可以是静态库或动态库,支持OC和Swift,可以选择静态链接和动态链接的方式来使用库,.framework其实是一个头文件+可执行文件+资源文件的合集。

那么.framework静态库和动态库有什么区别?

个人理解主要是其内部链接方式不同,也就是一个是编译时链接一个是运行时链接。

如果静态库中使用了任何category,主工程Build Settings下Other Linker Flags是必须要加上-ObjC的,而动态库则不用。

如果静态库中链接了一些系统库,主工程需要再次链接这些库的。而动态库如果链接了一些系统库,只需要在动态库中处理,主工程内不需要再次引入。所以动态库使用会更加方便一些。

当静态库被其他静态/动态库依赖时,会将其可执行文件合并至其他库,主工程无需再链接该静态库。而当动态库被其他静态/动态库依赖时,不会将其可执行文件合并,所以此时主工程需要同时引入依赖库与被依赖库的。

通常我们.framework静态库使用静态链接Linked Frameworks and Libraries,.framework动态库使用动态链接Embedded Binaries。那么选择静态链接和动态链接又有什么区别呢?

在编译出来的product.app中或在最后的.ipa包中解压查看内容(即为工程的bundle内容,也是main bundle),选择了静态链接的.framework不会存在,其可执行文件会与主工程的可执行文件合并,而且其直接包含的资源文件不会编译进入主工程bundle中,所以必须另外使用.bundle来管理资源文件。

而选择了动态链接的.framework,整个.framework文件是独立存在于工程文件中的Framework文件夹下,但其内部的头文件会不再独立存在,因为主工程的bundle中是不允许存在代码文件的,此时头文件已经在主工程编译时一起转化成为了可执行文件,而库内其他文件,包括库的可执行文件和资源等,都继续存在于可执行文件之下。

无论.framework库是动态库还是静态库都不会影响以上特性。动态链接和静态链接的意思是工程如何来链接这个库,是采用静态的方式在编译时就链接,还是动态的方式在运行时才链接,虽然我们一般习惯静态库使用静态链接动态库使用动态链接,但是链接方式与库的类型还是要分开来理解。

我们可以利用这个特性,通过解压ipa,替换动态库,重签名ipa包,这样不重新出包就能更新库,或者是做一些分包的需求可以用到。

那么使用Swift开发库和使用OC又有什么区别呢?

最重要的一点,目前Swift还没有做到ABI稳定,本来苹果爸爸说Swift4来做到ABI稳定,结果跳票到Swift5,然后现在5也还未正式发布。而ABI不稳定会带来一个什么结果?

  • 编译库时使用的Swift版本必须跟主工程支持的Swift版本一致,举个例子,目前XCode9.09.2的Swift环境是Swift4.0,XCode9.39.4是4.1,XCode10.0~10.1是4.2。如果用的XCode9.4编译SDK,而使用XCode9.2或XCode10来运行SDK的,会编译失败或者运行报错,只有同样Swfit环境是4.1的XCode9.3和9.4才可以正常运行。也就是目前Swift上下都不兼容,我曾尝试给新版XCode添加旧版Swift支持,在xcode9.4和10.1上尝试过都并未成功。

  • 最后编译出来的ipa包比正常包要大几十m,而且dis的包比dev的包又要大几十m,因为Swift的核心支持库都包含在了ipa包中,解压ipa,会有一些比如SwiftCore.framework这样的库存在与包内容中的Framework文件夹下,而dis的包又会多包含一个叫做SwiftSupport的文件夹在ipa包中,如果缺少这个文件夹会上传报错。这也是由于ABI不稳定的原因,造成了包体过大。

当主工程是OC工程时,调用Swift库有一个常见的报错,image not found xxxx。这个报错通常可能由于以下几个原因。

  • 可能是setting中Always Embed Swift Standard Libraries没有选成YES。

  • 可能是Swift的库版本不对应,需要重新出Swift库。

  • 可能是工程没有自动帮你引入Swift支持库,这个时候我们可以在工程中新建一个.swift文件,创建桥接文件选YES,然后在该文件中包含一些swift的头文件,比如UIKIT,CoreImage之类的,错误信息报的是哪个库就添加哪个,可以强行让工程引入这些缺失的支持库。

当使用Swift和OC混编的时候,是个很头疼的事情,SDK不像App,无法使用桥接文件。如果要使用一些OC的文件,可以直接把.h文件设置成Public,然后在head文件中包含这个.h,这样Swift就可以调用OC。但是这种做法也有一个很大的问题,使用了哪些OC头文件调用者都是可以看得到的,个人觉得这样暴露一大堆头文件的方式并不合适,查了一下资料后使用Module来解决了这个问题,具体使用方式参见后面的例子。

在使用Swift开发库这其中蹚了许多坑,个人认为目前环境Swift还不适合用来做商业级SDK开发,等到Swift5.0正式版出来之后,如果做到ABI稳定了,可以再做尝试。

二、搭建项目

搭建项目推荐使用WorkSpace来同时管理Demo工程和SDK工程,这样方便断点调试。我们先建立几个库的项目,下图中左边红标是.framework库,右边是.a库。

image

我们来创建2个framework类型的库(OC和Swift各一个),1个.a类型的库(只创建OC,Swift没有头文件无法调用.a库),创建一个demo工程,一个workspace文件,将他们都放在一个文件夹中。如下:

image

打开workspace文件,把这几个创建的工程都添加进来。

image

framework类型默认类型为动态库Dynamic Library,所以静态库需要进入buildsetting修改Mach-o类型为static Library。我们先把这OC的framework库设置成静态库。

image

为demo工程添加这几个库的链接,动态库使用动态链接Embbed Binaries(会自动在下面的Linked Frameworks也添加一次),静态库使用静态链接Linked Frameworks。

image

因为引用了Swift库,我们需要将Build Setting中的Always Embed Swift这个选项设置成YES。

image

这样项目就搭建好了,使用Workspace的好处在于可以直接让demo和库一起运行,也可以直接断点到库中,便于调试。

三、编译库

编译库的时候注意选择编译的对象,当选择Generic IOS Device时,编译出来的库支持真机,选择模拟器的时候则编译出来的库只支持模拟器。

image

想要同时支持真机和模拟器,需要先单独编译库再使用命令合并库,合并.a库命令为lipo -create xxx.a xxx.a -output xxx.a,如果是合并.framework库,则把xxx.a替换成可执行文件就行了。当合并Swift库的时候还需要注意合并xxx.framework/Modules/xxx.swiftmodule下的内容。

顺带一提如果想要拆分库,可以使用lipo xxx -thin armv7 -output xxx命令。

因为我们已经给demo添加了对这些库的依赖,所以当我们编译demo的时候库也会跟着编译,不过如果我们在.a库中使用了bundle文件,bundle文件是不会跟随一起编译的,每次改变该文件需要重新手动编译。

四、调用库

调用.a库,我们可以选择OCStatic这个类作为对外接口类,直接在这个OCStatic.h文件中添加对外暴露的方法,在.m中实现该方法,然后将.h文件添加到demo工程中即可调用。

image
image

调用OC的framework库,只需要将需要公开的头文件设置成public,然后可以将全部的公开头文件都包含进OCFramework.h这个文件中(为了方便引用),外部调用时就可以访问这些头文件了。

image
image

调用swift的库,新建一个.swift文件,创建一个类,需要用public来修饰它,其中的属性想要暴露就使用public或者不加任何修饰,不想暴露就加上privite。想要暴露的方法则需要加上@objc public来修饰。

image

编译后会自动创建一个-swift.h文件,里面已经为我们自动生成了转换成OC的方法,方便OC工程调用。

image

五、资源读取

.a和.framework静态库使用图片资源时,通常是新建一个bundle的target,bundle是在macOS类型中找,建立后把setting中的Support Platforms或Base SDK改成iOS。创建bundle后系统会自动生成一个bundle文件夹,可以把图片资源和其他的非代码的资源比如xib、plist、html等等都放在里面,bundle中是不可以有代码文件的。

bundle需要单独编译,.然后主工程需要把.bundle文件拖入,这里拖入的时候可以不选择copy items,这样当我们重新编译了bundle文件之后,主工程就不需要再次去拖入bundle文件了。

在库中访问自身bundle资源的时候不能直接使用mainbundle,因为这里的结构其实是mainbundle/xxx.bundle,自己创建的bundle文件存在于主工程bundle下,所以需要先获取到自己的bundle,再来访问其中文件。获取自己的bundle可以使用路径的方式获取,然后自己创建的bundle是有单独bundle id的,也可以通过bundle id来获取。

如果只有少量的几张图片资源,可以考虑转成base64的代码直接拷贝在代码中,这样使用库的时候就可以无需引入bundle了。

image

动态库我们在工程中使用了动态链接,使用资源则不需要去建立bundle,因为动态链接后库本身就是独立存在于mainbundle中的,自身就相当于另一个bundle,但在库中还是需要通过路径或bundle id来获取文件。

六、混编

在Swift库中想混编OC时,没有办法使用桥接文件,但有一些其他办法可以调用OC。

第一种方式是直接将要调用的OC的头文件设置成public,然后在head文件中import这个OC头文件,这样就可以让swift文件来调用,但是这样不仅对库暴露了头文件,也会对外部工程暴露头文件,这种方式显然不尽人意。

第二种方式我们可以使用module来实现混编,原理大概就是利用了Swift的模块系统,把OC文件当做一个模块来调用。我们先建立一个module.modulemap文件,在里面添加需要混编的OC头文件支持。想让这个modulemap文件生效,需要前往Build Settings下的Swift Compiler - Search paths,在import paths 里面添加module.modulemap所在文件夹路径。设置完毕后,即可在.swift文件中import头文件后调用。

image
image

七、依赖

如果是.a库想要依赖其他的库,直接将需要依赖的库拖入.a库工程中,即可在文件中使用import来引入依赖库。

如果是.framework库想要依赖其他库,可以直接将需要依赖的库拖入.framework库工程中,如果是同一个WorkSpace下管理的库,则可以在Linked Frameworks中直接添加该库的引用。添加完毕后即可在文件中使用import来引入依赖库。

image

如果需要依赖的库为静态库,则会包含进主库中,主工程使用主库无需再次引入依赖库。如果被依赖库为动态库,则只是依赖关系,主工程使用该库时需要再次引入被依赖动态库。

八、查看结构

我们先编译一下demo,这里先就不用archive,直接看product文件夹中的.app文件。

image

我们右键选择显示包内容。可以看到以下内容。

image

我们的工程依赖了3个库,只有动态链接的Swift库存在于Framework文件夹下,可以看到它里面已经没有头文件了。

而静态链接的库,无论是.a静态库还是.framwork静态库,都已经不见踪影,只留下了各自的bundle文件位于根目录,也就是主工程main bundle下。

这里我们也可以看到Framework文件夹中,有很多Swift支持库,这就是由于ABI不稳定导致的结果了,目前Swift工程必须自带支持库,所以要比普通库大很多。而且经过Archive后会在外层又多一个Swift support文件夹,缺少这个文件夹的话是无法上传至Appstore的。

九、总结与展望

本文主要总结了一下最近工作遇到的问题和解决思路,以及Swift库的一些内容。开始写Swift之后就不想再写OC了,Swift有着简洁的语法和强大的特性,相信其有一天会代替OC,但目前来说ABI不稳定成为开发中的一大障碍,希望这一切在Swift5.0推出后能跨越这个障碍,能真正开始使用Swift开发商业级SDK。

作者:嘿咻嘿咻T
链接:https://www.jianshu.com/p/e05363d700dd
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

推荐阅读更多精彩内容