本文翻译自The Amazing Adventures of NSArray
译者注:原文对NSArray进行了深入的探究,挖掘了许多令人不可思议的东西,但也有那么些错误的地方(当然只是现在运行起来会出问题),在文中我会指出。
首先我们用两行奇怪的代码开始这篇文章。
|
|
事实证明上面两行的返回值都是真。没事请放松,但是准备一个篮子防止你的脑子发生泄露,这比疯狂还要疯狂。
先看看Foundation
跟其他许多语言相比,Objective-C真的是简单时髦。它的句法实际上很简单,很多模式的实现都耍了一些花招(重新看看上面的两行代码)。但是通过深入的研究探索那些苹果实现的类,比较容易洞悉一些更加通常的模式和一些花招,这些模式和花招可能在一开始会让你觉得有些奇怪。
我们仅仅研究NSArray, NSNumber, NSString, NSDictionary, and NSObject(或者说Foundation),就将发现无数令人感兴趣的,具有指导意义的,有乐趣的东西。为了这个目的,我们将聚焦NSArray中的那些很”傻逼”的东西。本篇文章我们将探索下面这些东西:
- 字面量的句法
- 下标
- 类簇
- 免费桥
- 索引的成员变量
字面量
字面量是理解NSArray相关东西最简单的魔法,对于Objective-C来说是相对新的东西,在clang3.5中被加入,这种句法相当简单:
|
|
其实就是逐字翻译成下面的代码,以遵从数组的构建:
|
|
不幸的是没有钩子(hooks)让我们去利用这种句法。它仅仅对于NSArray和NSDictionary等可用,所以在这方面没有太多能探究的。
下标
Clang3.5同样提供了方括号下标的支持
|
|
这其实很简单,仅仅是依据下标进行了一个简单的转换,这个下标可以是整数类型(indexed)也可以是一个对象(keyed)。
|
|
这种代码虽然和以前相同,但是却更容易阅读。感谢NSArray和NSDictionary满足我们的期待实现了这些方法,如此事物突然变得很美好。
把内存当成一个连续的数组
我们构建一个简单的数组。(注意下面所有的代码都是在64位系统上编译运行,不过用iOS或OS X都可以)。
|
|
我们得到的对象实际上只是一个指向一块内存的指针(用星号来标记),通常我们认为一个指针性的Objective-C对象就是一个实现的细节,但是猜测其指向的那块内存的组成很有意思。我们先从这块内存的大小入手。
|
|
很好,这块内存有16个字节。像其他任何OC对象一样,最开始的8个字节是isa指针,所以只有8个字节值得我们探索。在这8个字节中,我们希望能够找到一些和数组结构相关的信息(例如数组长度或是一些对象的指针或是其他一些类似的东西)。
我们看看isa指针后面的8个字节是否是一个指针或是一些内置的数据结构例如盛装数组内容的缓冲。(如果你不确定为何要进行bridge转换,你或许需要看看我写的另一篇文章Objective-C ARC)
|
|
情形很乐观,我们使用的数组的长度正好是3,但这可能是个巧合,我们来试试其他数组的长度。
|
|
完美,我们发现了数组的长度!但是可能长度不是存在全部的8个字节中而是仅仅用了开始的4个字节。所以我们来打印一下第12到15字节,一个4字节的整型。
|
|
很不幸,从打印结果我们看不出什么,也许这写比特没被使用,也许长度真的储存在64位的内存里。
寻找余下的对象
无论如何,我们发现了一个严重的问题。在那16字节的内存里储存了isa指针、数组的长度,仅此而已。那么数组里的对象到底在哪里?
也许有一个全局的map储存着数组地址和数组内容的映射。然而如果是那样的话我将停止探索这极度愚蠢的设计。幸运的是不是那样。让我们回忆一下,我们使用class_getInstanceSize
得到数组实例的大小,但如果实际大小更大呢,我们看看这个数组所占的空间到底有多大而不是猜测。
|
|
什么鬼?这个类的实例应该只有16字节,这额外的32字节是从哪里来的,我们先跳过这个问题,看看这32字节里有什么。
|
|
译者注:原文的代码有些问题,是强转为uint32_t然后打印,但是运行的时候会出错,在64位系统中oc对象的地址也应该是64位的,这里明显是有问题的,我开始还以为原作者使用的环境是32位,后来发现他在文章开头就注明使用的环境是64位,所以这里应该是弄错了。
这是和NSArray内存布局等价的结构体。
|
|
实例的内存分配
既然我们已经明白了内存布局,下面我们需要搞明白为什么一开始得到的内存是16字节。不论是谁做的分配工作很明显不可能只使用了class_getInstanceSize
。有一块内存在后来被加在了最后。我们需要去翻翻
在这个头文件中似乎只有一个用于创建实例的方法。
|
|
这个方法允许你定义一个数字表示在对象的最后开辟额外空间的大小。设置一个符号断点在这个函数上,从LLDB中可以看到extraBytes这个参数总是n*sizeof(id),至此这其中神秘的面纱就被揭开了。同时也有一个方法用于获取这个额外内存的地址,下面看一下具体用法。
|
|
突然之间一切都明朗起来,我们弄清楚了NSArray的内存布局。似乎没有什么令人疑惑的东西遗留下来了,但是仔细想想class_getInstanceSize
是什么时候被调用的呢。
我们直接使用alloc-init方法而不是用字面量进行实例化操作,这样我们就能弄清楚发生了什么。
|
|
在调用alloc时使用的是class_getInstanceSize
方法返回的值,但是如何知道到底要分配多大的内存?关于这个数组有多大的信息直到init方法调用时才出现。
有一种可能是init方法释放了一开始分配的内存然后又重新分配了一段正确的内存(可以看看An Aside on Init’s Consumption of Self去弄明白为什么这在语义上是可能的),如果是这样做的那真是太浪费了。
我们先看看alloc方法返回的对象。我们要再一次使用MRR因为ARC不允许你单独使用alloc方法。
|
|
毫无疑问init方法返回了一块新内存。多么浪费。。。:(也许alloc返回的是一些有用的东西这样就不会产生浪费了。
译者注:作者在这里强调浪费,应该是为了引出下一节的内容,先抑后扬的手法吧
类簇&alloc返回的工厂类
alloc方法在被实现的时候暗中返回了一个工厂类(我们想的是这是一个全局的单例)。然后实现工厂类的init方法返回你所需要的一个新的对象。这就是NSArray所做的事情。每次你调用NSArray的alloc方法,它都会返回一个NSPlaceholderArray的全局实例,可以说是数组的魔法工厂。
alloc返回一个工厂的技巧是十分普遍的。它允许你推迟一切分配的工作直到init方法调用你有了全部的信息去构建你所需要的实例。如此你就可以耍一些花招例如在一个实例后增加一些额外的字节,或是选择特定的子类去实例化。
这种使用工厂类去实例化一个特定子类的模式叫做类簇。你可以任性的将一个抽象类的接口用在任何子类上。
- NSString是最适合用来实例化底层字符的类簇。
- NSNumber是一个返回全局单例(例如@YES和@NO)、标签指针或是CFNumber的类簇。
- NSDictionary和其他容器的类也是类簇。
类簇的一个最基本的概念是你不可能真正获取一个抽象基础类的实例,而是一些私有的子类。在前面的例子中,这个子类是__NSArrayI,用于替代NSArray的子类。
|
|
事实上在Foundation框架中有超过20个这样的NSArray的子类。真是一个好大的类簇。😀
超过20个真的很多,不知道作者是如何得到这个值的,可能是一样一样试出来的吧!真是厉害了,其实我也发现了几个这样的类比如:NSPlaceholderArray、 NSSingleObjectArrayI。
NSPlaceholderArray
现在我们已经知道NSArray的alloc方法返回一个工厂,在深入探究这个工厂对象也是很值得的。很明显这个工厂包含所有的init方法(因为它使用这些方法来进行实例化)但具体是怎样的呢?
嗯,它绝对是一个NSArray,从alloc函数返回的东西不会太不着调。因此假设NSPlaceholderArray是NSArray的子类是很正常的,
NSMutableArray的alloc方法也是返回一个NSPlaceholderArray的单例(尽管和前面的不是同一个),这意味着NSPlaceholderArray也一定是NSMutableArray的子类。他们继承关系如下:
NSArray <- NSMutableArray <- NSPlaceholderArray
这导致下面两行返回值都是真
|
|
下面两行也是一样:
|
|
因为NSNumber和NSValue都是返回同一个工厂的类簇,他们的继承关系如下:
NSValue <- NSNumber <- NSPlaceholderValue <- NSPlaceholderNumber
如果你仔细观察你会发现NSValue的alloc返回的是NSNumber的子类,所以我们能不能用NSPlaceholerValue去实例化一个NSNumber?
|
|
好吧!这家伙比我们想象中聪明。
结束
希望你享受这对于OC类内部不寻常实现的探索。深入苹果的类揭示了许多常见和不常见的花招。研究他们不仅仅是乐趣还能帮助我们更好的理解这门语言究竟是怎样的。如果这些改变了你写的代码,我很乐意知道同在一片星空下的你正在做什么。但至少你自己是知道的。