(iOS)年轻人,听说你想使用Framework - 基礎觀念

4,306 阅读8分钟

Frameworks

本文为译文,并已取得作者Nick Teissler同意。 原文链接:原文

Framework这小子问了几个很好的问题

本文章适合初学Framework的读者。

前言

Apple 已经将 iOS, macOS 的代码分成 Modules, libraries, frameworks。

Frameworks 的设计不单单只是为了封装资源跟模块化代码,更不只是为了减少代码的重编译时间而已。

要想减轻代码量、加速Debug、增加代码复用性,就不能只知道Framework是一个可以拖来拖去的工具箱,必须更近一步的了解这些:

  • 静态库 - Static Libraries
  • 动态库 - Dynamic Libraries
  • Framework的结构
  • Linking 链接 与 Embed 嵌入的不同
  • Q & A

以下内容适用 macOS, tvOS, iOS. 可能会随时间有改动。

静态库 Static Libraries

在静态库之前,我们要先从Object File说起,以基础的角度来说,object files 就是个有结构的位元块。这些位元块包含着一些程序代码、有些包含着准备给Linker跟Loader使用的资料结构。

可以试着在终端输入以下指令,瞄一眼objectfile的样子

objdump -macho -section-headers /bin/ls

Objectfiles通常以四种形式登场

  • Relocatable: 包含可以在编译时可被其他Relocatable Link的代码和数据,以共同制作成Executable。
  • Executable:准备好载入记忆体的可执行档。
  • Shared: 一种特殊的Relocatable file,概念类似 动态库。
  • Bundle: 我们不特别描述Bundle, 在macOS上通常当作插件使用。

译文外补充: Relocatable:包含着能与其他relocatable在编译时进行Linked的代码(执行代码)以及数据(变量全局数据)。 Shared: 编译时只依Header作api上的确认,不包进任一Executable,共享于Executable之间。

若你尝试编译一段没有 main函数的 C 代码,会得到一个 relocatable object file

fancy.c

void fancySwap(int *xp, int *yp) {
    if (xp == yp) return; // try it!
    *xp = *xp ^ *yp;  
    *yp = *xp ^ *yp;  
    *xp = *xp ^ *yp;  
}

在Terminal输入

cc -c fancy.c

产出的 fancy.o 是一种 objectFile ,包含着 fancySwap 符号以及对应的实现。为了更方便地处理 Relocatable object files,多个object files能被封装成 .a (a 代表着 archive) .a 档也被称作 静态库(static library) 或 静态归档(static archive)。

当executable中某function 或 数据来自于 .a,linker是能够智能地只链接其对应的符号,但是,linker还是会将所有的这些.a里的代码给一个固定的load地址并包含(copies and relocates)进executable中,造成肥大的executable,且增长读进memory的时间。

当你有10个应用使用此静态库,就需要个别有一份copy,也就是十个复本!而且若是静态库的开发者更新了代码,则所有的应用都必须重新编译一遍。而动态库就是为了解决这一步便利性而诞生的。

动态库 Dynamic Libraries

动态库 (也称作Shared Library, Shared object, 动态链接库), 跟静态库一样是多个object files封装起来的,不一样的是动态库只有在程序的载入时间(load time)或运行时(Run time)会被载入,并且在memory随机的配发一段地址。

以上行为是由动态链接器(Dynamic linker, macOS称dyld)来完成,动态库在:

  • macOS上 以 .dylib 存在
  • windows上 以 .DLL 存在
  • linux上 以 .so 存在

然而在运行时进行才做链接其实是一个笨重的负担,应合理安排哪些库需要Load以及时机。

在 Treminal输入以下指令查看更多关于动态载入的内容

man dlopen

Shared object 解决了前面static object files的问题

  • 现在多个executable只动态链接到 动态库的『唯一』拷贝。
  • 更新库并不影响executable
  • 库的依赖会被自动载入且链接(依赖库必须在search path里)

macOS 大规模的使用shared libraries,可以前往路径 /usr/lib文件夹查看系统的动态库。

如果你想要使用自己的动态库,必须确保它被 embedded 进你的App里。

看完上面这段,你可能会想 “太好了, 我不需要重编译了,但我最好为我库的使用者提供兼容性”。

是的没错,你的确需要考虑每当你update动态库时,API的兼容性,或许这也是你正在看这篇文章的原因.. 学习一种具有地址兼容性的bundling格式 - Framework

Framework

一个Framework其实就是一个有着特定结构的文件夹装着各种共享的资源。

这些资源通常是 图片、Xibs、动态库、静态库、文档...等,他们被封装成bundle存储却又不像其他的bundles( 像是 .app),Framework毫不掩饰的表明它纯粹就是一个文件夹。

这里指的Bundle并不是上面所提及的Bundle Object

Framework 以及其组成的子文件夹

Headers

这文件夹包含了Framework对外公开的C & Obj-C headers,Swift 并不会用到这些Headers,如果你的framework是用Swift写的,Xcode会自动帮你创建这个文件夹以提供互用性。

若你有时不清楚在Swift代码里加 @objc, @objcMembers 的影响,可以查看 Build Settings 里“SWIFT_OBJC_INTERFACE_HEADER_NAME”所指的文件,并试着改动@objc @objcMembers的宣告,看看这些改动对这文件的影响。

Modules

这文件夹包含了LLVM, Swift 的 Module信息。

.modulemap档案是给Clang使用的。

关于Clang 可看译者的另一篇文 LLVM前端Clang

.swiftmodule 文件夹下的档案类似headers,但是不像是 headers, 这些档案是二进制的且“无格式也有可能会改变”,在你Cmd-click 一个Swift函数时Xcode就是利用这些档案去定位其所属的module。

尽管这些都是二进制文件,但他们仍是一种叫 llvm bitcode 的结构,正因如此,我们能用llvm-bcanalyzer and llvm-strings取得相关信息。

MyCustomFramework

虽然他被finder标注成Unix executable,但他其实是一个 relocatable shared object file

Resources

本地化的资源, xibs, 档案, 图片... 跟其它资源存放在这

图片里的箭头 (Framework版本)

这些箭头都是 symlink 符号连结(捷径)。

每当产生一个新版本时,会被放进 Versions/B. Current 的箭头会被更新成指向 B。

与此同时,原本动态链接到A的程序仍会保持链接A而不是Current,怎么办到的?

每当executable 在编译时, dynamic linker会纪录与自己有兼容的Framework路径。

But

现今Apple已鲜少使用这个功能了

Xcode 里 Linking vs. Embedding Frameworks

这里我们来讨论Xcode里一些常见的设定,Target的 general settings.

Xcode's Target General Settings View

通常需要使用Framework时,都会选择 “Linked Frameworks and Libraries”,除非你打算在 runtime的时候才做链接跟载入(前面提及的dlopen)。

Linking 和 Embedding 的差别是什么?

为什么当你Embed一个Framework时,Xcode自动将Framework加到Link区?

想一下刚刚在读过的,dylibs储存在哪?当你想要用自己的dylib时该做什么?


是的!Embedding 实际上将Framework拷贝一份到你的application bundle里Frameworks文件夹下,

系统库是 iOS 和 macOS 自带的,你可以放心的链接他们,但是你自己的Framewroks并不是系统自带的,因此你必须要embedded 到 application bundle里。

而你的应用若是没有Linking它也没有任何用,所以Xcode自动帮你做了链接。

★★ Linking and embedding间接的暗示 动态 或是 静态链接,

我们现在知道embedding一个静态库是不合理的,因为静态库的符号已经被编译进executable,所以 Xcode不会应让你将static library放到 Embed

(但其实Xcode会让你放,然而这是很没效率的使用,所以你不该这么做)

技术上来讲,macOS是可以让你在runtime时去载入一个静态的 .a file的,并标记一块记忆体当作是 executable,

这样做能有效的载入静态库,然而这技术是由 JIT compilers完成的,iOS并没有标记记忆体的能力。

★★

听说你想要使用 Framework

在你使用Framework或是将自己的代码做成Framework时,有哪些因素要考虑的?

  • 你要开发一组应用吗?如果是,这就是Framework本质的一部分 - 分享 Frameworks,你需要去建一个包安装器把 shared frameworks放进 /Library/Frameworks or ~/Library/Frameworks.
  • 你的多个apps共享着些许的重复代码吗?使用framework或许不是最好的选择,如果只是要共享一个非常小量的代码,为了保持同步的 打包和版本控制反而会成为负担。
  • 你想试着模块化你的代码吗?如果你想要将代码移进Framework并将它变成模组,最好注意 Swift’s access control.
  • 别将Framework当作能不重新编译的万灵丹,有些项目会将代码中极大量快完成的部分移动到Framework以提升编译速度,这样做的确能降低时间,如我们所知Framework不需重新编译,然而这样的方式会使模块更复杂,Debug, 测试难度提升。
  • 你要将代码做成第三方包给其他人使用吗?这情况下,你可以选择使用动态库 静态库 或Framework。macOS, iOS已逐渐习惯动态Framewrok了,因为他的轻便性、普遍以及易用性。

Sources