Objective-C中的类簇

什么是类簇

类簇是Foundation框架广泛使用的设计模式。类簇在公共抽象父类下对多个私有的具体子类进行分组。以这种方式对类进行分组简化了面向对象框架的公共可见体系结构,而不会降低其功能丰富度。类簇是基于抽象工厂设计模式的。

OC中有哪些类簇呢?NSData、NSArray、NSDictionary、NSString、NSNumber等都是类簇。日常开发debug过程中我们可能会发现_NSCFString、__NSArrayI这样的类,其实这就是其类簇下面的私有子类,具体类簇下面有哪些子类,大家可以参考Github上的这篇

为什么苹果要这样设计呢?以NSArray为例,为了保持数组存取的高效,针对不同情况(可变、不可变、单元素等情况)必然要有相应的子类来优化实现。如果全部都用可见子类来实现的话,那么对于程序员来说,就要熟知大量的子类及其API,并且在调用的时候也要分情况去调用,这样使用起来太复杂了。而且如果子类实现改变的话,有可能导致接口也改变,框架API变化也就更加频繁,不利于使用。

为了解决这个问题,NSArray和NSMutableArray作为公开抽象父类,抽象了array功能的接口,但是具体的实现则是通过私有的具体子类来实现。再结合抽象工厂设计模式,程序员就可以通过抽象父类引用而指向私有具体子类,由子类根据自身情况实现父类抽象的方法。这样接口十分简洁,框架底层子类变化时也不会影响到接口的变化,增强了接口稳定性。

类簇的实现

类簇是基于抽象工厂设计模式的,所以咱们就先了解一下什么是抽象工厂设计模式。工厂模式属于创建型模式,具体可以分为简单工厂模式、工厂模式和抽象工厂模式。

简单工厂模式,定义一个工厂类,根据传入参数的不同返回不同的实例,被创建的实例具有共同的父类或者接口。

image.png

工厂模式,定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。工厂方法模式是简单工厂的仅一步深化, 在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的对象,而是针对不同的对象提供不同的工厂。也就是说每个对象都有一个与之对应的工厂。

image.png

抽象工厂模式,提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。( 在抽象工厂模式中,每一个具体工厂都提供了多个工厂方法用于产生多种不同类型的对象)。抽象工厂模式是工厂模式的进一步深化,在这个模式中的工厂类不单单可以创建一个对象,而是可以创建一组对象。这是和工厂方法最大的不同点。

image.png

上述的例子都是很简单的例子,只能说明工厂模式,但是却无法体现使用的实际场景和价值。使用工厂模式(简单工厂模式,抽象工厂模式)有什么用呢?我觉得主要由两个作用:

  1. 延迟及隐藏子类实例化过程。
  2. 解耦,把对象的创建和使用的过程分开。
  3. 代码复用,简化实例化代码。
  4. 容易扩展或修改。

工厂模式适用的一些场景(不仅限于以下场景):

  1. 对象的创建过程/实例化准备工作很复杂,需要初始化很多参数、查询数据库等。
  2. 类本身有很多子类,这些类的创建过程在业务中容易发生改变,或者对类的调用容易发生改变。

可以看到,抽象工厂方法隐藏了具体工厂类创建具体产品子类实现的过程,只暴露了抽象工厂的创建接口。结合到使用类簇的原因,我们就会发现这完美解决了我们的需求——只暴露抽象类及创建接口,隐藏一系列子类的实例化过程及具体实现,简化API。那么类簇是怎么实现的呢?下面我们以NSArray的类簇实现作为例子讲解。

首先我们将NSArray的alloc和init的方法分开,会返现如下的结果:

 id obj1 = [NSArray alloc];        //__NSPlaceholderArray
id obj2 = [NSMutableArray alloc]; //__NSPlaceholderArray
id obj3 = [obj1 init];            //__NSArray0
id obj4 = [obj2 init];            //__NSArrayM

alloc之后NSArray和NSMutableArray都生成了__NSPlaceholderArray对象,然后这个对象在init方法中分别生成了__NSArray0和_NSArrayM具体子类。在这个过程中__NSPlaceholderArray即可以看做抽象工厂,而init则是抽象工厂的子类实例化过程。

如果要实现这个过程,那么__NSPlaceholderArray必定用某种方式存储了它是由谁alloc出来的,然后在init过程中根据这个记录信息来实现某个相应的具体子类实例化。但是通过查看__NSPlaceholderArray的内存布局发现除了存储isa地址,并没有存储其他信息。因此推断Foundation使用静态实例地址的方式来实现记录alloc来源。伪代码如下:

static __NSPlaceholderArray *GetPlaceholderForNSArray() {
    static __NSPlaceholderArray *instanceForNSArray;
    if (!instanceForNSArray) {
        instanceForNSArray = [__NPlaceholderArray alloc];
    }
    return instanceForNSArray;
}

static __NSPlaceholderArray *GetPlaceholderForNSMutableArray() {
    static __NSPlaceholderArray *instanceForNSMutableArray;
    if (! instanceForNSMutableArray) {
        instanceForNSMutableArray = [__NPlaceholderArray init];
    }
    return instanceForNSMutableArray;
}

//NSArray实现  
+(id)alloc {
    if(self == [NSArray class]) {
        return GetPlaceholderForNSArray;
    } else {
        return [super alloc]; 
    }
}

//NSMutableArray实现  
+(id)alloc {
    if(self == [NSMutableArray class]) {
        return GetPlaceholderForNSMutableArray;
    } else {
        return [super alloc]; 
    }
}

//__NSPlaceholderArray 实现  
-(id)init {
    if (self == GetPlaceholderForNSArray) {
        self = [[__NSArray0 alloc] init];
    } else if (self == GetPlaceholderForNSMutableArray) {
        self = [[__NSArrayM alloc] init];
    } else {
        self = [super init];
    }
    return self;
}

上述是推测的Foundation实现过程,我们通过下面代码验证一下是否为静态地址:

id obj1 = [NSArray alloc];
id obj2 = [NSArray alloc];
id obj3 = [NSMutableArray alloc];
id obj4 = [NSMutableArray alloc];
// 1和2地址相同,3和4地址相同,无论多少次都相同

NSArray和NSMutableArray类簇下面有很多子类,包括:__NSArray0、__NSArrayI、__NSArrayI_Transfer、__NSSingleObjectArrayI、__NSArrayReversed、__NSFrozenArrayM、NSKeyValueArray、_NSCallStackArray、 __NSOrderedSetArrayProxy、NSXMLChildren、__NSArrayM、__NSCFArray。我们可以推测,根据不同的实例化方法,会通过__NSPlaceholderArray的init有多种子类实例化过程。通过类簇的实现过程,是不是发现了抽象工厂实际的使用场景以及其作用意义,这种实现方式对于API使用来说是非常友好的。

如何子类化类簇

通过上述的类簇实现过程分析,我们发现了一个问题:类簇通过抽象工厂模式实现,那么如果我们要写一个子类继承自NSArray,为了实现该子类的实例化,按照类簇实现思路,我们必须增加该子类在工厂中实例化过程。但是我们仔细思考一下,这样是不可能做到的。因此应该尽量避免使用类簇来创建新的子类,如果必须这样做则必须足够的小心。下面讲一讲我们如何子类化类簇。

根据官方文档《Concepts in Objective-C Programming》,如果要子类化类簇,要做到:

  • 以公共抽象类为父类,比如NSNumber、NSArray等,而非其子类
  • 声明必要的变量,并提供自定义存储
  • 重写(覆盖)父类所有初始化方法
  • 重写父类中原始方法(primitive methods

首先,类簇的父类都是抽象父类(Abstract Classes),类簇只有抽象父类是公开可见的,因此我们也只能以公共抽象类作为父类。子类继承了父类的接口,但是不包括实例变量(抽象父类也不会声明实例变量)。所以子类必须声明自己需要的实例变量,并且定义其存储。其次,由于抽象父类并没有直接实现实例化过程,因此子类必须自己重写父类所有的初始化方法。最后,原始方法(primitive methods)是构成类的基本接口,其他方法可以通过原始方法派生而来(也叫做派生方法 derived methods)。以NSArray为例,其原始方法包括:count和objectAtIndex这两个,因此其子类也必须重写这两个原始方法。一般而言在Foudation中,在注释中包含primitive或者在NSArray中声明的是原始方法,但是在分类中,比如NSArray(NSExtendedArray)声明的是派生方法。

在子类化类簇过程中,父类的alloc如果没有相对应的子类调用的是[super alloc],即NSObject的alloc,父类的init没有具体实现。因此对于子类,可以不重写alloc,但是对于有自己声明变量的必须要重写init...。除了重写init...之外,也可以根据需要提供+className类方法和实现。

举个栗子:

@interface MyPairArray : NSArray {
    id _objs[2];
}

- (id)initWithFirst: (id)first second: (id)second;

@end

@implementation MyPairArray

- (id)initWithFirst: (id)first second: (id)second {
    if((self = [super init])) {
        _objs[0] = first;
        _objs[1] = second;
    }
    return self;
}
    
- (NSUInteger)count {
    return 2;
}

- (id)objectAtIndex: (NSUInteger)index {
    if(index >= 2)
        [NSException raise: NSRangeException format: @"Index (%ld) out of bounds", (long)index];
    return _objs[index];
}

@end

其实除了上述的子类化类簇的方法外,还有其他两种方式也可以达到同样的目的。一种是声明一个NSArray的变量_realArray,然后在initWithArray: (NSArray *)array方法中复制数组参数_realArray = [array copy]。这样原始方法也可以通过调用_realArray的原始方法实现。还有一种是通过分类为已有类簇父类添加方法。一般而言更推荐后两种方法,因为Foundation已经为我们做了很好的优化,有时候我们自己实例化出来的类簇子类,并没有很好的性能。

借鉴类簇的实现方式

类簇的实现过程将alloc和init分离,通过工厂类来实现实例化过程。这个过程也值得我们借鉴,在日常开发中,比较常见的有一些适配问题,比如语言或者界面适配;一些业务逻辑问题:比如车商的查询报告,分为维保、出险、违章等查询,不同查询业务逻辑有所不同。

对于创建复杂,并且有很多子类的情况我们可以通过抽象工厂去实例化具体子类,在alloc时根据情况标记工厂类,在工厂类init时根据传入的参数或者工厂标记选择具体的子类实例化。对于没那么复杂的情况,我们也可以在alloc时就根据情况选择相应的子类alloc,这就保证了实例化时调用的是相应子类实例化方法。

下面是一个屏幕适配的例子:

+ (id)alloc {
if ([self class] == [SFSSearchTVC class]) {
    if ([UIDevice currentDevice] systemMajorVersion] < 7) {
        return [SFSSearchTVC6 alloc];
    } else if ([UIDevice currentDevice] systemMajorVersion] == 7) {
        return [SFSSearchTVC7 alloc];
    }
}
    return [super alloc];
}

总结

这篇文章我们认识了类簇,知道了工厂模式以及抽象工厂设计模式在类簇中的应用,由此延伸到我们实际编码过程中子类化类簇需要注意的问题,以及如何借鉴类簇的实现以及抽象工厂模式思路。这些实现并不是死板的套用,而是应该根据我们的实际情况灵活的进行实现方式的变通。在今后的编码中,可以更多的尝试用抽象工厂模式和工厂模式去实现代码复用和解耦,编写维护性良好的程序。

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

推荐阅读更多精彩内容