[关闭]
@qidiandasheng 2021-04-08T18:27:18.000000Z 字数 9228 阅读 11479

iOS里的动态库和静态库

iOS理论


介绍

动态库的作用

应用插件化:

每一个功能点都是一个动态库,在用户想使用某个功能的时候让其从网络下载,然后手动加载动态库,实现功能的的插件化

虽然技术上来说这种动态更新是可行的,但是对于AppStore上上架的app是不可以的。iOS8之后虽然可以上传含有动态库的app,但是苹果不仅需要你动态库和app的签名一致,而且苹果会在你上架的时候再经过一次AppStore的签名。所以你想在线更新动态库,首先你得有苹果APPStore私钥,而这个基本不可能。

除非你的应用不需要通过AppStore上架,比如企业内部的应用,通过企业证书发布,那么就可以实现应用插件化在线更新动态库了。

共享可执行文件:

在其它大部分平台上,动态库都可以用于不同应用间共享,这就大大节省了内存。从目前来看,iOS仍然不允许进程间共享动态库,即iOS上的动态库只能是私有的,因为我们仍然不能将动态库文件放置在除了自身沙盒以外的其它任何地方。

不过iOS8上开放了App Extension功能,可以为一个应用创建插件,这样主app和插件之间共享动态库还是可行的。(还需了解下App Extension)

Xcode6之后支持创建动态库工程

Xcode6之后苹果在iOS上开放了动态库。

创建:File->New->Project

创建

我们上面说过Framework即可以是动态库,也可以是静态库。那么我们上图中默认创建的是动态库,那么如何创建动态库呢?比如我创建的framework叫testLib,然后在build setting中设置动态库或静态库。如下图,创建framework的时候默认是Dynamic Library,我们可以修改为Static Library

如果我们创建的framework是动态库,那么我们直接在工程里使用的时候会报错:Reason: Image Not Found。需要在工程的General里的Embedded Binaries添加这个动态库才能使用。
因为我们创建的这个动态库其实也不能给其他程序使用的,而你的App ExtensionAPP之间是需要使用这个动态库的。这个动态库可以App ExtensionAPP之间共用一份(App 和 Extension 的 Bundle 是共享的),因此苹果又把这种 Framework 称为 Embedded Framework,而我把这个动态库称为伪动态库

具体创建静态库和Framework可以参考:Xcode7创建静态库和Framework

自己创建的动态库

我们创建的动态库和系统的动态库有什么区别呢?我们创建的动态库是在我们自己应用的.app目录里面,只能自己的App ExtensionAPP使用。而系统的动态库是在系统目录里面,所有的程序都能使用。

可执行文件和自己创建的动态库位置:

一般我们得到的iOS程序包是.ipa文件。其实就是一个压缩包,解压缩.ipa。解压缩后里面会有一个payload文件夹,文件夹里有一个.app文件,右键显示包内容,然后找到一个一般体积最大跟.app同名的文件,那个文件就是可执行文件。
而我们在模拟器上运行的时候用NSBundle *bundel = [[NSBundle mainBundle] bundlePath];就能得到.app的路径。可执行文件就在.app里面。

而我们自己创建的动态库就在.app目录下的Framework文件夹里。

下图就是测试工程DFCUserInterface.app的目录

我这里用了一个测试工程,即有系统的动态库(WebKit),又有自己的动态库(DFCUserInterface),我们可以看一下可执行文件中对动态库的链接地址。用MachOView查看可执行文件。其中@rpth这个路径表示的位置可以查看Xcode 中的链接路径问题,而现在表示的其实就是.app下的Framework文件夹。

下图表示了静态库,自己创建的动态库和系统动态库:


处理.a静态库

静态链接库其实就是个压缩包,我可以用 ar 这个工具进行解压

  1. ar -x libName.a
  2. 然后会发现报错:
  3. ar: libName.a is a fat file (use libtool(1) or lipo(1) and ar(1) on it) ar: libName.a: Inappropriate file type or format

这是由于这个静态链接库是由多个支持不同架构的静态链接库合并而成的。我们可以用一下命令查看:

  1. lipo -info libName.a
  2. 输出:
  3. Architectures in the fat file: *.a are: armv7 arm64 i386 x86_64

我们可以抽离出其中某一个架构的静态链接库:

  1. lipo -thin i386 libName.a -output libName.i386.a

然后就可以对这个libName.i386.a进行解压了:

  1. ar -x libName.i386.a

解压完成后你会看到好多以.o结尾的对象文件,这些对象文件就是给链接器最终生成静态链接库时用到的文件,也就是对应的可执行文件。还能看到__.SYMDEF,这个就是对应的符号表。

.o文件重新压缩成.a静态库

  1. libtool -static -o libNewName.a *.o

合并静态链接库

比如我们有libXXX.i386_x86_64.a和libXXX.armv7_arm64.a,那么我们可以通过如下命令来生成一个统一的静态链接库:

  1. lipo -create -output libXXX.a libXXX.i386_x86_64.a libXXX.armv7_arm64.a

查看.a库的符号

  1. xcrun nm -nm libName.i386.a

签名

系统在加载动态库时,会检查 framework 的签名,签名中必须包含 TeamIdentifier 并且 framework 和 host app 的 TeamIdentifier 必须一致。
我们在Debug测试的时候是不会报错的,在打包时如果有动态库,那么就会检查TeamIdentifier

如果不一致,否则会报下面的错误:

  1. Error loading /path/to/framework: dlopen(/path/to/framework, 265): no suitable image found. Did find:/path/to/framework: mmap() error 1

此外,如果用来打包的证书是 iOS 8 发布之前生成的,则打出的包验证的时候会没有 TeamIdentifier 这一项。这时在加载 framework 的时候会报下面的错误:

  1. [deny-mmap] mapped file has no team identifier and is not a platform binary:/private/var/mobile/Containers/Bundle/Application/5D8FB2F7-1083-4564-94B2-0CB7DC75C9D1/YourAppNameHere.app/Frameworks/YourFramework.framework/YourFramework

可以通过 codesign 命令来验证。

  1. codesign -dv /path/to/YourApp.app
  2. codesign -dv /path/to/youFramework.framework

如果证书太旧,输出的结果如下:

  1. Executable=/path/to/YourApp.app/YourApp
  2. Identifier=com.company.yourapp
  3. Format=bundle with Mach-O thin (armv7)
  4. CodeDirectory v=20100 size=221748 flags=0x0(none) hashes=11079+5 location=embedded
  5. Signature size=4321
  6. Signed Time=20151021 上午10:18:37
  7. Info.plist entries=42
  8. TeamIdentifier=not set
  9. Sealed Resources version=2 rules=12 files=2451
  10. Internal requirements count=1 size=188

注意其中的 TeamIdentifier=not set。

我们在用cocoapodsuse_framework!的时候生成的动态库也可以用codesign -dv /path/to/youFramework.framework查看到TeamIdentifier=not set。关于动态库的签名TeamIdentifier等之前没接触过,可以再去查看一下资料。

关于Framework

创建静态Framework

1.选择Framework

创建


2.选择为静态库


3.生成对应版本的静态库

静态库的版本(4种)

我们选择Release版本。编译模拟器和真机的所有CPU架构。

然后选择模拟器或者Generic iOS Device运行编译就会生成对应版本的Framework了。


4.合成包含真机和模拟器的Framework

终端cd到Products,然后执行以下代码,就会在Products目录下生成新的包含两种的执行文件,然后复制到任何一个testLib.framework里替换掉旧的testLib就可以了。

  1. lipo -create Release-iphoneos/testLib.framework/testLib Release-iphonesimulator/testLib.framework/testLib -output testLib

或者在工程的Build Phases里添加以下脚本,真机和模拟器都Build一遍之后就会在工程目录下生成Products文件夹,里面就是合并之后的Framework。

  1. if [ "${ACTION}" = "build" ]
  2. then
  3. INSTALL_DIR=${SRCROOT}/Products/${PROJECT_NAME}.framework
  4. DEVICE_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework
  5. SIMULATOR_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework
  6. if [ -d "${INSTALL_DIR}" ]
  7. then
  8. rm -rf "${INSTALL_DIR}"
  9. fi
  10. mkdir -p "${INSTALL_DIR}"
  11. cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/"
  12. #ditto "${DEVICE_DIR}/Headers" "${INSTALL_DIR}/Headers"
  13. lipo -create "${DEVICE_DIR}/${PROJECT_NAME}" "${SIMULATOR_DIR}/${PROJECT_NAME}" -output "${INSTALL_DIR}/${PROJECT_NAME}"
  14. #open "${DEVICE_DIR}"
  15. #open "${SRCROOT}/Products"
  16. fi

Framework目录

Framework的资源文件

CocoaPods如何生成Framework的资源文件

我们能看到用cocoapods创建Framework的时候,Framework里面有一个.bundle文件,跟Framework同级目录里也有一个.bundle文件。这两个文件其实是一样的。

那这两个.bundle是怎么来的呢?我们能看到用use_frameworks!生成的pod里面,pods这个PROJECT下面会为每一个pod生成一个target,比如我有一个pod叫做testLib,那么就会有一个叫testLibtarget,最后这个target生成的就是testLib.framework
那么如果这个pod有资源文件的话,就会有一个叫testLib-bundleNametarget,最后这个target生成的就是bundleName.bundle

上面创建静态Framework例子里生成资源文件

testLibtargetBuild Phases -> Copy Bundle Resources里加入这个这个.bundle,在Framework里面就会生成这样一个bundle。
testLibtargetBuild Phases -> Target Dependencies里加入这个target:testLib-bundleName,就会在Framework的同级目录里生成这样一个bundle。

静态Framework里不需要加入资源文件

一般如果是静态Framework的话,资源打包进Framework是读取不了的。静态Framework和.a文件都是编译进可执行文件里面的。只有动态Framework能在.app的Framework文件夹下看到,并读取.framework里的资源文件。

你可以用NSBundle *bundel = [[NSBundle mainBundle] bundlePath];得到.app目录,如果是动态库你能在Framework目录下看到这个动态库以及动态库里面资源文件。然后你只要用NSBundle *bundle = [NSBundle bundleForClass:<#ClassFromFramework#>];得到这个动态库的路径就能读取到里面的资源了。
但是如果是静态库的话,因为编译进了可执行文件里面,你也就没办法读到这个静态库了,你能看到.app下的Framework目录为空。

在framework或子工程中使用xib

问题

Swift 支持

跟着 iOS8 / Xcode 6 同时发布的还有 Swift。如果要在项目中使用外部的代码,可选的方式只有两种,一种是把代码拷贝到工程中,另一种是用动态 Framework。使用静态库是不支持的。

造成这个问题的原因主要是 Swift 的运行库没有被包含在 iOS 系统中,而是会打包进 App 中(这也是造成 Swift App 体积大的原因),静态库会导致最终的目标程序中包含重复的运行库(这是苹果自家的解释)。同时拷贝 Runtime 这种做法也会导致在纯 ObjC 的项目中使用 Swift 库出现问题。苹果声称等到 Swift 的 Runtime 稳定之后会被加入到系统当中,到时候这个限制就会被去除了(参考这个问题的问题描述,也是来自苹果自家文档)。

CocoaPods 的做法

在纯 ObjC 的项目中,CocoaPods 使用编译静态库 .a 方法将代码集成到项目中。在 Pods 项目中的每个 target 都对应这一个 Pod 的静态库。

当不想发布代码的时候,也可以使用 Framework 发布 Pod,CocoaPods 提供了 vendored_framework 选项来使用第三方 Framework。

对于 Swift 项目,CocoaPods 提供了动态 Framework 的支持。通过 use_frameworks! 选项控制。对于 Swift 写的库来说,想通过 CocoaPods 引入工程,必须加入 use_frameworks! 选项。

关于 use_frameworks!

在使用CocoaPods的时候在Podfile里加入use_frameworks! ,那么你在编译的时候就会默认帮你生成动态库,我们能看到每个源码Pod都会在Pods工程下面生成一个对应的动态库Framework的target,我们能在这个targetBuild Settings -> Mach-O Type看到默认设置是Dynamic Library。也就是会生成一个动态Framework,我们能在Products下面看到每一个Pod对应生成的动态库。

这些生成的动态库将链接到主项目给主工程使用,但是我们上面说过动态库需要在主工程target的General -> Embedded Binaries中添加才能使用,而我们并没有在Embedded Binaries中看到这些动态库。那这是怎么回事呢,其实是cocoapods已经执行了脚本把这些动态库嵌入到了.app的Framework目录下,相当于在Embedded Binaries加入了这些动态库。我们能在主工程target的Build Phase -> Embed Pods Frameworks里看到执行的脚本。

所以Pod默认是生成动态库,然后嵌入到.app下面的Framework文件夹里。我们去Pods工程的target里把Build Settings -> Mach-O Type设置为Static Library。那么生成的就是静态库,但是cocoapods也会把它嵌入到.app的Framework目录下,而因为它是静态库,所以会报错:unrecognized selector sent to instanceunrecognized selector sent to instance 。

使用 Swift Static Library

升级 CocoaPods 1.5,使用 Swift Static Library

参考

创建一个 iOS Framework 项目
Xcode7创建静态库和Framework
iOS 静态库开发
静态库与动态库的使用
iOS 静态库,动态库与 Framework
签名
iOS 动态库和静态库的的区别 动态库的隔离与静态库的吸附问题以及解决方法

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注