NSArray的奇妙探索之旅(深入探究NSArray)

作者:代培
地址:http://blog.csdn.net/dp948080952/article/details/53565702
转载请注明出处

本文翻译自The Amazing Adventures of NSArray

译者注:原文对NSArray进行了深入的探究,挖掘了许多令人不可思议的东西,但也有那么些错误的地方(当然只是现在运行起来会出问题),在文中我会指出。

首先我们用两行奇怪的代码开始这篇文章。

[[NSArray alloc] isKindOfClass:[NSMutableArray class]];
[[NSMutableArray alloc] isKindOfClass:[NSArray class]];

事实证明上面两行的返回值都是真。没事请放松,但是准备一个篮子防止你的脑子发生泄露,这比疯狂还要疯狂。

先看看Foundation

跟其他许多语言相比,Objective-C真的是简单时髦。它的句法实际上很简单,很多模式的实现都耍了一些花招(重新看看上面的两行代码)。但是通过深入的研究探索那些苹果实现的类,比较容易洞悉一些更加通常的模式和一些花招,这些模式和花招可能在一开始会让你觉得有些奇怪。

我们仅仅研究NSArray, NSNumber, NSString, NSDictionary, and NSObject(或者说Foundation),就将发现无数令人感兴趣的,具有指导意义的,有乐趣的东西。为了这个目的,我们将聚焦NSArray中的那些很”傻逼”的东西。本篇文章我们将探索下面这些东西:

  • 字面量的句法
  • 下标
  • 类簇
  • 免费桥
  • 索引的成员变量

字面量

字面量是理解NSArray相关东西最简单的魔法,对于Objective-C来说是相对新的东西,在clang3.5中被加入,这种句法相当简单:

NSArray *a = @[ foo, bar, baz ];

其实就是逐字翻译成下面的代码,以遵从数组的构建:

id objects[] = { foo, bar, baz };
NSArray *a = [[NSArray alloc] initWithObjects:objects count:3];

不幸的是没有钩子(hooks)让我们去利用这种句法。它仅仅对于NSArray和NSDictionary等可用,所以在这方面没有太多能探究的。

下标

Clang3.5同样提供了方括号下标的支持

IndexedContainer *ic = ...;
ic[3] = @"foo";
id obj0 = ic[5];

KeyedContainer *kc = ...;
kc[@"qux"] = @"bar";
id obj1 = kc[@"bazzle"];

这其实很简单,仅仅是依据下标进行了一个简单的转换,这个下标可以是整数类型(indexed)也可以是一个对象(keyed)。

IndexedContainer *ic = ...;
[ic setObject:@"foo" atIndexedSubscript:3];
id obj0 = [ic objectAtIndexedSubscript:5];

KeyedContainer *kc = ...;
[kc setObject:@"bar" forKeyedSubscript:@"qux"];
id obj1 = [kc objectForKeyedSubscript:5];

这种代码虽然和以前相同,但是却更容易阅读。感谢NSArray和NSDictionary满足我们的期待实现了这些方法,如此事物突然变得很美好。

把内存当成一个连续的数组

我们构建一个简单的数组。(注意下面所有的代码都是在64位系统上编译运行,不过用iOS或OS X都可以)。

NSArray *array = @[ @"one", @"two", @"three" ];

我们得到的对象实际上只是一个指向一块内存的指针(用星号来标记),通常我们认为一个指针性的Objective-C对象就是一个实现的细节,但是猜测其指向的那块内存的组成很有意思。我们先从这块内存的大小入手。

#import <objc/runtime.h>

NSLog(@"%lu", class_getInstanceSize([array class]);

> 16

很好,这块内存有16个字节。像其他任何OC对象一样,最开始的8个字节是isa指针,所以只有8个字节值得我们探索。在这8个字节中,我们希望能够找到一些和数组结构相关的信息(例如数组长度或是一些对象的指针或是其他一些类似的东西)。

我们看看isa指针后面的8个字节是否是一个指针或是一些内置的数据结构例如盛装数组内容的缓冲。(如果你不确定为何要进行bridge转换,你或许需要看看我写的另一篇文章Objective-C ARC

char *bytes = (char *)(__bridge void *)array;

NSLog(@"%x", *(uint64_t *)(bytes + 8));

> 3

情形很乐观,我们使用的数组的长度正好是3,但这可能是个巧合,我们来试试其他数组的长度。

char *bytesArrayLen4 = (char *)(__bridge void *)@[ @1, @2, @3, @4 ];
NSLog(@"%x", *(uint64_t *)(bytesArrayLen4 + 8));

char *bytesArrayLen7 = (char *)(__bridge void *)@[ @1, @2, @3, @4, @5, @6, @7 ];
NSLog(@"%x", *(uint64_t *)(bytesArrayLen7 + 8));

> 4
> 7

完美,我们发现了数组的长度!但是可能长度不是存在全部的8个字节中而是仅仅用了开始的4个字节。所以我们来打印一下第12到15字节,一个4字节的整型。

NSLog(@"%x", *(uint32_t *)(bytes + 12));

> 0

很不幸,从打印结果我们看不出什么,也许这写比特没被使用,也许长度真的储存在64位的内存里。

寻找余下的对象

无论如何,我们发现了一个严重的问题。在那16字节的内存里储存了isa指针、数组的长度,仅此而已。那么数组里的对象到底在哪里?

也许有一个全局的map储存着数组地址和数组内容的映射。然而如果是那样的话我将停止探索这极度愚蠢的设计。幸运的是不是那样。让我们回忆一下,我们使用class_getInstanceSize得到数组实例的大小,但如果实际大小更大呢,我们看看这个数组所占的空间到底有多大而不是猜测。

#import <malloc/malloc.h>

NSLog(@"%lu", malloc_size(bytes));

> 48

什么鬼?这个类的实例应该只有16字节,这额外的32字节是从哪里来的,我们先跳过这个问题,看看这32字节里有什么。

NSLog(@"%@", *(uint64_t *)(bytes + 16));
NSLog(@"%@", *(uint64_t *)(bytes + 24));
NSLog(@"%@", *(uint64_t *)(bytes + 32));
NSLog(@"%@", *(uint64_t *)(bytes + 40));

> one
> two
> three
> (null)

译者注:原文的代码有些问题,是强转为uint32_t然后打印,但是运行的时候会出错,在64位系统中oc对象的地址也应该是64位的,这里明显是有问题的,我开始还以为原作者使用的环境是32位,后来发现他在文章开头就注明使用的环境是64位,所以这里应该是弄错了。

这是和NSArray内存布局等价的结构体。

struct MyArray {
  Class isa;
  uint64_t length;
  id object0;
  id object1;
  id object2;
  ...
};

实例的内存分配

既然我们已经明白了内存布局,下面我们需要搞明白为什么一开始得到的内存是16字节。不论是谁做的分配工作很明显不可能只使用了class_getInstanceSize。有一块内存在后来被加在了最后。我们需要去翻翻

id class_createInstance(Class cls, size_t extraBytes)

这个方法允许你定义一个数字表示在对象的最后开辟额外空间的大小。设置一个符号断点在这个函数上,从LLDB中可以看到extraBytes这个参数总是n*sizeof(id),至此这其中神秘的面纱就被揭开了。同时也有一个方法用于获取这个额外内存的地址,下面看一下具体用法。

//Compiled with MRR (-fno-objc-arc)
// object_getIndexedIvars is not available in ARC.

NSArray *array = @[ @"one", @"two", @"three" ];

void *indexedIvars = object_getIndexedIvars(array);

NSLog(@"%@", *(id *)(indexedIvars + 0));
NSLog(@"%@", *(id *)(indexedIvars + 8));
NSLog(@"%@", *(id *)(indexedIvars + 16));

> one
> two
> three

突然之间一切都明朗起来,我们弄清楚了NSArray的内存布局。似乎没有什么令人疑惑的东西遗留下来了,但是仔细想想class_getInstanceSize是什么时候被调用的呢。

我们直接使用alloc-init方法而不是用字面量进行实例化操作,这样我们就能弄清楚发生了什么。

id objects[] = { @"one", @"two", @"three" };
NSArray *a = [[NSArray alloc] initWithObjects:objects count:3];

在调用alloc时使用的是class_getInstanceSize方法返回的值,但是如何知道到底要分配多大的内存?关于这个数组有多大的信息直到init方法调用时才出现。

有一种可能是init方法释放了一开始分配的内存然后又重新分配了一段正确的内存(可以看看An Aside on Init’s Consumption of Self去弄明白为什么这在语义上是可能的),如果是这样做的那真是太浪费了。

我们先看看alloc方法返回的对象。我们要再一次使用MRR因为ARC不允许你单独使用alloc方法。

//Compiled with MRR (-fno-objc-arc)
NSArray *array = [NSArray alloc];
NSLog(@"%lu", malloc_size(array));

id objects[] = { @"one", @"two", @"three" };
array = [array initWithObjects:objects count:3];

NSLog(@"%lu", malloc_size(array));

> 16
> 48

毫无疑问init方法返回了一块新内存。多么浪费。。。:(也许alloc返回的是一些有用的东西这样就不会产生浪费了。

译者注:作者在这里强调浪费,应该是为了引出下一节的内容,先抑后扬的手法吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值