不少 iOS 项目里都有 C++ 代码的痕迹,Objective-C 和 C++ 虽然都是 C 的 superset,但二者在语言特性上存在很大差异,Objective-C 的 runtime 使其语言的特性更丰富更易使用,但代价是会增加性能损耗以及编译后的 binary size。
很多成熟项目开发到一定阶段,会关注一些关键指标,比如 App size,现在超过 100 M 的 App 比比皆是,而 App Store 上超过 150 MB 的 App 只能通过 Wifi 下载,当常规的瘦身手段用尽之后,App size 每一个 MB 的减少都弥足珍贵,这篇文章向 iOS 开发者介绍 C++ 的 zero cost abstraction 特性,在特定的场景下使用能起到立竿见影的疗效:减小 iOS App 的 binary size,给 App 瘦身。
zero-cost abstraction
Objective-C 和 C++ 同为面向对象语言,我们通过对象来抽象世界中的概念,但 Objective-C 的抽象伴随着代价,抽象越多,定义的类越多,最后编译出的 binary size 也就越大,而 C++ 却没这方面的烦恼,无论你定义多少类,设计多少 component,采用多少设计模式,并不会增加最后的 binary size,也就是所谓的 zero-cost,对 iOS 开发者来说,这种理论听起来可能有些反常识,但如果你是先学习 C, C++,再接触 Objective-C 的 runtime,理解起来再直白不过。
举例来说,假设我们从服务器收到一段请求 user 信息的 response,一般我们会将 response 还原成一个业务 model 对象,user 类的定义如下:
class User { int gender; int age };
如果使用 C++ 来定义这个类,在 C++ 编译器的眼里,这个类的全部信息不过是两个连续存在于内存空间上的 4 个字节(假设一个 int 占 4 字节)。我们访问 user 对象的代码:
int age = user->age;
会被编译器翻译成一段类似如下的内存访问代码:
r11 = *(r10 + 0x4)
在编译器眼里没有类的概念,只有内存地址,偏移量,以及读写操作。即使我们加入更多的抽象,比如把 User 类放进 Car 类里面,再把 Car 放进 City 类里,当我们使用 city->car->user->age 时,编译器依旧会将代码翻译成直白的 memory access。
如果我们使用 Objective-C 来书写上述代码,情况就完全不一样了,熟悉 Objective-C runtime 的同学明白接下来会发生一系列操作,编译后的代码里,Objective-C 的 runtime 会先尝试给 user 对象发送 message(如果是通过 property 访问),需要通过 user 对象的 isa 指针找到 User 类定义,再通过 selector 在 cache 里找到 IMP 地址,最后才从函数返回需要操作的目标内存地址。我只列出了关键的几步,中间其实省略了 n 个流程,类越多,抽象的层次越多,步骤也就越多,这是由于 Objective-C 需要将 class 的定义编译进最后的 binary 里,需要依赖 class 的信息来实现 runtime 的一些机制,class 越多,最后生成 binary 自然也就越大。
简而言之,大部分编程语言和 Objective-C 类似,由于需要在 binary 中保存 class 的信息,而将抽象的成本带入了编译后的机器码。通过上面的分析我们也不难发现 zero-cost abstraction 的好处体现在两方面,一是 binary 更小,二是运行时更高效(没有一层层的中转)。
C++ 的 zero-cost 特性得益于编译器的高效实现,我们在代码里定义的所有类,最后都会被编译器降维,高楼被夷为平地,信息却不会丢失,编译器用一片二向箔将面向对象的世界压扁成一幅画,画里的机器码仍然能严格准确的表达我们的意图。
我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2h7509o9c6m88