这篇文章由很多平时的笔记积攒而成, 看起来会有些杂乱, 会有很多需要改进的地方, 希望发现问题的朋友不吝赐教.
类簇
类簇是 Foundation 框架广泛使用的设计模式. 类簇在公共抽象超类下对多个私有的具体子类进行分组. 以这种方式对类进行分组简化了面向对象框架的公共可见体系结构, 而不会降低其功能丰富度. 类簇是基于抽象工厂设计模式的.
我将苹果官方文档中的有关类簇的部分翻译了过来,
抽象工厂
抽象工厂模式是指当有多个抽象角色时, 使用的一种工厂模式. 抽象工厂模式可以向客户端提供一个接口, 使客户端在不必指定产品的具体的情况下, 创建多个产品族中的产品对象. 很多人会混淆抽象工厂模式和工厂模式. 实际上, 两种的差别还是比较明显的, 如下表.
抽象工厂模式 | 工厂模式 |
---|---|
通过对象组合创建抽象产品 | 通过类继承创建抽象产品 |
创建多系列产品 | 创建一种产品 |
必须修改父类的接口才能支持新的产品 | 子类化创建者并重载工厂方法以创建新产品 |
有关抽象工厂的东西我们先讲到这里, 剩下的我们有时间再聊.
再往下就是针对具体情况的分析了, 这段比较冗长, 不喜欢的可以直接跳到最后一部分.
注: 在开始前, 点击它来查看子类的名称, 有些子类实在是过于稀少, 就不单独拿出来说明了.
NSArray
《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.
在使用了 NSArray 的 alloc 方法来获取实例时, 该方法首先会分类一个属于某类的实例, 此实例充当 "占位数组". 该数组稍后会转为另一个类的实例, 而那个类则是 NSArray 的实体子类.
因为在之前的文章中陈述过这一段, 代码略过, 直接上结论:
不管创建的事可变还是不可变的数组, 在 alloc 之后得到的类都是__NSPlaceholderArray. 而当我们 init 一个不可变的空数组之后, 得到的是__NSArray0; 如果有且只有一个元素, 那就是__NSSingleObjectArrayI; 有多个元素的, 叫做 __NSArrayI;init 出来一个可变数组的话, 都是 __NSArrayM.
这里__NSSingleObjectArrayI, 需要说明它的用意:
__NSSingleObjectArrayI
作为对比,__NSArrayI 必须要实现
- count
- objectAtIndex:
这两个个方法, 但是我们可以非常显而易见的看出来, 当数组只有一个数字的时候, 是完全不需要这两个方法的.
再深入一点的说明一下,__NSSingleObjectArrayI 是不需要去记录字符串长度的. 它会比__NSArrayI 少 8 个字节的长度. 苹果可能是为了优化性能考虑, 从而在 iOS8 之后推出这个新的子类.
另外需要说明的是, 实际上,__NSArrayM 本身只有 7 个方法, 分别是:
- count
- objectAtIndex:
- insertObject:atIndex:
- removeObjectAtIndex:
- addObject:
- removeLastObject
- replaceObjectAtIndex:withObject:
所有其它高等级的抽象建立在它们的基础之上. 例如 - removeAllObjects 方法简单地往回迭代, 一个个地调用 - removeObjectAtIndex:.
NSDictionary
NSDictionary 与 NSArray 类似, 不管创建的事可变还是不可变的字典, 在 alloc 之后得到的类都是__NSPlaceholderDictionary. 而当我们 init 一个不可变的空数组之后, 得到的是__NSDictionary0; 如果有且只有一个元素, 那就是__NSSingleEntryDictionaryI; 有多个元素的, 叫做 __NSDictionaryI;init 出来一个可变数组的话, 都是 __NSDictionaryM.
不过呢, 我这里还调试出了一种很有趣的子类,__NSFrozenDictionaryM.
过程是
- ...
- @property (nonatomic, copy) NSMutableDictionary *mutableDictionary;
- ...
- NSMutableDictionary *dictionary = [[NSMutableDictionary alloc]init];
- [dictionary setObject:@"aaa" forKey:@"name"];
- self.mutableDictionary = dictionary;
- ...
在违反属性规范的情况下, 获得了这个有趣的子类. 不过不用担心, 这个子类没什么特殊的作用, 它仍然会被视为不可变字典. 也就是说, 对它进行改变的操作, 会导致程序崩溃. 崩溃信息如下:
- 2018-08-25 09:23:45.214879+0800 setget[90992:17397034] -[__NSFrozenDictionaryM setObject:forKey:]: unrecognized selector sent to instance 0x6000000dde40
- 2018-08-25 09:23:45.238002+0800 setget[90992:17397034] *** Terminating App due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSFrozenDictionaryM setObject:forKey:]: unrecognized selector sent to instance 0x6000000dde40'
其实在在 NSArray 中也有个对应的__NSFrozenArrayM, 不过我没有找到触发条件. 不过应该与它类似.
NSSet
好吧, NSSet 的子类不过是 NSDictionary 换了个名字而已, 不做细讲了.
这里说明一下,__NSSingleObjectSetI 不需要打扰实际的哈希表, 因为只有一个对象需要担心. 类似的方法 containsObject: 不需要遍历任何东西或查找任何东西, 它可以简单地将参数与 set / array / dictionary 表示的单个对象进行比较.
NSString
重头戏来了!!!
// 以下的结论全部来自 64 位系统
当我们测试创建 NSString 对象的时候, 通过创建 NSString 不同的对象, 并利用 object_getClassName 方法打印对象.
- NSString *str1 = @"biboyanggggggg";
- //str1: __NSCFConstantString
- NSString *str2 = [NSString stringWithString:@"biboyanggggggg"];
- //str2: __NSCFConstantString
- NSString *str3 = @"biboyang";
- //str3: __NSCFConstantString
- NSString *str4 = [NSString stringWithFormat:@"biboyang"];
- //str4: NSTaggedPointerString
- NSString *str5 = [NSString stringWithFormat:@"sa"];
- //str5: NSTaggedPointerString
- NSString *str6 = [NSString stringWithFormat:@"123456789"];
- //str6: NSTaggedPointerString
- NSString *str7 = [NSString stringWithFormat:@"1234567890"];
- //str7: __NSCFString
我们可以发现, 这里出现了三个子类
- __NSCFConstantString
- __NSCFString
- NSTaggedPointerString
- __NSCFConstantString
它是一个字符串常量. 它的引用计数非常大, 是 4294967295, 它的意思是, 这个属性, 怎么都不会被释放. 相同的对象, 内存地址是相同的, 可以直接使用 == 方法 (但是, 这个对象的指针的地址依然不同, 还是两个不同的对象). 它在编译时就决定的, 不能在运行时创建.
更详细的可以查看念纪的这篇博客
__NSCFString
这个就是可变的 NSString 所属的子类了. 不必多说.
NSTaggedPointerString
要从从 iPhone5s 开始说起, iPhone5s 开始采用了 64 位处理器. 在 32 位时代, 一个指针大小是 32 位 (4 字节), 而在 64 位时代翻倍, 一个指针的大小变成了 64 位 (8 字节). 这样子, 在处理某些小一点, 短一点的 NSString,NSNumber,NSDate 对象的时候, 会显得过于浪费效率. 这个时候, 苹果推出了 Tagged Pointer 技术.
苹果将一个对象的指针拆分成了两部分, 一部分直接保存数据, 另一部分作为特殊标记 (tag), 表示这个是一个特别的指针. 这样呢, 就会将节省很多的时间, 因为它不在需要正常创建对象的申请和创建空间, 处理引用计数, 以及直接读取 (在 objc_msgSend 当中, Tagged Pointer 会被识别出来, 直接从指针中读取).
苹果之前说过, 使用 Tagged Pointer 技术之后, 在内存上读取的速度快了 3 倍, 创建时的速度比以前快乐 106 倍.
当然, 这么做其实也是会有问题的, 因为它并不是一个真正的对象, 当你想要想其他普通的对象一样获取指针的时候, 编译器直接就会报错 (因为它也是在编译时创建的, 而且压根没有 isa 指针).
编译器会告诉你正确的方法: 改为使用 object_getClass().
总结
类簇的优点在于:
1. 可以将抽象基类背后的复杂细节隐藏起来
2. 程序员不会需要记住各种创建对象的具体类实现, 简化了开发成本, 提高了开发效率
3. 便于进行封装和组件化
4. 减少了 if else 这样缺乏扩展性的代码
5. 增加新功能支持不影响其他代码
缺点也很明显:
已有的类簇非常不好扩展!!!
我们了解类簇的好处:
出现 bug 时, 可以通过崩溃报告中的类簇关键字, 快速定位 bug 位置.
在实现一些固定切并不需要经常修改的事物时, 可以高效的选择类簇去实现.
举个例子: 针对不同版本, 不同机型往往需要不同的设置, 这时可以选择使用类簇;
App 的设置页面这种并不需要经常修改的页面, 可以使用类簇去创建大量重复的布局代码.
资料来源
Friday Q&A 2015-07-31: Tagged Pointer Strings
来源: https://juejin.im/entry/5bd955fe6fb9a022433c38aa