从 Core Foundation 看更大世界

Core Foundation 是被 iOSer 忽略的一个重要框架。说重要,因为 Core Foundation 提供了丰富的组件库,这些组件库可以很好的用于开发工作。
但之所以被忽略,因为很多开发工作,可以用更友好的 Foundation 框架替代。
Core Foundation 有 Foundation 没有的功能,比如 CFDictionary 的 Key 元素无需实现 NSCoping 协议、CFArray 可以不进行对象引用计数等。反过来,Foundation 也有 Core Foundation 无法胜任的工作,最大的来说就是自动引用计数功能。
在 iOS 项目开发过程中,我们可以使用基于 C 语言的 Core Foundation 框架写一些业务功能逻辑,甚至有时候非用 Core Foundation 不可,因为它有 Foundation 没有的功能。

Foundation 是用 Objective-C 语言写的,Core Foundation 使用 C 和 C++ 语言写的。我们都知道 Objective-C 是 C 的超集,所以认为 Objective-C 和 C、C++ 混编是正常的。
那么,什么是超集?Objective-C 是动态的面向对象的,C 是静态的面向过程的,如何实现这个超集?
既然 Objective-C 可以和 C、C++ 一起使用,那么 Golang 呢?我们可不可以用 Go 来做混合开发?

通过 Core Foundation,可以有更大的认知空间。
比如各类高级语言在计算机中是如何运行的?Dart (flutter) 可以做混合开发,原理是什么?Lua 做热更新,它不是 C 语言也不是 Objective-C 语言,是怎么被计算机调用执行的?用 Node.js 写 iOS 代码,到底行不行?
各种风马牛不相及的高级编程语言,是否有各自的边界?

iOS 开发框架有哪些

框架是库的更高一层描述,这里的库一般指的是动态库,但说静态库,也完全没有问题。
比如,我们要写一个语音识别功能,我们写了 1-n 个动态库 (a.framework、b.framework…) 来完成这个功能。在项目最后,客户说只需要一个动态库,我们就把这 1-n 个动态库组合成 1 个动态库并命名 GJAudioKit.framework,就可以叫 GJAudioKit 为语音识别框架了。
所以框架这个专有名字该怎么解释说明?
一来是众多库组合起来的意思。为了一个功能,需要写 1-n 个库。最后将 1-n 个库组合成 1 个库,这个库就叫一个框架。
二来框架是一个抽象,是比库更高层级的抽象。不管静态库还是动态库,都是目标文件 (.o) 的合集。而框架比库的抽象层级更高,表示一个功能、一个业务或者一个模块,比如语音识别框架。

iOS 系统提供了很多框架给我们使用,详见下图:

我们经常使用的框架都正在图中的分层结构中找到对应的影子。
也可以从 Xcode 的资源文件中查看,如下图:

刚才说到框架有 1-n 个库组成,上图中的 SwiftUI.framework 框架里面只有一个 SwiftUI.h 头文件,而 Foundation.framework 框架里面有近 130 个头文件。我们可以理解为一个头文件即可单独生成一个动态库。

框架的理解就到这,值得一提的是,上面列出的框架,都是系统提供的,也都是 C、C++、Objective-C 写的。我们自己当然也可以开发需要的库或者框架,那么,我们必须要用 C 族语言开发吗?

如果用 Go 来写 iOS 框架会怎么样

手机和 PC 有很多不同,比如 PC 的硬件都是可以拆卸的,而手机一般都不会拆卸,所有硬件都是集成到一个主板焊死的,我们不能随便更换存储和内存。CPU 也不一样,PC 有很多种类的 CPU 支持,可以根据用户是美术生或者喜欢玩游戏而选择不同的产品型号,比如撕裂者等。而手机为了省电,都用的 ARM 架构 CPU。
但不管手机和 PC 如何不同,有一个共同点是计算机发展几十年来不曾改变的,乃至手机和计算器都是一样的原理,那就是他们都依托发展于冯诺伊曼机。

程序需要执行,不能执行没有任何意义的程序,所以输入输出是必须的。而冯诺伊曼机的程序执行就是执行二进制。

所以从这点上看,高级语言再怎么变化,最终也跑不了 CPU 指令集二进制执行这个宿命。

PC 上我们可以跑上千种语言,因为这些语言最终都是二进制。只要语言被编译汇编成能够被执行的指令集,那么这些语言就有被编写和执行的意义,不管是在 PC 上执行,或者手机或者树莓派上执行。

所以,Go 当然可以在 iOS 手机上运行,不仅 Go,Java、Ruby、Lua、Node.js 都可以。那,怎么才能将 Go 写到 iOS 程序里面呢?
这就要分析 C 是如何被 iOS 系统执行的。因为他们都属于高级语言,如果 C 能够依靠一个逻辑被执行,那么 Go 按照这个逻辑也就可以执行。

C 语言是如何被操作系统执行的

系统调用和运行时

很久很久之前,是用纸带打洞进行编程。那时候 CPU 直接执行二进制。
现在就不行了,因为有操作系统存在了。操作系统是一个大管家,管理着所有的应用程序,通过合理的管理应用程序的内存,进行系统级别的控制。
操作系统该如何操作,才能管理应用程序呢?显然,控制了代码,就控制了所有。如果实际运行的代码,都在操作系统的管辖范围内,那么操作系统就想怎么控制就怎么控制了。
这个时候,我们就无法绕过操作系统直接让 CPU 执行我们的二进制了,而是需要让操作系统在中间做一个中间者,我们调用操作系统的接口,操作系统进而让 CPU 执行进入内核态 (内核塌陷),这个时候我们的代码才算被执行。这个操作系统的接口,就是系统调用
操作系统提供了完善的服务,人们都装了主流的几大操作系统。因为我们的代码需要被用户拿去执行才有意义和价值,所以,我们的代码全都需要接受操作系统的控制。
所以现在,我们的 C 语言代码从开发阶段到被执行,是下面这样的:

系统调用,都是汇编实现的,并实现了 C 的接口供用户调用。这里需要说明一点,几大操作系统都是用 C 语言提供的系统调用接口。不管用户态是什么类型的高级语言,系统调用提供的仅仅是 C 接口。

下面是部分系统调用接口,

系统调用接口并不是很多,都是操作系统提供给外界的刚需接口,大约 350 个左右(不同系统的接口数量不同)。这个时候,C 开发人员开发的时候都是调用 open 用来打开文件,调用 brk 来申请内存。显然和现实不太一样,我们开发的时候,都用的 fopen 打开文件,用 malloc 申请内存。这是为什么呢?

原因就是直接使用系统调用,非常困难。具体分两点来说:

  1. 系统调用提供的接口都是基础接口,比较生硬且基础。程序员需要的一个很基础功能,可能需要调用好多个系统调用接口才能完成。
  2. 系统调用是操作系统提供的。如果用户用 Linux 系统的系统调用接口开发了程序 A,那么如果想让程序 A 在 Windows 系统上运行,那是不可能的,因为两个系统的系统调用接口完全不一样。

显然,C 语言开发者直接进行系统调用,遇到了困难。而中间件可以解决所有困难,如果解决不了,那就再加一个中间件。

下面是添加了 C Runtime Library(运行时)后的调用流程:

运行时是一个中间层,用户写的代码,最终调用的都是运行时接口。这个接口可以专门为 C 语言提供非常丰富的接口调用,有下面四种情况:

  1. 1 个系统调用接口可以为 n 个运行时接口提供服务,比如 malloc 和 free 都使用了 brk。
  2. 1 个运行时接口可以调用 n 个系统调用接口,比如 w 接口,需要 x、y、z 接口同时提供服务。
  3. 1 个运行时接口可以仅调用 1 个系统调用接口,这个时候是 1-1 关系。
  4. 1 个运行时接口可以不调用系统调用接口,如 strcpy,专门的 C 语言字符串处理函数。

而且,不同系统提供的系统调用是不同的,但只要改变运行时,不需要修改用户代码,即可适配多平台。而每个系统都需要维护一套语言级别的运行时,这是必要且可行的。

这样,C 运行时作为中间层,极大的提高了开发人员生产力。

跨过运行时直接系统调用

这里有一点需要说明,C 运行时虽然作为高级语言和系统调用的中间层,但也不一定非要过这个中间层不可。因为操作系统只管理系统调用,而上层如何调用系统调用,是不受约束的。如果有一门语言,完全没有运行时概念,用户代码直接对接系统调用完全没有问题,就是上面说的系统调用生硬且基础不能跨平台两个缺点。所以 C 语言真实调用逻辑如下:

开发人员在编写代码的时候,可以调用 C 运行时库接口,也可以直接进行系统调用。但还是那句话,这样用的人肯定不多,项目里面可能个别代码会如此实现,但一定是有足够把握才会这么做。

Windows API 的存在

还有一点需要说明,Windows 和 Linux 还有些不一样。Linux 的 CRT 直接进行系统调用,而 Windows 又加了一层中间层,名叫 Window API。这个中间层夹在系统调用和 MCRT 之间,如下图:

Window API 对微软意义重大,作为最出名对商业软件,Window API 更好的保障了用户升级带来对兼容性,所以中间层真的很好用。

C 运行时和 C 标准库的关系

下面额外补充一点,C 语言规范中,出了标准,没有出实现。所以 C 语言的编译器和相关库版本非常多。
因为不同操作系统之间,有相同特性也有不同特性,所以不同操作系统的运行时接口有相同的也有不同的。
比如,内存分配,提供的 api 都是 malloc。而 windows 有图形界面的绘图 api,对应的 linux 就没有体现。
于是就把默认提供的 api 如 malloc 或者 printf 等叫做 C 标准库。其他各自独有的也并入 C 运行时库。详见下图:

C 的运行时比较特别,主要因为 C 出了标准,却没有给实现,于是各家为政。所以 C 的运行时库里面包含了 C 标准库,还有其他接口如启动函数等。而对于其他高级语言,一般就没有运行时库包含标准库的概念,因为标准库和运行时库也是独立的。比如,Objective-C 里面,系统提供了非常多的标准库即框架,这些框架都是动态库形式,但他们不是运行时,有单独的运行时库负责系统调用 (OC 比较特别,实际为对接 C 运行时而非系统调用,下面会详细说明)。

C 语言运行总结

到这里,不知道大家有没有发现秘密,C 语言能够被执行,有两个要点:

  1. 系统调用。对外界提供统一的内核塌陷。
  2. C 运行时。提供 C 程序员开发的接口。

所以做为高级语言的 C 语言,能够在计算机和手机甚至嵌入式系统执行,核心就在于系统调用。我们编写的代码,在编译汇编链接后,都变成了对运行时的调用,而运行时对系统调用 Api 进行调用。系统调用由操作系统控制,所以操作系统才能严丝合缝的对我们编写的代码进行控制和管理。当然最终执行还是 CPU 执行指令,只是优先级、内存分配、线程调度等,都是操作系统控制了。

所以,只要一门高级语言,最终能够通过 L Runtime Library (语言运行时) 进行系统调用,那么,该语言就可以在操作系统的控制下,完成指令集的运行。

Objective-C 是如何被操作系统执行的

上面我们了解了高级语言 C 语言运行的原理,那么我们紧接着看下 Objective-C 是如何被运行的。
我们都知道,Objective-C 是 C 语言的超集,这个超集该如何理解呢?
C 是面向过程的静态的,C 不是动态化语言(不是完全动态化),因为函数调用在编译时候已经确定。
但是基于 C 语言的 Objective-C 语言,却是完完全全的动态化语言。Objective-C 是面向对象的编译型语言,所有函数的执行都是在运行过程中确定,超集是如何做到这些功能的呢?

C 的代码执行我们已经知道,我们写的代码,在编译后都变成 C 运行时函数调用的二进制。
这个 C 运行时作为中间层,干了一件大事,就是完成对系统调用的隔离和封装。
超集从字面可以理解,是在 C 的基础上做一些事情。
所以,我们可以想一下,如果在一个中间层上面再加一个中间层,在 C 运行时上面再加一个 OC 运行时,那么 OC 运行时是不是就可以做更多的事情?
比如,在 Objective-C 中,代码调用 A 类的 a 对象的 method_1 () 函数,那么在运行时,我们希望调用 method_2 () 函数。那么这么一个函数调用的变化,肯定不能依靠系统调用来做,它管内核状态,不管应用层事情。也不能 C 运行时来做,因为 C 运行时是 C 语言特有的功能,不会单独为高级语言 Objective-C 来做这个事,本身它也做不了,因为它是静态语言,自身都没有动态性。so,肯定有一层单独为 Objective-C 做了这个事情,这一层,就是 OC 运行时。

我们通过 OC 运行时源码分析一下对象创建的过程。
如果我们有一个 OC_Person 类,如下:

1
2
3
4
5
@interface OC_Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation OC_Person
@end

现在,我们要创建一个对象 person,即:

1
OC_Person *person = [[OC_Person alloc] init];

详细调用流程图如下:

从上面的函数调用链,我们发现有两个关键点:

  1. Objective-C 进行对象内存初始化的时候,通过 Objective-C 的函数调用,最终调用到 C 语言的 calloc () 函数调用。
  2. 内存分配的大小,是通过”->” 结构体从一个 struct 拿到的。即调用一个结构体的 instanceSize 函数。(C++)

所以,我们在 Objective-C 里面创建一个对象并进行内存分配,开始的时候调用的是 OC 运行时,最终是调用了 C 运行时。我们创建的 Objective-C 对象本身,在运行时阶段,都是通过 struct 结构体获取值,所以对象在项目 Build 后,都转化成 C/C++ 结构代码了。

我们在看下下面代码执行流程:

1
[person setName:@"x"];

详细调用过程如下:
[person setName:@"x"]->objc_msgSend(person,SEL(setName),@"x")-> 汇编代码在运行时阶段查找 struct OC_Person{...} 结构体中的 setName 函数的地址 p_setName->call Oxab435c2(p_setName)

上面函数调用过程分析如下:

  1. Objective-C 的函数调用,是通过汇编语言编写的 objc_msgSend 进行的中转。其实 objc_msgSend 本身就是一个中间层,是动态转发的入口,将函数调用中转到运行时阶段。
  2. OC 运行时阶段进行函数地址的查找,在找到对应的函数地址后,进行地址调用 (函数执行)。

从上面对 OC 运行时的分析,我们可以看出,说 Objective-C 是 C 的超集,其实应该这样理解:
Objective-C 是高级语言,在代码编译后,会调用 OC 运行时接口,进行相关操作如对象创建,方法查找等。而 OC 运行时接口的具体实现,则是依托 C 运行时实现的。
比如,我们创建的 Objective-C 对象,在编译后,都会转换成 struct 结构体的形式进行 OC 运行时调用。再比如,我们创建对象调用 OC 运行时的 alloc 接口,在内部,却是调用的 C 运行时的 calloc 接口 (显然,calloc 调用的是系统调用的 brk 接口)。
面向对象是面向开发人员的,OC 运行时负责面向对象的 Objective-C 代码和 C 运行时之间的沟通。
Objective-C 的标准库和 C 就不一样了。C 的标准库上面说到,是 C 运行时的一部分。对于其他高级语言来说,标准库就是单纯为应用层封装的动态库,不属于运行时的一部分了。

下面是具体的流程图:

我在之前文章中,也有一个 Objective-C 和运行时库的说明:Objective-C 和 Runtime

Go 该如何被 iOS 系统执行

我们已经分析了 C 和 Objective-C 在 iOS 操作系统上运行的原理。
我们可以确定,我们写的代码,只要最终能够对接系统调用并编译成二进制交由操作系统运行,那么我们的代码就能运行。
我们写的代码都是高级语言代码,比如我们写的高级语言的函数调用:[OC_Person alloc],这个函数在编译后我们假设为 call Oxa1b2c3,其中 Oxa1b2c3 是 OC 运行时的_objc_rootAlloc 函数的虚拟地址。
_objc_rootAlloc 内部会调用 calloc() 进行 C 运行时函数调用,我们假设为 call Oxd4e5f6。到此为止,我们自己写的 [OC_person alloc] 代码,我们知道在代码区里面,那么 Oxa1b2c3Oxd4e5f6 这两个运行时的函数在哪里呢?
运行时说到底,也是代码。运行时有两种存在形式,一个是动态库,一个是静态库。
我们的操作系统都默认有 C 的动态库运行时。所以我们在 Linux A 电脑编译出来的 ELF 执行文件,在 Linux B 电脑上是可以直接运行的,就是因为 C 运行时库是用动态库的形式在执行文件启动后进行链接的。不仅仅操作系统,只要是计算机,大差不差都有 C 的动态库运行时,所以 C 语言才如此通用。因为只要写了 C 语言,不出意外到哪里都可以跑起来,除非用了特定系统的 api。
那如果有一个操作系统,真的没有 C 的动态库运行时,是不是就不能支持可执行文件了呢?
运行时存在两种形式,一种动态库,还有一种静态库。我们在编译可执行文件的时候,可以把运行时打到可执行文件中,这样刚才说到的 Oxa1b2c3Oxd4e5f6 运行时函数就打到可执行文件中了,即代码区。这样,及时操作系统没有安装 C 的动态库运行时,可执行文件一样可以跑起来。只不过,这样的化,可执行文件就会变大,因为包含了静态库运行时的大小。
所以 Objective-C 能够在 iOS 和 Mac 上运行,就是因为这两个系统里面,有动态库 OC 运行时。
那 Objective-C 能不能在安卓手机或者树莓派上面运行呢?因为 Objective-C 不支持运行时的静态库链接,而安卓和树莓派上没有动态库 OC 运行时,所以就运行不了 Objective-C App 了,因为找不到对应的函数调用,即上面说到的 Oxa1b2c3Oxd4e5f6

Go 静态库运行时的必要性

所以,Golang 该如何在 iOS 系统上执行?Golang 本身是高级语言,肯定有运行时库,分别有动态库和静态库两个版本。因为 iOS 操作系统本身没有 Golang 运行时,那么在编写 Golang 的代码后,在编译链接的时候,把 Golang 的静态库链接到最终的执行文件中 (静态库或者动态库,或者叫框架),那么这串 Golang 编写的代码,就能够在 iOS 系统上完美的运行起来。
这个时候,Golang 运行时需要做那些事情呢?

  1. Golang 需要做一个静态库运行时,链接到执行文件中。因为 iOS 系统本身只有 Objective-C 运行时、C 运行时、C++ 运行时,没有 Golang 运行时。
  2. Golang 原本肯定没有考虑运行在 iOS 上,所以 Golang 的运行时对接了 Windows 和 Linux 的系统调用。那么现在,Golang 的静态库运行时就需要对接 iOS 的系统调用。

当上面两个步骤完成,我们就可以通过 Golang 编写代码并导出 framework 库,在 iOS 系统上被执行。
我们写出 Golang 库,肯定还是希望被 Objective-C 调用,因为 Objective-C 和 C 支持混编,而 Go 有一个库 CGo,可以让 Go 和 C 连通,所以 Objective-C 这个时候就可以放心的调用 Golang 开发的库 / 框架了。

下面是 Golang 在 iOS 系统上的运行流程图:

幸运的是,Go 开发 iOS 所需要的库 / 框架,目前已经有发行版了,即 Go Mobile。我测试一下,简单一行代码,在编译后的嵌入动态库中,也有 1.5M,原因就是这个动态库中,有 Go 的静态运行时和 CGO 静态库。

其他高级语言如何编写 iOS 需要的动态库

其实不止 Go,Node.js 也一样可以用来开发 iOS 的动态库,有这个 Node.js for Mobile Apps
Go 和 Node.js 能够写 iOS 动态库,那么按照同样的逻辑,其他高级语言也一样可以做这件事,比如,游戏开发中,很多就用 Lua 来做热更新,Lua 代码要被执行,也需要一个 Lua 的静态运行时嵌入到库中。

上面说到的,还都是业务功能库,不是 UI。那 Flutter 就是完全依靠 Dart 语言来做跨平台的混合开发方案。Flutter 的库会让 iOS App 的包体积增加 15-25M,就是因为里面有一系列的运行时和相关 UI 组件库存在。
我们通过上面 Go 的大小可以发现,运行时库本身没有多大,Flutter 库比较大的原因,是 Flutter 通过 Dart 完全重新实现了自己一套 UI 框架,所以代码量肯定是巨大的,框架体积自然就增大了。

限制高级语言的枷锁是什么

所以我们可以发现,至少在 iOS 上面,是没有高级语言限制的,只要高级语言有这个运行在 iOS 系统上的需求,都能实现。
而 Go 如果后期希望像 Flutter 一样实现 UI 框架,也一样没有问题。限制它们的,仅仅是业务需求罢了(实际上,对于 Go 和 Node.js 的 iOS 动态库开发,需求不大,所以都是试探性发展,因为 C 和 C++ 已经足够优秀,用 Objective-C 来开发本身也够用了)。举例而言,C++ 是编写稳定后台服务的热门语言,而基于 C++ 的 Qt,就可以用来做跨平台的 GUI。而 Swift 初期被用来开发 iOS/Mac App,现在也一样可以用作服务器开发。甚至 Javascript 只是浏览器端的脚步语言,引入 V8 引擎后,JS 已经花开两朵,前端和 Node.js 后台发展的都非常棒。

我们也可以认知到,高级语言的存在,只是特定场景的需求。如果当年苹果不开发 Objective-C,用 Java 来开发 iOS App,也完全可以的,只是苹果需要一套自己的能够被私有控制的开发体系。
语言是用来完成特定场景的工作任务,如果用 Objective-C 来写服务器的 I/O 多并发,显然没有 Go 和 Node.js 的事件驱动来的吞吐量大。而 Objective-C 后期能不能实现协程、多进程等特性?当然可以,就是需要不需要而已。
限制语言功能及发展的,仅仅是它的业务场景,而不在于语言本身或者操作系统

Core Foundation 和 Foundation 的区别

我们在上面已经研究了语言和框架。框架和库的关系,在文章开头也已经说明。
这里就来研究一下 Core Foundation 和 Foundation 两个框架的区别和联系。

Core Foundation 是基于 C 开发的,Foundation 是基于 Objective-C 开发的。但是有一点,Foundation 是基于 Core Foundation 封装并实现了 Core Foundation 所没有的部分。
我们可以用下图来表示 Core Foundation 和 Foundation 的关系:

Foundation 用 Objective-C 封装了 Core Foundation 的 C 组件,并实现了额外了组件供开发人员使用。而 Core Foundation 也有一些 Foundation 没能彻底封装的功能,这些功能是 Core Foundation 特有的。

下面可以看一下 Foundation 和 Core Foundation 的组件库都有哪些:

从图中,我们可以看到,Foundation 的组件是多于 Core Foundation 的,比如 NSBundle 在 Core Foundation 就没有体现。而 NSArray 就和 Core Foundation 的 CFArray 是对应的。反过来,Core Foundation 的 CFTree 和 CFBitVector 在 Foundation 里也没有体现,或许是在其他组件中使用到了这两个算法库。

因为 Core Foundation 是 C 实现的,虽然 Objective-C 能够兼容并调用 C,但是和 C 相互通信并转换,就不那么容易了。
其实 Objective-C 和 C 直接通信就像 Go 和 C 直接通信一样,是高级语言之间的通信。Go 有 CGO 库完成了这个中间层,Objective-C 虽然基于 C,有得天独厚的优势,但是如果没有官方实现,那还是会出现高级语言之间的代沟。
举例来说,现在有下面两个 C 和 Objective-C 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
C:

typedef struct person{
int age;
char *name;
} Person;

---

Objective-C:

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
@end

我们现在创建一个 C 语言的 p 变量和 Objective-C 的 pp 对象,尝试将他们互通,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
C:
Person p = (Person *)malloc(sizeof(Person));
p->age = 10;
p->name = "GJ";

Objective-C:
Person pp = Person.new;
pp.age = 100;
pp.name = @"GJ2";

开始通信1:
pp = <Conver>p;
NSLog(@"pp name is %@", pp.name);

开始通信2:
p = <Conver>pp;
printf("p name is %s\n", p->name);

C 是面向过程的,Objective-C 是面向对象的。上面的 p 和 pp 的格式转化,目前来看的确是没有办法完成的。也就是说,缺少 <Conver> 这个环节。
Objective-C 本身是可以直接使用 C 代码的,虽然转化比较困难,但可以在不转化的前提下,直接调用 C 结构体变量进行使用。但是 C 却没有办法直接调用 Objective-C 对象了,所以这个时候可以写一个转换层,来完成这个工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Objective-C to C:

C_Person * conver(Person *p) {
C_Person *c_p = (C_Person *)malloc(sizeof(C_Person));
c_p->age = (int)p.age;
c_p->name = [p.name cStringUsingEncoding:NSUTF8StringEncoding];
return c_p;
}

---

使用:

p = conver(pp);
printf("p name is %s\n", p->name);
free(p);

我们可以看到,通过这样中转的方式,我们可以将 C 和 Objective-C 相互转换并通信。

显然,大家也发现有些费事。虽然这些转换如果真的要写 C 代码,那么就必不可少。但是如果使用 Core Foundation,那会方便很多。
我们刚才说过,Foundation 是封装的 Core Foundation,苹果开发了一个强大的功能,即桥接(Bridge)。通过桥接,可以非常方便的实现 C 和 Objective-C 的数据转换,比如下面:

1
2
3
CFMutableDictionaryRef cf_mu_dic = CFDictionaryCreateMutable(kCFAllocatorDefault, 10, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
NSMutableDictionary *oc_dic = (__bridge_transfer NSMutableDictionary *)(cf_mu_dic);
// CFRelease(cf_mu_dic);// 注意,这里因为__bridge_transfer缘故,就可以不用执行CFRelease释放内存了。

对于 Core Foundation 使用过程中产生的变量,都可以通过桥接的方式,变成 Foundation 对象。桥接帮我们做了格式转换的同时,也帮我们做了 ARC。
刚才,我们在执行 p = conver(pp) 的时候,大家注意,后面使用了 C 语言的内存释放,即 free(p)。Core Foundation 本身也有引用计数,但是没有自动计数即 ARC。所以 Core Foundation 的对象释放的时候,需要调用 CFRelease,那么在桥接到 Foundation 后,就可以使用 Objective-C 的 ARC 了,非常方便。
桥接中的__bridge/__bridge_transfer/__bridge_retain 可以很方便的帮我们做对象管理转移操作,我们就不需要手动去释放内存了。

这里还是要补充一点,基于 C 语言的 Core Foundation 之所以能作为 Objective-C 开发框架,就是上面提到的,只要是高级语言,只要有相关运行时,就可以用来开发组件 / 库 / 框架。

Core Foundation 和 C 与 Objective-C 的转换

桥接(Bridge)

我们从上面 Core Foundation 和 Foundation 之间了解到,通过桥接,可以很好的转换 Core Foundation 和 Foundation 对象。
桥接做了两件事,一个是自动引用计数,一个是格式转换

我们先说一下格式转换,因为桥接的转换局限性很大。
我们上面把 C 的 struct person 结构变量转换成 Objective-C 的 Class Person,需要自己写类似于 C_Person * conver(Person *p) 的转换函数。说明 C 和 Objective-C 之间转换本身是不能直接进行的。
但是 Core Foundation 的 CFArray、CFString 等和 Foundation 的 NSArray 和 NSString 等转换,通过桥接就可以直接转换。这是因为 Core Foundation 比较特别。Core Foundation 是苹果自己写的 C 代码,所以在桥接的时候,苹果拥有 Core Foundation 的数据结构和 Foundation 的对象细节,所以桥接可以自动完成转换工作。
而我们自己写的 C 结构体变量,和 Objective-C 对象之间,就不能很好转换了,如果我们写了 C 代码需要和 Objective-C 进行转换,就必须自己写一个中间层了。
这就是桥接对于数据格式转换的局限性,准确来说,桥接对数据格式转换,的确只在 Core Foundation 里面才有体现,毕竟如上所说,苹果自己知道 Core Foundation 和 Foundation 之间的所有细节。
这对于我们来说,其实已经完全够用了,因为我们真实业务开发场景,如果需要避免 Objective-C 的运行时带来的消耗,的确可以通过 Core Foundation 来编写代码。

下面再说桥接的另一个大杀器,那就是自动引用计数。
Core Foundation 在和 Foundation 进行转换的时候,可以通过__bridge/__bridge_transfer/__bridge_retain 进行自动引用计数控制,这个不在细说。
这里介绍 C 和 Objective-C 之间通过桥接进行引用计数控制。引用计数是针对 Objective-C 对象来说的,我们看一下 Objective-C 对象和 C 之间的转换:

1
2
3
4
5
6
7
8
9
10
11
12
Person *oc_p1 = Person.new;
oc_p1.name = @"GJ";
oc_p1.age = 10;

// 1. 假设这里因为业务代码需要,我们将OC对象转成C指针进行传递
void *c_p = (__bridge void *)(oc_p1);// c_p = 0x0000600003977500
// 2. 程序执行过程中,有各种原因可能会导致oc_p1对象计数-1,比如离开块区域等。这里我们通过置nil进行模拟
oc_p1 = nil;
// 3. 这里需要把之前由C指针存储的指针还原成Objective-C对象
__weak Person *oc_p2 = (__bridge Person *)(c_p);// 这里有"__weak"避免计数影响。因为默认是"__strong",计数会+1
// 4. 这里使用还原后的Objective-C对象
NSLog(@"%@", oc_p2.name);

上面我们使用 **__bridge** 进行 Objective-C 和 C 的强制指针转换,表示不对计数进行任何改变。
在代码执行到第 3 步的时候,就会奔溃。
因为第 1 步前,堆对象的计数为 1,第 1 步没有改变计数,堆对象计数还是 1。
经过第 2 步,堆对象不在有引用计数了,所以堆对象就被释放了。
在第 3 步,想要使用 C 的 c_p 指针的时候,这个指针所存储的 0x0000600003977500 堆地址,已经变成野指针了,使用的时候直接会崩溃。

下面看下 **__bridge_retained __bridge_transfer** 的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Person *oc_p1 = Person.new;
oc_p1.name = @"GJ";
oc_p1.age = 10;

// 1. 假设这里因为业务代码需要,我们将OC对象转成C指针进行传递
void *c_p = (__bridge_retained void *)(oc_p1);// c_p = 0x0000600003977500
// 2. 程序执行过程中,有各种原因可能会导致oc_p1对象计数-1,比如离开块区域等。这里我们通过置nil进行模拟
oc_p1 = nil;
// 3. 这里需要把之前由C指针存储的指针还原成Objective-C对象
__weak Person *oc_p2 = (__bridge_transfer Person *)(c_p);// 这里有"__weak"避免计数影响。因为默认是"__strong",计数会+1
// 4. 这里使用还原后的Objective-C对象
NSLog(@"%@", oc_p2.name);
// 5. 这里继续将C指针存储的指针还原成Objective-C对象
__weak Person *oc_p3 = (__bridge_transfer Person *)(c_p);

这里代码运行情况分析如下:
第 1 步之前,堆对象计数为 1。经过第 1 步后,**__bridge_retained 会使得计数 + 1,堆对象计数变成 2。
经过第 2 步,堆对象计数变成了 1。
第 3 步
__bridge_transfer** 会使得计数 - 1,堆对象计数变成 0,堆对象被释放。
第 4 步会打印 null,因为 oc_p2 本身为 null。
第 5 步,程序崩溃,因为 c_p 指针存储的堆对象已经释放,指针此时为野指针。

从上面两个例子,我们可以看到,在和 C 进行赋值的过程中,桥接帮我们做了引用计数的工作。和 Core Foundation 的转换过程中的计数规则是一样的。
我们在赋值过程中,使用 **__bridge_retained __bridge_transfer 可以有效的降低崩溃风险,因为这两种 bridge 方式,帮我们做了引用计数的加和减。
单独进行
__bridge 赋值的时候,引用计数没有改变,相当于同一时间,有多个指针指向堆对象,但是对象的计数却和指向指针的个数不一致。如果对象被释放,很可能还有指针在指向,这个时候使用就会发生野指针。
通过
retained transfer**,在赋值过程中,加 1 和减 1 是同步的,这样可以有效降低对象计数和指向指针个数不一致的野指针风险。

通过桥接给 C 和 Objective-C 赋值的风险

通过上面两个例子,或者写更多其他 Objective-C 和 C 指针赋值的代码后,就会发现这样写代码的风险非常大。最大的风险就是野指针和内存不释放。
如果完全写 Objective-C 的代码,OC 运行时已经帮我们处理了引用计数和对象释放后指针自动变 nil 问题,所以我们大概率不会出现野指针和内存不释放情况(OC 运行时的 Weak 表帮我们处理了对象释放后指针自动变 nil。而 Objective-C 的引用计数的内存管理方式,也容易因为循环引用导致内存不释放,这是引用计数管理内存的天然缺陷)。
但是在 C 赋值嵌入进来后,即使通过桥接进行计数管理,也依旧摆脱不了随时崩溃的风险。原因就是因为对象被释放导致野指针随时可能会发生,或者对象无法释放导致内存泄漏。
对于经常写 C 代码的程序员来说,应该不会担心这些问题,因为他们已经习惯内存需要手动管理。被拥有自动内存释放机制娇生惯养的程序员们,就需要注意这个风险了。
比如下面代码,就很发生内存泄漏:

1
2
3
4
5
6
7
8
9
10
11
12
13
void func {
int number = 10;
char *c_chars = (char *)malloc(sizeof(char) * number);
memset(c_chars, 0, number);
int i = 0;
for (; i < number - 1; ++i) {
*(c_chars + i) = 'a' + i;
}

NSString *oc_str = [NSString stringWithCString:c_chars encoding:NSUTF8StringEncoding];
free(c_chars);
NSLog(@"%@", oc_str);// 打印“abcdefghi”
}

上面代码中,前面用 C 语言申请了 10 个字节的堆空间,然后开始赋值被转成 Objective-C 的 NSString 对象。
oc_str 是 ARC 控制的,出了 func 函数作用域,内存就会被释放。可是如果忘记写 free(c_chars); 这行代码,就会导致 10 字节的内存泄漏。像这样的内存细节,防不胜防的同时又会慢慢耗尽内存空间。

所以,如果需要避免 Objective-C 的运行时带来的消耗而想采用 C 写业务,最好使用 Core Foundation,它和 Foundation 之间的桥接非常完美,一般不会出问题。而自己写 C 进行混用,野指针和内存不释放是挥之不去的地雷

Core Foundation 的使用

Core Foundation 只是一个非常优秀的框架,但是苹果用 C 写的 Core Foundation 框架和 Objective-C 写的 Foundation 框架,不是 iOS 框架的全部。框架是库的抽象,用 Golang 等其他高级语言,一样可以写出优秀的框架。Dart 就是举足轻重的例子。

上文的截图中,给出了 Core Foundation 框架里面都有哪些好用的组件,比如 CFString、CFDate 等。下面的一些示例,是用 Foundation 不好实现的。

CFRunloop 介绍

iOS 的 Runloop 水还是很深的。我也写了 Runloop 的一篇文章,一直在草稿中未能发布。因为牵涉面太广,如事件驱动、线程休眠、自动释放池、UI 刷新等。通过 Runloop 能够更加清楚明白的理解 App 运行的原理,也可以做非常多有用的东西,如主线程卡顿监控、线程保活等。
Foundation 提供了 NSRunloop 供我们开发人员使用,但是 NSRunloop 有一个大坑,对于不了解 Runloop 的开发人员来说,很容易陷进去。
网上有很多 Runloop 的介绍,在介绍让线程执行一段时间的时候,会使用 [[NSRunLoop currentRunLoop]run]。我揣摩本意,发现他们并不是想要让线程永久长活,但是却使用了 run 函数。这样会使得当前线程永远无法释放,是永远。因为 NSRunloop 里面 run 函数是对 CFRunLoopRun() 函数的 true 循环封装,当结束一次循环后,NSRunloop 会立刻再次调用 CFRunLoopRun() 函数,没有任何办法可以销毁当前线程的 Runloop。这样,项目里面就永远的多出来一条可能已经不再需要的线程。主线程就使用的这个逻辑。
在 CFRunnloop 里面,仅有两种方式安全启动线程的 runloop,分别为 CFRunLoopRun()CFRunLoopRunInMode,其中 CFRunloopRun 还是语法糖。这两种启动方式,都是一次循环,客户端可以自行控制啥时取消 Runloop,有效的降低 Runloop 未知风险。相关源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CFRunloop.c

// RunLoop 运行循环
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
// 调用RunLoop执行函数(默认运行default Mode)
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

// 切换并运行到对应的mode(运行modeName参数对应的mode)
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

iOS 的 runloop,就是通过调用 CFRunLoopRunSpecific()->__CFRunLoopRun() 实现的。其中,NSRunloop 的 run 函数,相当于下面代码:

1
2
3
4
5
- (void)run {
do {
CFRunloopRun();
} while(true);
}

所以,线程永远也无法销毁。因为 CFRunloopRun () 函数会在 Mode 切换或者手动调用 CFRunLoopStop() 等情况下执行完毕,但是外部的 do-while true 循环,永远结束不掉。

这里,如果需要写 Runloop 相关的代码,我强烈建议使用 CFRunloop,而不要使用 NSRunloop。相比来说,CFRunloop 提供了比 NSRunloop 更加细致化的 Api,相比之下,NSRunloop 就寥寥无几了。

下面是我写的一些 CFRunloop 测试代码,因为 Core Foundation 是 C 语言写的,所以里面的组件都是面向过程的调用方式,和面向对象有些不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(createThread) object:nil];
[self.thread setName:@"test thread 1"];
[self.thread start];

- (void)createThread {

NSLog(@"the thread is [%@]", [NSThread currentThread]);

// NSRunLoop *runloop = [NSRunLoop currentRunLoop];
// self.port = NSMachPort.new;
// self.port.delegate = self;
// [runloop addPort:self.port forMode:NSRunLoopCommonModes];
// [self addObserver];


CFRunLoopAddCommonMode(CFRunLoopGetCurrent(), (__bridge CFStringRef)@"dadada");

CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^{
printf("abc");
});

NSLog(@"the runloop is [%@]", [NSRunLoop currentRunLoop]);

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
// CFRunLoopRunInMode(kCFRunLoopDefaultMode, 5, NO);
NSLog(@"---end");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
主线程下调用"addObserver",可以实时查看主线程的Runloop状态

- (void)addObserver {
CFRunLoopObserverRef runloopObserver = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0,
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

switch (activity) {
case kCFRunLoopEntry:
NSLog(@"--1 即将进入loop kCFRunLoopEntry--");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"--2 即将处理timer kCFRunLoopBeforeTimers--");
break;
case kCFRunLoopBeforeSources:
NSLog(@"--3 即将处理source kCFRunLoopBeforeSources--");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"--4 即将休眠 kCFRunLoopBeforeWaiting--");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"--5 即将从休眠唤醒 kCFRunLoopAfterWaiting--");
break;
case kCFRunLoopExit:
NSLog(@"--6 即将退出loop kCFRunLoopExit--");
break;
default:
break;
}
});

CFRunLoopAddObserver(CFRunLoopGetCurrent(), runloopObserver, kCFRunLoopCommonModes);
}

CFDictionary 介绍

Foundation 里面有 NSDictionary 与之对应,如果我们希望用我们自定义的对象为 key,存储与 NS 字典中,直接存储是不行的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 定义Person类

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
- (NSString *)description {
return [NSString stringWithFormat:@"name:%@, age:%lu", self.name, (unsigned long)self.age];
}
- (void)dealloc {
printf("Person Dealloc.\n");
}
@end

- (void)func {
Person *p = Person.new;

NSMutableDictionary *oc_dic = NSMutableDictionary.new;
[oc_dic setObject:@"" forKey:p];// 这里会崩溃
}

上面代码中,如果我们把自定义 Person 类的对象 p 作为 key 存储到 NSMutableDictionary 中,运行时是会崩溃的。
因为 Foundation 规定,字典的 key 必须要实现 NSCoping 协议,字典在添加属性的时候,是调用 [key copy] 作为字典 key 的。
改写如下:

1
2
3
4
5
6
7
8
9
10
11
12
@interface Person : NSObject<NSCopying>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
- (id)copyWithZone:(nullable NSZone *)zone {
Person *p = Person.new;
p.name = self.name;
p.age = self.age;
return p;
}
@end

如果我们使用 Core Foundation,就可以避开这个限制,即 Person 类不需要实现 NSCoping 协议,如下:

1
2
3
4
5
Person *p = Person.new;

CFMutableDictionaryRef cf_mu_dic = CFDictionaryCreateMutable(kCFAllocatorDefault, 10, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFStringRef cf_str_value = CFSTR("value");
CFDictionaryAddValue(cf_mu_dic, (__bridge const void *)(p), cf_str_value);

CFDictionary 默认会对 Key 和 Value 做 retain,所以我们使用 **__bridge 即可。当 p 被当作 key 加入 cf_mu_dic 后,p 的引用计数已经变成 2 了。
如果我们使用
__bridge_retained**,如下:

1
2
3
4
5
6
7
8
9
Person *p = Person.new;

CFMutableDictionaryRef cf_mu_dic = CFDictionaryCreateMutable(kCFAllocatorDefault, 10, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFStringRef cf_str_value = CFSTR("value");
CFDictionaryAddValue(cf_mu_dic, (__bridge_retained const void *)(p), cf_str_value);

p = nil;
CFDictionaryRemoveAllValues(cf_mu_dic);
// 这里因为"__bridge_retained"缘故,p置空和移除CFDictionary所有元素后,对象的引用计数还是1,所以内存泄漏。

这里因为”__bridge_retained” 缘故,p 置空和移除 CFDictionary 所有元素后,对象的引用计数还是 1,所以内存泄漏

CFDictionary 的 key 不需要实现 NSCoping 协议这一特性,YYModel 就有使用,这也是 YYModel 使用 CFDictionary 的最终原因:

1
2
3
4
5
6
7
8
9
YYMemoryCache.m

_dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key));
CFDictionaryGetCount(_dic);
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));

CFRelease(_dic);

YYModel 使用 CFDictionary,就是因为缓存对象是各式各样的,极大可能都是没有实现 NSCoping 协议的。
因为 YYModel 通过一个__unsafe_unretained 类型的双向链表来保存对象,所以 YYModel 需要一个容器来持有缓存对象防止被提前释放。
为了加快查询对象的速度,使用查找复杂度为 1 的 hash map 结构即字典 (CFDictionary),而非数组 (CFArray)。

CFDictionary 还有一个巨大特性,是可以吊打 NSDictionary 的,那就是可以自行控制引用计数。下图表示 CFDictionary 的创建函数及相关函数调用:

我们举例一下,

1
_dic = CFDictionaryCreate(CFAllocatorGetDefault(), keys, values, n, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);

这里创建一个_dic 变量,这里每个 key 都会经过 kCFTypeDictionaryKeyCallBacks 结构体获取到 retain/release/copyDescription/equal/hash 进行函数调用。
比如,如果两个 key 一样,那么 equal 就会比对出 true,第二个 key 元素就会被过滤。注意,这里和 NSDictionary 不一样,NSDictionary 是完全 hash map table,两个元素如果一样,就会通过拉链法或者开放寻址法进行存储。但是在 CFDictionary 里面,如果两个元素 equal 为 true,则会过滤另一个。
然后,一个 key 被存储的时候,会调用 retain 函数进行引用计数 + 1。这里调用的是系统默认的,如图中所示,如果我们用自己的 retain 函数代替系统的,就可以实现引用计数的多变性:

1
2
3
void * Custom_CFDictionaryRetainCallBack(CFAllocatorRef allocator, const void *value) {
return value;// 系统默认为return CFRetain(value);
}

我们通过改写一个 key retain 函数,就可以改变 CFDictionary 的 key 在 retain 时候的计数是否 + 1。
如果执行 CFDictionaryRemoveAllValues(cf_mu_dic);,则字典中所有元素都会被移除,这个时候每个 key 都会被调用 release 函数执行引用计数 - 1 操作,我们也可以重写:

1
2
3
void Custom_CFDictionaryReleaseCallBack(CFAllocatorRef allocator, const void *value) {
// 系统默认为CFRelease(value),现在啥都不做
}

我们改写 key release 函数,就可以使得 key 被移除释放的时候,引用计数不在 - 1。

这里,我们的操作性非常强,我们可以提供自己的函数地址,就可以实现多样化的 CFDictionary 引用计数逻辑,详细代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const void * custom_dictionary_key_retain(CFAllocatorRef allocator, const void *value) {
return value;
}

void custom_dictionary_key_release(CFAllocatorRef allocator, const void *value) {
}

const void * custom_dictionary_value_retain(CFAllocatorRef allocator, const void *value) {
return value;
}

void custom_dictionary_value_release(CFAllocatorRef allocator, const void *value) {
}

- (void)func {

CFDictionaryKeyCallBacks custom_dictionary_key_call_backs = {
0,
custom_dictionary_key_retain,
custom_dictionary_key_release,
CFCopyDescription,
CFEqual,
CFHash,
};

CFDictionaryValueCallBacks custom_dictionary_value_call_backs = {
0,
custom_dictionary_value_retain,
custom_dictionary_value_release,
CFCopyDescription,
CFEqual,
};

Person *p = Person.new;

CFMutableDictionaryRef cf_mu_dic = CFDictionaryCreateMutable(kCFAllocatorDefault, 10, custom_dictionary_key_call_backs, custom_dictionary_value_call_backs);
// 这里的cf_mu_dic对于key和value的引用计数完全改变了,key和value在加入和移除的时候,引用计数都不会被CFDictionary改变
CFDictionaryAddValue(cf_mu_dic, (__bridge const void *)(p), (__bridge const void *)(p));
}

这里,我们描述了很多 CFDictionary 相比于 NSDictionary 的不同,有如下:

  1. CFDictionary 的 key 不需要实现 NSCoping 协议,NSDictionary 的 key 如果没有实现 NSCoping 协议,则会运行时崩溃。YYModel 等开源库主要就是使用了这个特性。
  2. CFDictionary 的 key 如果相等,在元素不会被插入。NSDictionary 则会通过拉链法开放寻址法进行数据存储。
  3. CFDictionary 的 key 和 value 的引用计数,都可以自行控制。NSDictionary 的 key 用的 [key copy],value 用的 retain。

所以,CFDictionary 相比 NSDictionary 来说,扩展性也更强。


最近一直在喝 Luckin Coffee,最近因为收入造假,快要被纳斯达克下市了。