深入了解Swift

发布于:2014-06-20 15:52阅读数:

Swift是苹果新的编程语言,很多人都认为它可以替换Objective-C,但事实却并非如此。我花了一些时间对Swift运行时和二进制文件进行了逆向工程,不过(不过表转折,可去掉)发现了很多有趣的事

本文由CocoaChina翻译小组成员 昨夜城 (微博)翻译,dada(github主页)校对。欢迎加入我们的译者小组(support@cocoachina.com,并注明社区ID、工作状态、电话以及个人博客等任何让我们更了解你的方式)
 
原文:Inside Swift
 
Swift是苹果新的编程语言,很多人都认为它可以“替换”Objective-C,但事实却并非如此。我花了一些时间对Swift运行时和二进制文件进行了逆向工程,不过(不过表转折,可去掉)发现了很多有趣的事。总结起来就是:Swift是没有消息机制的Objective-C。
 
面向对象
不管你信不信,Swift对象实际上是Objective-C对象。在Mach-O二进制文件中可以发现__objc_classlist部分包含了每个类的二进制数据,结构类似:
  1. struct objc_class { 
  2.     uint64_t isa; 
  3.     uint64_t superclass; 
  4.     uint64_t cache; 
  5.     uint64_t vtable; 
  6.     uint64_t data; 
  7. }; 
 (注:所有的结构都是在64位机器上编译)
 
注意数据入口,它指向一个列出了类的方法、实例变量以及协议等等的结构。通常情况下,数据是8个字节对齐的。然而,在Swift的类中,最后一个数据将只占有1个字节。
 
Swift类的实际结构有点奇怪。Swift类没有类似于Objective-C的方法,这点会在后面说明。Swift类中变量被存储为一个实例变量。在Swift中利用getter和setter方法修改这个实例变量的值(译者注:此处的getter和setter并不实际存在,只是抽象为实例变量的访问和修改入口,是一种规范,在下文中不对getter和setter进行特别翻译)。奇怪的是,Swift类的实例变量其实是没有类型编码。通常应该指向类型编码的指针为NULL,这想必是因为Objective-C的运行时不会亲自去处理Swift变量。
 
继承
正如你所期望的那样,Swift同样拥有类的继承与派生。比如,在Swift中,Shape(形状)的子类Square(方形)在Objective-C类中同样是Shape的子类。然而,如果Swift中的类没有父类会怎样?
 
例如:
  1. class Shape { } 
在这种情况下,Shape类是一个SwiftObject的子类。而类swiftobject是一个类似于Objective-C中NSObject这样的一个基类。它没有父类,这意味着isa变量指向其本身。其目的是利用Swift运行时方法处理像分配和释放内存这样的事,而不是标准的Objective-C运行时。比如说,改为- (void)retain不是调用objc_retain,而是调用swift_retain
 
类方法
就像我前面提到的,Swift对象的类没有方法。相反,他们使用像C++一样的函数、重载以及其他替代。这可能是为什么Swift要比Objective-C快得多;这里不怎么需要用objc_msgsend来寻找和调用方法的实现。
 
在Objective-C中,方法实现类似这样:
type method(id self, SEL _cmd, id arg1, id arg2, ...)
 
Swift方法是非常相似的,但稍有不同的是参数布局,self只能作为最后一个参数传递,这里没有选择器。
type method(id arg1, id arg2, ..., id self)
 
虚函数表
就像在C++一样,Swift的类有一个虚函数表,在虚表里列出了类中的方法。它位于二进制的数据后面,看起来像这样:
  1. struct swift_vtable_header { 
  2.     uint32_t vtable_size; 
  3.     uint32_t unknown_000; 
  4.     uint32_t unknown_001; 
  5.     uint32_t unknown_002; 
  6.     void* nominalTypeDescriptor; 
  7.     // vtable pointers 
这里需要说明的是,Swift类的虚函数表只能当它在编译时可见的情况下使用。否则将会报错。
 
Name Mangling(命名重整)
Swift在各自的标志中保存元数据的函数(以及更多),这就是所谓的name mangling。该元数据包括函数名,属性、模块名称,参数类型,返回类型等等。
  1. class Shape{ 
  2.     func numberOfSides() -> Int { 
  3.         return 5 
  4.     } 
以此为例:Simpledescriptio方法改编(重载)的名字是_tfc9swifttest5shape17simpledescriptionfs0_ft_si。这里分解下:
t–所有Swift符号的前缀。所有事情皆始于次。
F–函数。
C一类的函数。(方法)
9swifttest–带有一个前缀长度的模块名称。
5shape–函数所属类的名称,带有一个前缀长度。
17simpledescription–函数名。
f–函数属性。在本例中,它是“f”,这是一个常见的函数。
s0_ft–我不确定这意味着什么,但它似乎是标记参数的开始,并返回类型。
“_”该下划线把参数类型和返回类型分别开来。由于函数没有参数,所以它直接跟在S0_FT后边。
S--这是返回类型的开始。“S”代表Swift;返回类型是一个Swift内建类型。下一个字符决定类型。
i–这是Swift的内建类型。一个小写的“i”,可代替int (小写的“I”代表Int)。
 
函数属性
 
Swift的内置命令
 
  
命名重整并不仅仅只用于函数,但我只是给出一个简短的概述。
 
函数连接
这句子有足够的语义,让我们来到一个有趣的部分!如果我们有这样的一个类:
  1. class Shape { 
  2.     var numberOfSides: Int; 
  3.    init(){ 
  4.            numberOfSides = 5; 
  5.        } 
  6.    } 
 
我们说我们想把numberofsides 改为4。有多种方式来做这个。我们可以使用MobileSubstrate连接到getter方法,并改变返回值,像这样:
  1. int (*numberOfSides)(id self); 
  2.  
  3. MSHook(int, numberOfSides, id self){ 
  4.     return 4; 
  5.  
  6. %ctor{ 
  7.     numberOfSides = (int (*)(id self)) dlsym(RTLD_DEFAULT, "_TFC9swifttest5Shapeg13numberOfSidesSi"); 
  8.     MSHookFunction(numberOfSides, MSHake(numberOfSides)); 
如果我们创建一个Shape实例,并打印出变量numberofsides的值,我们看到结果为4!这并不是很坏,是吧?现在,我知道你在想什么;“难道不是应该返回一个对象,而不是字符“4”吗?”
 
的确,在Swift中,大量的内建类型都是文本。例如Int,与C中的Int一样。注意,字符串类型有点奇怪,这是一个little-endian UTF-16字符串,所以没有可以使用的C字面量。
 
让我们做同样的事情,但是这一次,我们会连接setter代替getter。
  1. void (*setNumberOfSides)(int newNumber, id self); 
  2.  
  3. MSHook(void, setNumberOfSides, int newNumber, id self){ 
  4.     _setNumberOfSides(4, self); 
  5.  
  6.  
  7.  
  8. %ctor { 
  9.     setNumberOfSides = (void (*)(int newNumber, id self)) dlsym(RTLD_DEFAULT, "_TFC9swifttest5Shapes13numberOfSidesSi"); 
  10.     MSHookFunction(setNumberOfSides, MSHake(setNumberOfSides)); 
再试试跑一次程序吧…结果仍然是5。你一定会问发生什么事了?这是因为,在Swift的某些地方函数是内联的。该类的构造函数是一个这样的函数。它直接设置numberofsides ivar。所以,只有数值在顶部的代码中被再次设置时才会调用setter方法。执行调用后,我们会的到4这个值。
 
最后,让我们通过直接设置变量实例改变numberofsides的值。
  1. void (*setNumberOfSides)(int newNumber, id self); 
  2.  
  3. MSHook(void, setNumberOfSides, int newNumber, id self){ 
  4.     MSHookIvar<int>(self, "numberOfSides") = 4; 
  5.  
  6.  
  7.  
  8. %ctor { 
  9.     setNumberOfSides = (void (*)(int newNumber, id self)) dlsym(RTLD_DEFAULT, "_TFC9swifttest5Shapes13numberOfSidesSi"); 
  10.     MSHookFunction(setNumberOfSides, MSHake(setNumberOfSides)); 
这样同样可以达到效果。虽然并不推荐这样做,但它仍是可行的解决方案。
 
这就是我要写的所有东西。虽然有很多其他的东西,但我不知道是否有足够精力去写。在这篇文章中许多东西是会变化的。它们只是我使用Swift从运行时和二进制文件逆向工程得到的。但我觉得这还不错。这意味着,MobileSubstrate不会随着Objective-C消亡,仍有调整的空间。
 
如果你发现更多关于Swift是如何工作的细节,不要犹豫,请让我知道!

CocoaChina是全球最大的苹果开发中文社区,官方微信每日定时推送各种精彩的研发教程资源和工具,介绍app推广营销经验,最新企业招聘和外包信息,以及Cocos2d引擎、Cocos Studio开发工具包的最新动态及培训信息。关注微信可以第一时间了解最新产品和服务动态,微信在手,天下我有!

请搜索微信号“CocoaChina”关注我们!

搜索CocoaChina微信公众号:CocoaChina

顶部