第一章:熟悉 Objective-C
第1条:了解 Objective-C
语言的起源
第2条:在类的头文件中尽量少引入其他头文件
背景:
使用 #import "ClassName.h"
可以引入其他文件的所有接口细节。
问题:
- .h头文件中,在编译一个使用了某类的文件时,不需要知道这个类的全部细节,只需要知道有这个类就好。
- A头文件中引入B头文件,C头文件引入A头文件,就会一起引入B头文件的所有内容。此过程若持续下去,则要引入许多根本用不到的内容,这当然会增加编译时间。
解决办法:
使用
@class ClassName
“向前声明”(forward declaring),只声明有这个类,没有具体细节,可以解决上述问题。除非确实有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合(coupling)。
继承 和 遵从协议 不能使用向前声明。 有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
向前声明的作用:
- 防止引入根本用不到的内容,减少头文件细节引用。
- 解决两个类相互引用的问题。
将引入头文件的时机尽量延后,只在确有需要时才引入,这样可以减少类的使用者所需引入头文件的数量。
第3条:多用字面量语法,少用与之等价的方法
使用字面量语法(literal syntax)可以缩减源代码的长度,使其更为易读。
第4条:多用类型常量,少用 #define
预处理指令
问题:
#define
定义的常量没有类型信息,编译器只会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
解决办法:
- 在实现文件中使用 static const 来定义“只在编译单元内可见的变量”。由于此类常量不在全局符号表中,所以无须为其名称加前缀。代码实现如下:
// .h 文件
@interface 类名: 父类名
...
@end
// .m 文件
// 类内使用
static const 类型 常量名 = 常量值;
@implementation 类名
...
@end
- 在头文件中使用 extern 来声明的全局变量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区分,通常用与之相关的类名做前缀。
// .h 文件
// 类外可用声明
extern 类名 const 常量名;
@interface 类名: 父类名
...
@end
// .m 文件
// 类外可用声明
类名 const 常量名 = 常量;
@implementation 类名
...
@end
常量名称常用命名法是:
- 只在类内使用,在前面加字母
k
。 - 类外也可使用,以类名最为前缀。
第5条:用枚举表示状态、选项、状态码
- 使用枚举,给这些值起个易懂的名字。
- 将枚举值定义为2的幂,多枚举选项可以同时使用,可以通过按位或操作进行组合。
- 定义枚举时,指明其底层数据类型,便于处理。
- 在处理枚举类型的
switch
语句中不要实现default
分支,便于加入新枚举后,编译器报错,知道需要修改的地方。
第二章:对象、消息、运行期
第6条:理解 “属性” 这一概念
第7条:在对象内部尽量直接访问实例变量
第8条:理解 “对象等同性” 这一概念
==
操作符比较的是两个指针本身,不是其所指的对象。NSObject
协议中声明的isEqual
方法判断两个对象的等同性。isEqual
默认实现是:当且仅当其 “指针值” 完全相等时,这两个对象才相等。- 特定类具有等同性的判定方法。
NSString
:isEqualToString:
NSArray
:isEqualToArray:
NSDIctionary
:isEqualToDictionary:
若比较的对象不是对应的类型,就会抛出异常,崩溃。 - 等同性判定的执行深度取决于受测对象。 对象个数相同的数组比较,对应位置上的对象均相等,数组就相等,这叫做“深度等同性判定”。 为了性能,建议尽可能的降低深度。
- 容器中可变类的等同性判定 将某对象放入容器后,又修改其内容,那么后面的行为将很难预料,建议不要这么做。
第9条:以 “类族模式” 隐藏实现细节
类族模式:使用继承,实现多种职能的子类,父类通过设定不同的类型来创建某种子类,执行其相应的职能。 作用:将实现细节隐藏在一套简单的公共接口后面。 需要注意的是,创建的实例的真实类型是什么,需要我们知道
新增 Cocoa
中 NSArray
这样的类族的子类,需要遵守以下几条规则:
- 子类应该继承自类族中的抽象基类。
- 子类应该定义自己的数据存储方式。
NSArray
本身只是包在其他隐藏对象外面的壳,它仅仅定义了所有数组都需具备的一些接口。 - 子类应当覆写超类文档中指明需要覆写的方法。 在每个抽象的基类中,都有一些子类必须覆写的方法。
第10条:在既有类中使用关联对象存放自定义数据
在对象中存放相关信息:
- 从对象所属的类中继承一个子类,然后修改这个子类对象。
- 通过“关联对象”的特性,给某对象关联许多其他对象,这些对象通过“键”来区分。
关联对象方法:
以给定的键和存储策略为某对象设置关联对象值
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
参数说明:
object
关联的源对象。key
关联的key
。通常使用静态全局变量做键。value
关联key
所对应的值。传nil
可以清除现有的关联。policy
关联的存储策略,也就是对应的内存管理语义,是一个枚举值。
objc_AssociationPolicy
枚举值如下表: | 关联类型 | 等效的 @property 属性 | | --- | --- | OBJC_ASSOCIATION_ASSIGN | assign | OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic, retain | OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic, copy | OBJC_ASSOCIATION_RETAIN | retain | OBJC_ASSOCIATION_COPY | copy |
根据给定的键从某对象中获取对应的关联对象值。
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
移除指定对象的全部关联对象。
objc_removeAssociatedObjects(id _Nonnull object)
注:只有在其他方法都行不通时才考虑使用它。若是滥用,则很快就会令代码失控,使其难于调试。
第11条:理解 objc_msgSend
的作用
OC方法调用
代码:
id returnValue = [someObject messageName: paramter];
代码说明:
someObject
接受者。messageName
选择子。选择子和参数合起来称为“消息”
底层C语言代码实现:
id returnValue = objc_msgSend(someObject, @selector(messageName:), paramter);
消息发送机制核心函数
原型代码:
void objc_msgSend(id self, SEL cmd, ...)
这是个“参数个数可变函数”,能接受两个或者两个以上的参数。 参数说明:
self
接受者。cmd
选择子(方法的名字)。- 后续参数为消息中的那些参数,顺序不变。
具体实现:
注:OC
方法调用需要很多步骤,较为耗时。objc_msgSend
会将匹配结果缓存在“快速映射表”,每个类都有这样一块缓存。虽然还是不如“静态绑定的函数调用操作”那么迅速,但是也不会慢很多。
边界情况:
objc_msgSend_stret
待发送的消息要返回结构体,就交由此函数处理。objc_msgSend_fpret
待发送的消息要返回浮点数,就交由此函数处理。objc_msgSendSuper
要给超类发消息,就交由此函数处理。如:[super message:parameter]
。
尾调用优化
Objective-C
对象的每个方法都可以看做简单的 C
函数,其原型如下:
Class_selector(id self, SEL _cmd, ...)
这个原型和 objc_msgSend
函数很像,是为了利用 “尾调用优化” 技术。令 “跳至方法实现” 这一操作跟简单些。
使用范围:某函数的最后一项操作仅仅是调用另一个函数而不会将其返回值另作他用。 步骤:编译器会生成调转至另一函数所需的指令码,不会向调用堆栈中推人新的 “栈帧”。 不优化后果:
- 每次调用
Objective-C
方法之前,都需要为调用objc_msgSend
函数准备 “栈帧”,可以在 “栈踪迹” 中看到。 - 过早的发生 “栈溢出” 现象。
第12条:理解消息转发机制
消息转发: 第一阶段:动态方法解析: 征询接收者(所属的类),看其是否能动态添加方法,以处理当前这个 “未知的选择子”。 第二阶段: 1. 备援的接收者: 请接收者看看有没有其他对象(备援的接收者)能处理这条消息。 2. 完整的消息转发机制: 运行期系统会把与消息有关的全部细节都封装到
NSInvocation
对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。
动态方法解析
是否能新增一个实例方法来处理选择子,调用方法如下:
// 实例方法
+ (BOOL)resolveInstanceMethod:(SEL)selector
// 类方法
+ (BOOL)resolveClassMethod:(SEL)selector
使用前提:相关方法的实现代码已经写好,只等运行的时候动态插入到类里面。
动态添加方法函数如下:
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
参数说明:
cls
添加方法的类name
被添加方法的名字imp
函数指针,指向待添加的方法(C语言实现)。type
待添加方法的 “类型编码” 。
备援接收者
是否有其他对象处理这条消息,调用方法如下:
- (id)forwardingTargetForSelector:(SEL)selector
若找到备援对象,该方法返回备援对象,反之,返回 nil
。
注意:我们无法操作经由这一步所转发的消息。
完整的消息转发机制
创建 NSInvocation
对象,此对象包含 选择子 、目标 及 参数。
消息派发调用方法如下:
- (void)forwardInvocation:(NSInvocation *)invocation
若发现某调用操作不应由本类处理,则向上寻找,直至 NSObject
。如果最后调用了 NSObject
的方法,那么该方法还会继续调用 doesNotRecognizeSelector:
以抛出异常,表明选择子最终未能得到处理。
总结:
接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。
消息转发代码:https://github.com/AlonerOwl/Runtime/tree/master/Runtime/MessageSend
第13条:用 ”方法调配(method swizzling)技术“ 调试 ”黑盒方法“
函数指针(IMP):id (*IMP)(id, SEL, ...)
方法表:函数指针所组成的一个集合。
操作类的方法表:
- 新增选择子
第12条已经说过了。BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
- 改变某选择子所对应的方法实现
- 交换两个选择子所映射到的指针,也就是交换两个方法实现。 方法交换函数:
参数:两个待交换的方法实现 获取方法实现函数:func method_exchangeImplementations(_ m1: Method, _ m2: Method)
Method class_getInstanceMethod(Class cls, SEL name)
这个方法可以为那些 “完全不知道具体实现” 的黑盒方法增加日志记录功能,这非常有助于程序调试。 若是滥用,反而会令代码变的不易读懂且难于维护。
method swizzling代码:https://github.com/AlonerOwl/Runtime/tree/master/Runtime/MethodSwizzling