OC中类簇的实现和应用

4,095 阅读10分钟

什么是类簇

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

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

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

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

类簇的实现

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

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

类簇-简单工厂模式.png

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

类簇-工厂模式.png

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

类簇-抽象工厂模式.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];
}

总结

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