类簇,从NSArray说起

在iOS开发中,广泛运用了类蔟(Class clusters)的设计模式。如NSNumber、NSString、NSArray等。类簇其实是对现实的一种抽象和封装,基于抽象工厂模式(Abstract Factory Pattern)。最近在读书过程中联想到一些东西,于是尝试更加深入地去了解它。

问题

所谓抽象工厂模式就是将各种同一主题的工厂类封装起来,提供一个通用的抽象工厂类而不用知道具体的工厂类。对于类蔟的论述已经多如牛毛了,在此更加推荐阅读苹果的官方文档。本篇文章将以NSArray为例,着重讲下从alloc到init过程中发生的事。在重温《Effective Objective-C 2.0》的过程中我注意到这么一段话:

In the case of NSArray, when an instance is allocated, it’s an instance of another class that’s allocated (during a call to alloc), known as a placeholder array. This placeholder array is then converted to an instance of another class, which is a concrete subclass of NSArray. This is a pretty little dance but beyond the scope of this book to explan fully.

可以用一个具体的例子来说明,比如:

NSArray *placeholder = [NSArray alloc];
NSArray *arr1 = [placeholder init];
NSArray *arr2 = [placeholder initWithObjects:@0, nil];
NSArray *arr3 = [placeholder initWithObjects:@0, @1, nil];
NSArray *arr4 = [placeholder initWithObjects:@0, @1, @2, nil];

NSLog(@"placeholder: %s", object_getClassName(placeholder));    // placeholder: __NSPlaceholderArray
NSLog(@"arr1: %s", object_getClassName(arr1));                  // arr1: __NSArray0
NSLog(@"arr2: %s", object_getClassName(arr2));                  // arr2: __NSSingleObjectArrayI
NSLog(@"arr3: %s", object_getClassName(arr3));                  // arr3: __NSArrayI
NSLog(@"arr4: %s", object_getClassName(arr4));                  // arr4: __NSArrayI

可以看到,alloc后所得到的类为__NSPlaceholderArray。而当init为一个空数组后,变成了__NSArray0。如果有且仅有一个元素,那么为__NSSingleObjectArrayI。如果数组大于一个元素,那么为__NSArrayI。这儿暂且不去讨论为什么arr1-4有所区别——先来关心一下为什么alloc和init前后转化为了不同的类。

从名字上很容易知道__NSPlaceholderArray作用为占位,我们可以尝试打印几个地址:

NSArray *placeholder = [NSArray alloc];
NSArray *placeholder2 = [NSArray alloc];
NSArray *arr1 = [placeholder init];
NSArray *arr2 = [placeholder initWithObjects:@0, nil];

NSLog(@"placeholder: %p", placeholder);     // placeholder: 0x618000013b10
NSLog(@"placeholder2: %p", placeholder2);   // placeholder2: 0x618000013b10
NSLog(@"arr1: %p", arr1);                   // arr1: 0x618000013b30
NSLog(@"arr2: %p", arr2);                   // arr2: 0x608000014050

可以看到[NSArray alloc]产生的实例为一个单例,而在init或者其他初始化方法后,地址发生了变化,也就是说,placeholder目前看来只是一个占位用的单例,在init后即被新的实例给替换掉了。那么,这个placeholder真的只用做占位吗?

__NSPlaceholderArray

我们可以参考另一个开源实现GNUstep一瞥究竟。根据GNUstep的代码,可知NSObject的alloc是直接返回的[self allocWithZone: NSDefaultMallocZone()],也就是说调用了对应类实现的此方法。我们来看看GNUstep中NSArray的allocWithZone:是如何实现的:

+ (id) allocWithZone: (NSZone*)z
{
  if (self == NSArrayClass)
  {
    /*
    * For a constant array, we return a placeholder object that can
    * be converted to a real object when its initialisation method
    * is called.
    */
    if (z == NSDefaultMallocZone() || z == 0)
    {
      /*
      * As a special case, we can return a placeholder for an array
      * in the default malloc zone extremely efficiently.
      */
      return defaultPlaceholderArray;
    }
    else
    {
      // 此处省略
    }
  }
  else
  {
    return NSAllocateObject(self, 0, z);
  }
}

可以看到NSArray此时会返回defaultPlaceholderArray。在GNUstep的实现中,defaultPlaceholderArray实例所对应的类为GSPlaceholderArray。所以alloc完成后的init消息是发送给GSPlaceholderArray实例的。而init恰恰调用的是initWithObjects:count:——这个方法其实就是NSArray的指定初始化方法。我们继续看看GNUstep实现:

// GSPlaceholderArray
- (id) initWithObjects: (const id[])objects count: (NSUInteger)count
{
  self = (id)NSAllocateObject(GSInlineArrayClass, sizeof(id)*count, [self zone]);
  return [self initWithObjects: objects count: count];
}

// GSInlineArray
- (id) initWithObjects: (const id[])objects count: (NSUInteger)count
{
  _contents_array = (id*)(((void*)self) + class_getInstanceSize([self class]));

  if (count > 0)
  {
    NSUInteger  i;

    for (i = 0; i < count; i++)
    {
      if ((_contents_array[i] = RETAIN(objects[i])) == nil)
      {
        _count = i;
        DESTROY(self);
        [NSException raise: NSInvalidArgumentException format: @"Tried to init array with nil object"];
      }
    }
    _count = count;
  }
  return self;
}

可以看到在GSPlaceholderArray的initWithObjects:count:方法中,通过NSAllocateObject给GSInlineArray实例分配空间,包括所包含元素的空间。并且在GSInlineArray的initWithObjects:count:方法中,对分配的元素的空间进行初始化。自此就返回了一个类型为GSInlineArray的实例。

CoreFoundation中NSArray的相关实现会比GNUstep中的实现复杂些,但通过汇编代码来看可以知道基本逻辑是类似的,在此不再赘述。有几点可以提下:1、当元素为空时,返回的是__NSArray0的单例;2、当元素仅有一个时,返回的是__NSSingleObjectArrayI的实例;3、当元素大于一个的时候,返回的是__NSArrayI的实例。根据网上的资料,大多未提及__NSSingleObjectArrayI,可能是后面新增的,理由大概还是为了效率,在此不深究。

同样的,对于NSMutableArray、NSNumber、NSString等也是有相同的NSPlaceholderNumber机制的。

可变类的Placeholder

提到NSMutableArray,那么问题来了——NSMutableArray是否也有NSMutablePlaceholderArray呢?

答案是:并没有。一开始我也是先入为主地认为一定对应着一个可变类型的placeholderArray。但在好奇心驱使下打印了各个实例的父类后,我吃惊的发现其实并没有——它依然是__NSPlaceholderArray。

NSArray *placeholder = [NSArray alloc];
NSArray *arr1 = [placeholder init];
NSArray *arr2 = [placeholder initWithObjects:@0, nil];
NSArray *arr3 = [placeholder initWithObjects:@0, @1, nil];

NSLog(@"superclass of placeholder: %s", class_getName(placeholder.superclass)); // superclass of placeholder: NSMutableArray
NSLog(@"superclass of arr1: %s", class_getName(arr1.superclass));               // superclass of arr1: NSArray
NSLog(@"superclass of arr2: %s", class_getName(arr2.superclass));               // superclass of arr2: NSArray
NSLog(@"superclass of arr3: %s", class_getName(arr3.superclass));               // superclass of arr3: NSArray

NSMutableArray *mPlaceholder = [NSMutableArray alloc];
NSMutableArray *mArr1 = [mPlaceholder init];
NSMutableArray *mArr2 = [mPlaceholder initWithObjects:@0, nil];
NSMutableArray *mArr3 = [mPlaceholder initWithObjects:@0, @1, nil];

NSLog(@"mPlaceholder: %s", object_getClassName(mPlaceholder));    // mPlaceholder: __NSPlaceholderArray
NSLog(@"mArr1: %s", object_getClassName(mArr1));                  // mArr1: __NSArrayM
NSLog(@"mArr2: %s", object_getClassName(mArr2));                  // mArr2: __NSArrayM
NSLog(@"mArr3: %s", object_getClassName(mArr3));                  // mArr3: __NSArrayM

NSLog(@"superclass of mPlaceholder: %s", class_getName(mPlaceholder.superclass));   // superclass of mPlaceholder: NSMutableArray
NSLog(@"superclass of mArr1: %s", class_getName(mArr1.superclass));                 // superclass of mArr1: NSMutableArray
NSLog(@"superclass of mArr2: %s", class_getName(mArr2.superclass));                 // superclass of mArr2: NSMutableArray
NSLog(@"superclass of mArr3: %s", class_getName(mArr3.superclass));                 // superclass of mArr3: NSMutableArray

当时我的心里大概出现了这么个文件名:大吃一惊.jpg。但转念一想也是可以接受的,毕竟NSMutableArray是NSArray的子类,从这个角度来看,共用一个NSPlaceholderArray也是情有可原的。那么现在的问题是:它是个单例,又该怎么区分可变和不可变数组的呢?毕竟两个初始化方法selector是相同的。GNUstep似乎并不能找到答案,那么就再次祭出大杀器汇编吧。

CoreFoundation`-[__NSPlaceholderArray initWithObjects:count:]:
; 前略
    0x10edf9698 <+40>:  je     0x10edf96b3               ; <+67>
    0x10edf969a <+42>:  nopw   (%rax,%rax)
    0x10edf96a0 <+48>:  cmpq   $0x0, (%rdx,%r8,8)
    0x10edf96a5 <+53>:  je     0x10edf972c               ; <+188>
    0x10edf96ab <+59>:  incq   %r8
    0x10edf96ae <+62>:  cmpq   %r9, %r8
    0x10edf96b1 <+65>:  jb     0x10edf96a0               ; <+48>
->  0x10edf96b3 <+67>:  cmpq   %rdi, 0x3b514e(%rip)      ; __immutablePlaceholderArray
    0x10edf96ba <+74>:  je     0x10edf96d2               ; <+98>
->  0x10edf96bc <+76>:  cmpq   %rdi, 0x3b514d(%rip)      ; __mutablePlaceholderArray
    0x10edf96c3 <+83>:  jne    0x10edf97b7               ; <+327>
    0x10edf96c9 <+89>:  movq   0x3aa260(%rip), %rdi      ; (void *)0x000000010f1a5db0: __NSArrayM
    0x10edf96d0 <+96>:  jmp    0x10edf9717               ; <+167>
    0x10edf96d2 <+98>:  cmpq   $0x1, %r9
    0x10edf96d6 <+102>: je     0x10edf96f5               ; <+133>
    0x10edf96d8 <+104>: testq  %r9, %r9
    0x10edf96db <+107>: jne    0x10edf9710               ; <+160>
    0x10edf96dd <+109>: leaq   0x3b7c9c(%rip), %rax      ; __NSArray0__
    0x10edf96e4 <+116>: movq   (%rax), %rdi
    0x10edf96e7 <+119>: movq   0x3a862a(%rip), %rsi      ; "retain"
    0x10edf96ee <+126>: popq   %rbp
    0x10edf96ef <+127>: jmpq   *0x371b2b(%rip)           ; (void *)0x000000010e961ac0: objc_msgSend
    0x10edf96f5 <+133>: movq   0x3aa224(%rip), %rdi      ; (void *)0x000000010f1a5d60: __NSSingleObjectArrayI
    0x10edf96fc <+140>: movq   (%rdx), %rdx
    0x10edf96ff <+143>: movq   0x3a92c2(%rip), %rsi      ; "__new::"
    0x10edf9706 <+150>: xorl   %ecx, %ecx
    0x10edf9708 <+152>: callq  *0x371b12(%rip)           ; (void *)0x000000010e961ac0: objc_msgSend
    0x10edf970e <+158>: popq   %rbp
    0x10edf970f <+159>: retq   
    0x10edf9710 <+160>: movq   0x3aa211(%rip), %rdi      ; (void *)0x000000010f1a5d88: __NSArrayI
    0x10edf9717 <+167>: movq   0x3a92b2(%rip), %rsi      ; "__new:::"
    0x10edf971e <+174>: xorl   %r8d, %r8d
    0x10edf9721 <+177>: movq   %r9, %rcx
    0x10edf9724 <+180>: callq  *0x371af6(%rip)           ; (void *)0x000000010e961ac0: objc_msgSend
    0x10edf972a <+186>: popq   %rbp
; 后也略

让我们重点关注两个箭头所指向的cmpq指令吧。可以很清楚地知道,其实就是判断self == __immutablePlaceholderArray和self == __mutablePlaceholderArray。也就是说,CoreFoundation在某个时机初始化了两个NSPlaceholderArray,分别存起来。在调用__NSPlaceholderArray的initWithObjects:count:方法时,直接通过判断存起来的这两个单例来判断是否是不可变还是可变数组。真相就是这么赤裸裸的简单粗暴。

我们再来看看+[NSArray allocWithZone:]

CoreFoundation`+[NSArray allocWithZone:]:
    0x10b5004a0 <+0>:   pushq  %rbp
    0x10b5004a1 <+1>:   movq   %rsp, %rbp
    0x10b5004a4 <+4>:   pushq  %r15
    0x10b5004a6 <+6>:   pushq  %r14
    0x10b5004a8 <+8>:   pushq  %rbx
    0x10b5004a9 <+9>:   subq   $0x18, %rsp
    0x10b5004ad <+13>:  movq   %rdx, %r14
    0x10b5004b0 <+16>:  movq   %rdi, %rbx
    0x10b5004b3 <+19>:  movq   0x3aa47e(%rip), %rdi      ; (void *)0x000000010b8acdd8: NSArray
    0x10b5004ba <+26>:  movq   0x3a9647(%rip), %r15      ; "self"
    0x10b5004c1 <+33>:  movq   %r15, %rsi
    0x10b5004c4 <+36>:  callq  *0x371d56(%rip)           ; (void *)0x000000010b068ac0: objc_msgSend
->  0x10b5004ca <+42>:  cmpq   %rbx, %rax
    0x10b5004cd <+45>:  je     0x10b500511               ; <+113>
    0x10b5004cf <+47>:  movq   0x3aa392(%rip), %rdi      ; (void *)0x000000010b8ace50: NSMutableArray
    0x10b5004d6 <+54>:  movq   %r15, %rsi
    0x10b5004d9 <+57>:  callq  *0x371d41(%rip)           ; (void *)0x000000010b068ac0: objc_msgSend
->  0x10b5004df <+63>:  cmpq   %rbx, %rax
    0x10b5004e2 <+66>:  je     0x10b500521               ; <+129>
    0x10b5004e4 <+68>:  movq   %rbx, -0x28(%rbp)
    0x10b5004e8 <+72>:  movq   0x3aa7e9(%rip), %rax      ; (void *)0x000000010b8acea0: NSArray
    0x10b5004ef <+79>:  movq   %rax, -0x20(%rbp)
    0x10b5004f3 <+83>:  movq   0x3a88b6(%rip), %rsi      ; "allocWithZone:"
    0x10b5004fa <+90>:  leaq   -0x28(%rbp), %rdi
    0x10b5004fe <+94>:  movq   %r14, %rdx
    0x10b500501 <+97>:  callq  0x10b6acb50               ; symbol stub for: objc_msgSendSuper2
    0x10b500506 <+102>: addq   $0x18, %rsp
    0x10b50050a <+106>: popq   %rbx
    0x10b50050b <+107>: popq   %r14
    0x10b50050d <+109>: popq   %r15
    0x10b50050f <+111>: popq   %rbp
    0x10b500510 <+112>: retq   
    0x10b500511 <+113>: movq   0x3aa428(%rip), %rdi      ; (void *)0x000000010b8ace78: __NSPlaceholderArray
    0x10b500518 <+120>: movq   0x3a94d9(%rip), %rsi      ; "immutablePlaceholder"
    0x10b50051f <+127>: jmp    0x10b50052f               ; <+143>
    0x10b500521 <+129>: movq   0x3aa418(%rip), %rdi      ; (void *)0x000000010b8ace78: __NSPlaceholderArray
    0x10b500528 <+136>: movq   0x3a94f1(%rip), %rsi      ; "mutablePlaceholder"
    0x10b50052f <+143>: addq   $0x18, %rsp
    0x10b500533 <+147>: popq   %rbx
    0x10b500534 <+148>: popq   %r14
    0x10b500536 <+150>: popq   %r15
    0x10b500538 <+152>: popq   %rbp
    0x10b500539 <+153>: jmpq   *0x371ce1(%rip)           ; (void *)0x000000010b068ac0: objc_msgSend
    0x10b50053f <+159>: nop    

依旧看两个箭头,可以看到当self为NSArray和NSMutableArray时候分别返回immutablePlaceholder和mutablePlaceholder,它们都是__NSPlaceholderArray类型的。这样就验证了上面的想法。

Primitive methods

上面多处提到了initWithObjects:count:。为什么它这么重要?我们可以看看NSArray的interface是如何定义的:

<figure class="highlight" style="background: rgb(255, 255, 255);">

@interface NSArray<__covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration>

@property (readonly) NSUInteger count;
- (ObjectType)objectAtIndex:(NSUInteger)index;
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithObjects:(const ObjectType _Nonnull [_Nullable])objects count:(NSUInteger)cnt NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;

@end

不同于普通的继承,在创建某个类蔟的具体的子类时,通常不需要实现所有的功能。也不同于普通的抽象类,在公共的抽象基类中,一般提供了辅助的方法的实现,子类只需要提供几个核心方法的实现即可。

在CoreFoundation的类蔟的抽象工厂基类(如NSArray、NSString、NSNumber等)中,Primitive methods指的就是这些核心的方法,也就是那些在创建子类时必须要重写的方法,通常在类的interface中声明,在文档中一般也会说明。其他可选实现的方法在Category中声明。同时还需要注意其整个继承树的祖先的Primitive methods也都需要实现。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,294评论 8 265
  • 在iOS开发中,我们在非常非常多的地方用到了数组。而关于数组,有很多需要注意和优化的细节,需要我们潜入到下面,去了...
    伯陽阅读 6,064评论 3 30
  • 整理出的一些简单实用的OC笔试题,如有错误之处希望大家及时提出,以便修改,不误人子弟.1、不会立刻使引用计数器改变...
    李xiao屁的忧伤阅读 2,878评论 1 20
  • 本文转自:咖门 公众号「一位“老炮儿”的研发心得:做一杯好茶,就像导演一部大片」 近两年来,新茶饮逐渐流行并颇有百...
    吴建伸阅读 688评论 0 4