Book ISBN Notes
编写高质量 iOS 与 OS X 代码的 52 个有效方法 9787111451297 -
Date Num. Xcode
2019-05 1-3 10.2.1
2019-06 4 10.2.1
2019-07 5-9 10.2.1

熟悉 Objective-C

了解 Objective-C 语言的起源

  • 消息结构(Message Structure)的语言,运行时执行的代码由运行环境决定;函数调用(Function Calling)的语言,运行时所执行的代码由编译器决定,对于多态,则按照虚函数表(Virtual Method Table)寻找。
  • 运行时组件(Runtime Component) 本质上是一种与开发者所编写代码相链接的「动态库(Dynamic Library),这样的好处是只需要更新运行时组件(无需重新编译)即可提升程序性能。
  • Obj-C 是 C 的超集。
  • Obj-C 中的对象所占内存总是分配在堆空间(Heap Stack);而指向对象的指针所占内存总是分配在栈帧(Stack Frame)中。
  • 堆中的内存需要程序员自己管理,栈中的内存会在其栈帧弹出(Pop)时自动清理。
  • 创建对象相比创建结构体(C 结构体)需要额外开销,例如分配和释放堆内存等。

1

// ⚠️ 由于 Obj-C 中的字符串(NSString)略有特殊,此处并未使用书中的 NSString 作为范例
// 对象本身被分配在堆上;obj1 & obj2 被分配在栈上
NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = obj1;

// (lldb) p obj1
// (NSObject *) $0 = 0x00000001005092b0
// (lldb) p obj2
// (NSObject *) $1 = 0x00000001005092b0
// (lldb) p &obj1
// (NSObject **) $2 = 0x00007ffeefbff508
// (lldb) p &obj2
// (NSObject **) $3 = 0x00007ffeefbff500

在类的头文件中尽量少引入其他头文件

  • 当需要知道有一个类的存在时,而不关心其内部细节时,可以使用向前声明(Forward Declaring)告知编译器,即可以在 .h(头文件)中 @class SomeClass;而在 .m(实现文件)中引入实际的 .h。
  • 当在两个头文件中互相引入对方,则会导致「循环引用(Chicken-Egg Situation)」,无法通过编译。
  • 将引入头文件的时机尽量延后,只有在确定有需要时才引入,否则会增加耦合度、拉长编译时间、产生相互依赖等问题。
  • 继承父类和遵循协议则不能使用向前声明,必须引入相应的头文件,因此协议最好声明在单独的头文件中。
  • 由于代理协议(Delegate Protocol)和遵守协议代理的类声明在一起时才有意义,最好在实现文件中声明类遵守了该代理协议,并将实现代码放在 Class-Continuation 分类(Class-Continuation Category)中;因此只需要在实现文件中引入包含代理协议的头文件即可,而不需要将其放在公共头文件(Public Header File)中。

多用字面量语法,少用与之等价的方法

  • 字面量(Literal)语法简化了 Obj-C 的部分 API:

    NSString *strValue = @"str";
    NSNumber *intValue = @1;
    NSNumber *doubleValue = @3.14;
    NSArray *arrValue = @[@"a", @"b", @"c"];
    NSString *firstValueForArr = arrValue[0];
    // ⚠️ 字面量创建的数组、字典都是不可变的
    NSMutableArray *mutableArrValue = [@[@"a", @"b", @"c"] mutableCopy];
    mutableArrValue[0] = @"maimieng.com";
    NSDictionary *dictValue = @{@"key" : @100};
    NSNumber *valueForDictByKey  = dictValue[@"key"];
    NSMutableDictionary *mutableDictValue = [@{@"key" : @100} mutableCopy];
    mutableDictValue[@"key"] = @200;
    
  • 字面量语法在 NSArrayNSDictionary 等类中插入 nil 对象时会直接崩溃,而直接使用 API 则会发生「截断」,对于这两个用法差异务必要注意:

    id nilObj = nil;
    // *** -[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[3]
    NSArray *arrWithNilObject1 = @[@0, nilObj, @2];
    // (0)
    NSArray *arrWithNilObject2 = [NSArray arrayWithObjects:@0, nil, @2, nil];
    // *** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[0]
    NSDictionary *dictWithNilObject1 = @{@"a" : @"A", @"b" : nilObj, @"c" : @"C"};
    // { a = A; }
    NSDictionary *dictWithNilObject2 = [NSDictionary dictionaryWithObjectsAndKeys:@"A", @"a", nilObj, @"b", @"C", @"c", nil];
    
  • 除了字符串,字面量语法仅适用于 Foundation 框架中,即我们自定义继承自上述支持字面量的类时,将不再支持使用字面量。

多用类型常量,少用 #define 预处理指令

  • #define 预处理指令没有类型信息,且会在引用到包含该指令的所有文件中进行替换,因此更加推荐使用类型常量。

    // 预处理指令(编译前进行直接替换)
    #define ANIMATION_DURATION 0.3
    
    // Some.m
    
    // 类型常量
    // const 决定了其为常量,不可被再次改变
    // static 决定了其作用域,即当前文件(因此如果将其定义在 *.h 中,其他引入该头文件的文件也可以访问到)
    static const NSTimeInterval kAnimationDuration = 0.3;
    
    // 如果不使用 static 修饰,编译器会创建一个外部符号(External Symbols)
    const NSTimeInterval SomeAnimationDuration = 0.3;
    // 此时如果在其他文件内声明同名常量,则会报错「duplicate symbol」
    // Another.m
    const NSTimeInterval SomeAnimationDuration = 0.5;
    
    // ---
    
    // main.m
    // 声明为 `static` 和 `const` 的变量,编译器不会为其再创建符号
    #import <Foundation/Foundation.h>
    
    int foo = 1;
    
    // static const + int
    static const int k1 = 1;
    // static const + int *
    // 对于指针,既要使其本身的内容不能改变,也不能改变其指向的内存地址,否则这个指针还是有可能改变的
    static int const * const k2 = &foo;
    // static const + NSString *
    static NSString * const k3 = @"";
    static NSString const * const k4 = @"";
    
    // const
    const int k5 = 3;
    // static
    static int k6 = 4;
    
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 为了防止编译器优化未用到的符号,我们这里简单输出以上定义的变量
        NSLog(@"%d", k1);
        NSLog(@"%d", *k2);
        NSLog(@"%@", k3);
        NSLog(@"%@", k4);
        NSLog(@"%d", k5);
        NSLog(@"%d", k6);
    }
    return 0;
    }
    
    // 输出所有符号
    // ➜  Debug nm -C Demo
    // 通过以下输出可以得出由 static 和 const 修饰的 k1,k2 编译器都没有为其产生符号
    // 但 NSString * 是个例外(Why? 猜测是编译器认为该常量后续还要被修改的可能,因此需要保留其符号)
    //                  U _NSLog
    //                  U ___CFConstantStringClassReference
    // 0000000100000000 T __mh_execute_header
    // 00000001000010a0 D _foo
    // 0000000100001028 s _k3
    // 0000000100001030 s _k4
    // 0000000100000fb4 S _k5
    // 00000001000010a4 d _k6
    // 0000000100000eb0 T _main
    //                  U _objc_autoreleasePoolPop
    //                  U _objc_autoreleasePoolPush
    //                  U dyld_stub_binder
    
    // 仅输出外部符号
    // ➜  Debug nm -gC Demo
    // 所有使用 static 修饰的变量均不在外部符号中,说明 static 将这些符号的作用域限制在当前文件中
    //                  U _NSLog
    //                  U ___CFConstantStringClassReference
    // 0000000100000000 T __mh_execute_header
    // 00000001000010a0 D _foo
    // 0000000100000fb4 S _k5
    // 0000000100000eb0 T _main
    //                  U _objc_autoreleasePoolPop
    //                  U _objc_autoreleasePoolPush
    //                  U dyld_stub_binder
    
  • 类型常量的命名规则:

    • 若常量局限于某个编译单元(Translation Unit,即实现文件 *.m),则需要以小写字母 k 开头;
    • 若常量在类之外也可见,则通常需要以类名开头。
  • 关于 nm 命令的基本使用,可以参考 Obj-C 中实例变量和类的访问控制一文。

    // Some.h
    // extern 告知编译器全局符号表中存在该符号,允许外界使用
    extern NSString * const SomeConstant;
    
    // Some.m
    // 外界需要访问则不能声明为 static;外界不可变更,因此声明为 const
    // 这里的 const 修饰的是 NSString *(即 const 从右向左原则),SomeConstant 保存的内存地址不能再改变(即不能指向另外一个 NSString 对象)
    // 编译器会在数据段(Data Section)为字符串分配存储空间,链接器会将目标文件相互链接,生成最终的可执行文件
    // 全局符号命名规则:范围(通常使用类名)+ 名称
    NSString * const SomeConstant = @"SomeConstant";
    
    // 在 Obj-C 中,除了 C 中原有的 extern 还存在以下相关的宏定义
    // FOUNDATION_EXTERN、FOUNDATION_EXPORT、FOUNDATION_IMPORT、UIKIT_EXTERN
    
    // NSObjCRuntime.h
    #if defined(__cplusplus)
    // C++ 下兼容 C++ 的 extern
    #define FOUNDATION_EXTERN extern "C"
    #else
    #define FOUNDATION_EXTERN extern
    #endif
    
    
    #define FOUNDATION_EXPORT  FOUNDATION_EXTERN
    #define FOUNDATION_IMPORT FOUNDATION_EXTERN
    
    // UIKitDefines.h
    // UIKIT_EXTERN 还声明了符号的可见范围
    #ifdef __cplusplus
    #define UIKIT_EXTERN		extern "C" __attribute__((visibility ("default")))
    #else
    #define UIKIT_EXTERN	        extern __attribute__((visibility ("default")))
    #endif
    
  • 对于选择 extern 还是以上 SOME_EXTERN,c 得出以下的结论:

    • 对于这些固定前缀的 extern 应当在其范围下使用,即 FOUNDATION_* 在 Foundation 框架内部使用,UIKIT_*UIKit 内部使用,在我们自己的库内应当推荐自定义 SOME_* 来使用,而不是直接套用其他库内的宏定义;
    • 默认的 extern 由于可能不兼容 C++,那么在库中需要兼容 C++ 时应当使用 extern "C"

用枚举表示状态、选项、状态码

  • Obj-C 中的枚举(enum)来自 C 语言,而 C++11 标准使得枚举可以定义其底层数据类型(Underlying Type)。

    // 实现枚举所用的数据类型取决于编译器
    // 枚举的底层数据类型不是固定的,其二进制位(bit)的个数必须能完全表示下枚举编号
    // 比如 ImageSourceType 可以使用 char 类型(但在 Xcode 里其底层数据类型是第一项默认为 0 的 int 类型)
    enum ImageSourceType {
    ImageSourceTypeCamera,
    ImageSourceTypeGallery
    };
    
    enum ImageSourceType type1 = ImageSourceTypeCamera;
    
    // typedef 可以简化枚举类型声明
    typedef enum ImageSourceType ImageSourceType;
    ImageSourceType type2 = ImageSourceTypeGallery;
    
    // C++ 11 标准,显式声明枚举底层数据类型
    enum NetworkType: NSInteger {
    NetworkTypeUnknown = -1, // 显式为从 -1 开始
    NetworkTypeWiFi   // 0 递增
    NetworkCellular   // 1
    };
    
    // 枚举的向前声明(在 .m 中引入或实现即可)
    // Some.h
    enum NetworkType: NSInteger;
    
  • 枚举中使用按位或操作符(Bitwise OR Operator)可以使得枚举的每个选项均可启用或禁用:

    // UIViewAutoresizingNone 是无法与其他项目同时存在,因此为 0
    
    // UIView.h
    typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
    };
    
    // 使用时可以使用按位或来组合多个选项:
    UIViewAutoresizing resize = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    
    // 判断时使用按位与(Bitwise AND Operator)即可
    if (resize & UIViewAutoresizingFlexibleWidth) {
    NSLog(@"UIViewAutoresizingFlexibleWidth");
    }
    if (resize & UIViewAutoresizingFlexibleHeight) {
    NSLog(@"UIViewAutoresizingFlexibleHeight");
    }
    
    // OUTPUT:
    // UIViewAutoresizingFlexibleWidth
    // UIViewAutoresizingFlexibleHeight
    
    /* NS_ENUM supports the use of one or two arguments. The first argument is always the integer type used for the values of the enum. The second argument is an optional type name for the macro. When specifying a type name, you must precede the macro with 'typedef' like so:
    
    typedef NS_ENUM(NSInteger, NSComparisonResult) {
    ...
    };
    
    If you do not specify a type name, do not use 'typedef'. For example:
    
    NS_ENUM(NSInteger) {
    ...
    };
    */
    #define NS_ENUM(...) CF_ENUM(__VA_ARGS__)
    #define NS_OPTIONS(_type, _name) CF_OPTIONS(_type, _name)
    
    // CFAvailability.h
    #define CF_ENUM(...) __CF_ENUM_GET_MACRO(__VA_ARGS__, __CF_NAMED_ENUM, __CF_ANON_ENUM, )(__VA_ARGS__)
    // 根据是否按照 C++ 模式编译而不同
    #define __CF_ENUM_GET_MACRO(_1, _2, NAME, ...) NAME
    #if (__cplusplus && __cplusplus >= 201103L && (__has_extension(cxx_strong_enums) || __has_feature(objc_fixed_enum))) || (!__cplusplus && __has_feature(objc_fixed_enum))
    #define __CF_NAMED_ENUM(_type, _name)     enum __CF_ENUM_ATTRIBUTES _name : _type _name; enum _name : _type
    #define __CF_ANON_ENUM(_type)             enum __CF_ENUM_ATTRIBUTES : _type
    #define CF_CLOSED_ENUM(_type, _name)      enum __CF_CLOSED_ENUM_ATTRIBUTES _name : _type _name; enum _name : _type
    #if (__cplusplus)
        #define CF_OPTIONS(_type, _name) _type _name; enum __CF_OPTIONS_ATTRIBUTES : _type
    #else
        #define CF_OPTIONS(_type, _name) enum __CF_OPTIONS_ATTRIBUTES _name : _type _name; enum _name : _type
    #endif
    #else
    #define __CF_NAMED_ENUM(_type, _name) _type _name; enum
    #define __CF_ANON_ENUM(_type) enum
    #define CF_CLOSED_ENUM(_type, _name) _type _name; enum
    #define CF_OPTIONS(_type, _name) _type _name; enum
    #endif
    
    // Some.mm
    typedef enum Direction: int FooDirection;
    enum Direction: int {
    Up      = 1 << 0,
    Down    = 1 << 1,
    Left    = 1 << 2,
    Right   = 1 << 3
    };
    
    // 在使用或运算操作两个枚举值时,C++ 认为运算结果的数据类型应当是枚举的底层数据类型,即 `NSUInteger`,且 C++ 不支持将底层数据类型隐式转换为枚举类型本身
    // ERROR: Cannot initialize a variable of type 'FooDirection' (aka 'Direction') with an rvalue of type 'int'
    // FooDirection direct = Up | Left;
    
  • Foundation 框架中枚举的辅助宏具备向后兼容(Backward Compatibility)能力,即根据目标平台决定相应的语法;

  • 凡是需要按位或操作来组合的枚举都应当使用 NS_OPTIONS 来定义;否则使用 NS_ENUM

对象、消息、运行期

理解「属性」这一概念

  • 应用程序二进制接口(Application Binary Interface,即 ABI)定义了很多内容,其中有生成代码时所应遵循的规范。
  • Obj-C 中实例变量(ivar)的内存布局在编译时刻固定,因此访问实例变量时,编译器会将其替换为偏移量(Offset),而偏移量是硬编码,表示其距离内存起始地址的长度;
  • 如果代码使用了编译时刻计算的偏移量,那么在修改类定义之后必须重新编译,即重新计算偏移量;
  • 对于旧的类定义链接了新的类定义会出现不兼容情况,Obj-C 的做法如下:

    • 将实例变量当作一种存储偏移量所用的特殊变量,交由类对象保管,偏移量将在运行时查找,这样即使类定义变化了,也能找到正确的偏移量,甚至可以在运行时新增实例变量;
    • 尽量不要直接访问实例变量,而是通过 getter & setter,Obj-C 中可以使用 @property 语法;
  • 自动合成(Autosynthesis):编译器会自动为 Obj-C 中的属性便携其所需的方法,该过程由编译器在编译时刻执行;

  • 编译器还会自动为 Obj-C 中的属性添加适当类型的实例变量,并在属性名前加 _ 前缀作为实例变量名。

    // Some.h
    @property NSString *foo;
    @property NSString *bar;
    
    // Some.m
    // @dynamic 将不会自动合成,也不会创建实例变量
    // 但使用 getter & setter 可以编译,因为其相信会在运行时找到这些方法
    @dynamic bar;
    
    // @synthesize 指定了属性的实例变量名
    @synthesize foo = _foooo;
    
    self->_foooo = @"Foo";
    NSLog(@"%@", [self foo]);
    
  • 原子性

    • 在化学中,原子是不可分割的最小粒子,因此可以理解为一个原子操作是不会被中断的,即线程安全的(但这并不代表 atomic 的属性是线程安全的);
    • 默认情况下(即不明确指定原子性时),由编译器所合成的方法(即 getter & setter)会通过锁定机制确保其原子性(Atomicity);
    • 自己实现的 getter & setter 需要自己来保证相应的原子性(即自己实现时与声明的原子性没有关系);
    • 若属性为 nonatomic 则不使用同步锁,iOS 中使用同步锁开销较大,可能带来性能问题,因此 nonatomic 性能会好点且 atomic 并不能保证操作是原子的,因此通常都使用 nonatomic
  • 读写权限

    • readwrite 即拥有 getter & setter,readonly 即仅拥有 getter
    • readwrite 可以用在 .h 声明为 readonly 但内部类扩展中声明为 readwrite 来允许内部设置的情况。
  • 内存管理语义

    • assign:setter 只会针对纯量类型(Scalar Type)例如 CGFloatNSInteger 等的简单赋值操作;
    • strong:定义一种拥有关系,为这种属性设置新值时,setter 会先保留新值,并释放旧值,然后再将新值设置上去;
    • weak:定义一种非拥有关系,为这种属性设置新值时,setter 既不保留新值,也不释放旧值,类似 assign,但当属性所指向的对象销毁时,属性值也会置为 nil
    • unsafe_unretained:内存语义与 assign 相同,但适用于对象类型,定义一种非拥有(不保留)关系,但当属性所指向的对象销毁时,属性值不会置为 nil(与 weak 的差异;
    • copy:类似 strong,但 setter 不保留新值,而是将其拷贝;当属性类型为 NSString * 时,可以使用 copy 保护其封装性,因为 setter 接收的新值有可能是指向 NSMutableString 的实例,若不拷贝则可能会被外界随时更改,因此需要拷贝为不可变的情况;
  • 方法名

    • getter=<name> 可以指定 getter 名称;
    • setter=<name> 可以指定 setter 名称;
  • 属性特性(Attribute)只在自动生成时有效,自己实现的 getter & setter 要保证其具备相应的属性特性。

    @interface Foo : NSObject
    // 即使 readonly 也要注明 copy,因为在 init 中进行了 copy
    @property (copy, readonly) NSString *bar;
    @end
    
    @implementation Foo
    
    - (instancetype)initWithBar:(NSString *)bar
    {
    self = [super init];
    if (self) {
        // 要保证属性的内存语义
        _bar = [bar copy];
    }
    return self;
    }
    
    @end
    

在对象内部尽量直接访问实例变量

  • 直接访问实例变量(ivar)与通过属性(getter & setter)访问的区别:
    • 直接访问不经过方法派发,速度更快;
    • 直接访问不经过所定义的内存管理语义(例如 copy);
    • 直接访问不经过 KVO;
    • 通过属性访问便于 Debug(可以打断点)。
  • 因此建议读取时直接访问,写入时通过属性,当然具体问题见仁见智。

    // 懒加载的属性必须要通过 getter 访问
    - (NSObject *)foo {
    if (!_foo) {
        _foo = [NSObject new];
        return _foo;
    }
    }
    

理解「对象等同性」这一概念

  • 对于 Obj-C 中的对象类型,== 比较的是两个指针本身的值(即存储的地址)是否一致。
  • NSString 类中的 isEqualToString:isEqual: 方法速度更快(但其实差别不大)。

    - (BOOL)isEqual:(id)object {
    // 地址一致,则必然一样
    if (self == object) return YES;
    // 如果非同类,则不相等(子父类之间比较视情况而定)
    if ([self class] != [object class]) return NO;
    
    Foo *foo = (Foo *)object;
    if (![_bar isEqualToString:foo.bar])
        return NO;
    
    if (_baz != foo.baz)
        return NO;
    
    return YES;
    }
    
  • 若两个对象相等,hash 也相等;hash 相等,但两个对象并不一定相等。

  • 实现 hash 方法要在减少碰撞与降低运算复杂程度(性能影响)之间权衡:

    // 返回固定值
    // 可行,但在 collection 中使用该类型对象将产生性能问题,因为 collection 检索哈希表(Hash Table)时,会用对象的哈希码(Hash Code)做索引。
    // 假如某个 collection 是用 set 实现的,其可能会根据哈希码把对象分装到不同的数组中。
    // 若每个对象都返回相同的哈希码,则需要将所有对象全部扫描。
    - (NSUInteger)hash {
    return 1024;
    }
    
    // 属性拼接字符串并 hash
    // 但需承担创建字符串的开销;添加到 collection 中由于必须计算哈希码也会产生性能问题
    - (NSUInteger)hash {
    NSString *str = [NSString stringWithFormat:@"%@:%i", _bar, _baz];
    return [str hash];
    }
    
    // 效率高,范围确定
    // 虽然会碰撞,但允许
    - (NSUInteger)hash {
    NSUInteger bar = [_bar hash];
    NSUInteger baz = _baz;
    return bar ^ baz;
    }
    
  • 编写特定类的等同性判定方法(比如 isEqualToString:)时,应一并重写 isEqual: 方法:

    - (BOOL)isEqualToFoo:(Foo *)foo {
    if (self == object) return YES;
    
    if (![_bar isEqualToString:foo.bar])
        return NO;
    
    if (_baz != foo.baz)
        return NO;
    
    return YES;
    }
    
    - (BOOL)isEqual:(id)object {
    // 相同类则自己判断
    if ([self class] == [object class]) {
        return [self isEqualToFoo:(Foo *)object];
    }
    
    // 不同类则交给父类
    return [super isEqual:object];
    }
    
  • 等同性判定的执行深度取决于受测对象,即若有某个 ID 字段则可以直接根据该 ID 来判定。

    // 将对象放入 collection 中,不应再改变其哈希码,即需要确保哈希码并非根据对象的可变部分计算得出
    NSMutableSet *set = [NSMutableSet new];
    NSMutableArray *arr1 = [@[@1] mutableCopy];
    NSMutableArray *arr2 = [@[@1, @2] mutableCopy];
    
    [set addObject:arr1];
    [set addObject:arr2];
    
    // {((1), (1,2))}
    NSLog(@"%@", set);
    
    // Set 中存在了两个完全相同的结构
    // {((1,2), (1,2))}
    [arr1 addObject:@2];
    NSLog(@"%@", set);
    

以「类族模式」隐藏实现细节

  • 类族(Class Cluster,又称类簇)是一种隐藏抽象基类背后的实现细节的模式(例如 UIButtonbuttonWithType: 类方法)。

    // 模式是不限定于语言的,因此这里使用了 Swift 来简单实现
    
    enum FooType {
    case first
    case second
    }
    
    class Foo {
    static func build(_ type: FooType) -> Foo {
        switch type {
        case .first: return FooFirst()
        case .second: return FooSecond()
        }
    }
    
    func bar() {
        fatalError("bar should be implemented in subclasses.")
    }
    }
    
    class FooFirst: Foo {
    override func bar() {
        print(#function)
    }
    }
    
    class FooSecond: Foo {
    override func bar() {
        print(#function)
    }
    }
    
    // 类族模式目的是隐藏内部实现的细节,因此外界创建的变量虽然是工厂类的类型,但实际上其实是内部子类的类型。
    // 因此在 Obj-C 中使用 isMemberOfClass 判断类是否为工厂类本身,则会返回 NO。
    // foo 在 Swift 中被推断为 Foo
    let foo = Foo.build(.first)
    
    print(foo is Foo)
    print(foo is FooFirst)
    print(foo is FooSecond)
    
    // OUTPUT:
    // true
    // true
    // false
    
  • 系统框架中有许多类族,比如大部分的 collection 类(比如 NSArray);

  • 由于 NSArray 是类族,因此 [fooArr class] == [NSArray class] 将一直返回 false,但可使用 [fooArr isKindOfClass:[NSArray class]]

  • 向类族中新增实体子类需要遵守以下规则:

    • 子类应该继承自类族中的抽象基类;
    • 子类应该定义自己的数据存储方式:类族只是定义了通用的接口,实际存储的方式则由背后具体的实体子类承担;
    • 子类应当覆写超类文档中指明需要覆写的方法:类族中的通用接口需要子类实现。

在既有类中使用关联对象存放自定义数据

理解 objc_msgSend 的作用

  • 略;关于 objc_msgSend,可详见 objc_msgSend 一文。

理解消息转发机制

Reference