第1章 熟悉Objective-C
第1条 了解Objective-C语言的起源
- Objective-C是一种“消息结构”的语言,而非“函数调用”语言。
- 关键区别在于:使用消息结构的语言,其运行时所执行的代码由运行环境来决定;而使用函数调用语言,则由编译器决定。若是函数调用语言,若调用的函数是多态的,则需要按照“虚方法表”来确定到底应该执行哪个函数实现。(即需要“运行时派发”(runtime method binding)),而“消息结构语言”无论是否多态,总是在要运行时才会去查所执行的方法,实际上编译器甚至不关系消息是何种类型,接收消息的对象问题也要在运行时处理,这个过程叫做“dynamic binding”。
- Objective-C的重要工作都是由“运行期组件(runtime component)”完成的,而非编译器完成的。使用Objective-C的面向对象特性的所需全部数据结构及函数都在运行期组件里面。举例:运行期组件含有全部内存管理方法。通俗来讲:只要重新运行Objective-C工程即可提升应用程序性能,而工作都在“编译期”完成的语言,如果想获得性能的提升,必须要重新编译。
- Objective-C语言中的指针用来指向对象,这点完全照搬C语言。
NSString *string = @"string";
它声明了一个指向NSString类型的指针string,这表示了该string指向的对象分配在堆上,在Objective-C中,所有对象都分配在堆上,而string本身分配在栈上。 - 分配在堆中的内存必须直接管理,而分配在栈上的内存则会在其栈帧弹出时,自动清理。
-
CGRect rect
表示的是C语言中的结构体类型,他们会使用栈空间。因为若整个Objective-C语言都使用对象,则性能会受影响。
第2条 在类的头文件中尽量少引入其他头文件
- 将引入头文件的时机尽量延后,只在确定有需要时才引入,这样就可以减少类的使用者所引入的头文件数量。而若是把头文件一股脑的全部引入,会增加很多不必要的编译时间。若需要在头文件中声明一个其他类的
@property
,则可以首先使用向前声明@class XXX.h
这样就可以告诉编译器,我先引入这个类,实现的细节以后再告诉你。 - 使用向前声明同时也可以解决了两个类相互引用的问题。
- 要点:
- 除非有必要,否则不要引入头文件,一般来说,应在某个类的头文件中尽量使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合。
- 有时无法使用向前声明,比如要声明某个类遵守某个协议,这样的话尽量把“该类所遵守的协议” 这条声明放在“class-continuation分类”中,如果不行,还可以把分类放在一个单独的头文件中再引入。
第3条 多用字面量语法,少用与之等价的语法
- 字面数值NSNumber
- 普通方法:
NSNumber *someNumber = [NSNumber numberWithInt:1];
等价的字面量方法:NSNumber *someNumber = @1;
能够以NSNumber类型表示的所有类型都可以使用该语法。字面量语法也可用于下面的表达式:
int x = 5;
int y = 6;
NSNumber *num = @(x * y);
- 字面数组NSArray
- 普通方法:
NSArray *array = [NSArray arrayWithObjects:@"cat", @"dog", @"pig", nil];
字面量方法:NSArray *array = @["dog", @"cat", @"pig"];
该方法在语义上也是等效的,但是更为简便。若要取出第1个元素则array[0]
- 需要注意的是,当使用字面量方式创建数组时,若数组元素对象中有nil,则会抛出异常,因为字面量语法实际上是一种语法糖,其等效于先创建一个数组,再把所有元素添加到这个数组中,而使用普通方法创建数组时,若数组某个元素为nil,则会直接在该位置完成数组的创建,nil之后的元素都将被丢弃,并且也不会报错。所以使用字面量语法更为安全,抛出异常终止程序总比直接得到错误的结果要好。
- 字面字典NSDictionary
- 使用字面量语法创建字典会使得字典更加清晰明了。并且与数组一样,字面量创建字典时,若遇到nil也会抛出异常。
- 字典也可以像数组那样用字面量语法访问。普通方法:
[data objectForKey:@"hehe"];
等价于字面量方法:data[@"hehe"]
;
- 可变数组与字典
- 也可以使用字面量的方式修改其中的元素值:
mutableArray[1] = @"gege";
- 局限性
- 使用字面量语法创建出来的各个Foundation框架中的对象都是不可变类型的,若要将其转化为可变类型,则需要复制一份
NSMutableArray *mutable = [@[@"cat", @"dog", @"pig"] mutableCopy];
这样做会多调用一个方法,还要再多创建一个对象,但是好处还是大于这些缺点的。 - 限制:除了字符串外,所创建出来的对象必须属于Foundation框架才行,即NSArray的子类就不可以使用字面量语法,不过一般也不需要自定义子类。
第4条 多用类型常量,少用#define预处理指令
- 当使用
#define
预处理指令定义变量时,假设#define ANIMATION_DURATION 0.3
时,你以为已经定义好了,实际上当编译时,会将整个程序所有叫做ANIMATION_DURATION
的值都替换为0.3
,也就是说假设你在其他文件也定义了一个ANIMATION_DURATION
,它的值也会被改变。要想解决这个问题,则需要充分利用编译器的特性,比如:static const NSTimeInterval kAnimationDuration = 0.3;
这样就定义了一个名为kAnimationDuration
的常量。 - 若不打算公开某个常量,则应该讲它定义在
.m
文件中,变量一定要同时用static
和const
来定义,使用const
声明的变量如果视试图修改它的值,编译器就会报错。而使用static
声明的变量,表示该变量仅仅在定义此变量的编译单元中可见(即只在此.m
文件中可见)。假设不为变量添加static
修饰符,则编译器会自动为其创建一个external symbol外部符号
此时若另一个.m
文件中也定义了同名变量,则会报错。 - 实际上若一个变量既声明为
static
又声明为const
,name编译器会直接像#define
一样,把所有遇到的变量都替换为常量。不过还是有一个区别:用这种方式定义的常量带有类型信息。 - 当需要对外公开某个常量时,可以使用
extern
修饰符来修饰常值变量。例如在通知中,注册者无需知道实际字符串的具体值,只需要以常值变量来注册自己想要接收的通知即可。此类变量常放在“全局符号表”中,以便可以再定义该常量的编译单元之外使用。例如
// .h
extern NSString *const LYStringConstant;
// .m
NSString *const LYStringConstant = @"VALUE";
- 使用上述方式,即可在头文件中声明,在实现文件中定义。一旦编译器看到
extern
关键字,就知道如何在引入此头文件的代码中处理常量了。此类常量必须要定义,并且只能定义一次,通常都是在声明该常量的.m
文件中定义该常量。编译器在此时,会在“data segment”中为字符串分配存储空间。链接器会把此目标文件与其他目标文件相链接,生成最终的二进制文件。 - 注意常量的名字,为了避免名称冲突,一般前缀都为与之相关的类。
- 在实现文件中使用
static const
定义“只在编译单元内可见的常量”,并且通常名称前加前缀k
。
第5条 用枚举表示状态,选项,状态码
- 应该用枚举来表示状态机的状态,传递给方法的选项以及状态码等值,给这些值通俗易懂的名字。
- 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,应该使用
NS_OPTIONS
通过按位与操作将其组合起来。 - 用
NS_ENUM
与NS_OPTIONS
宏来定义枚举类型,并指明其底层的数据类型,这样做可确保枚举是用开发者所选的底层数据类型实现的。 - 在处理枚举类型的
switch
语句中,不要使用default
分支,这样加入新枚举之后编译器便会提示开发者:switch语句还未处理所有的枚举。
第2章 对象,消息,运行期
- 使用
Objective-C
编程时,对象就是“基本的构造单元”(buliding block)
,在对象间 传递数据 并且 执行任务 的过程就叫做 “消息传递Messaging” 一定要熟悉这两个特性的工作原理。 - 当程序运行后,为其提供支持的代码叫做:
Objective-C运行期环境(Objective-C runtime)
,它提供了一些使得对象之间能够传递消息的重要函数,并且包含创建类实例所用的全部逻辑。都是需要理解的。
第6条 理解“属性”这一概念
- 当直接在 类接口 中定义 实例变量 时,对象布局在 “编译期” 就已经固定了。只要碰到访问该实例变量的方法,编译器就自动将其替换为 “偏移量(offset)”,并且这个偏移量是 硬编码 ,表示该变量距离存放对象的内存区域的起始地址有多远,这样做一开始没问题,但是一旦要再新添加一个实例变量,就需要重新编译了,否则把偏移量硬编码于其中的那一些代码都会读取到错误的值。
Objective-C
避免这个错误的做法是把 实例变量 当做一种存储 偏移量 所用的 “特殊变量” ,交给 “类对象” 保管。偏移量会在运行期 runtime
查找,如果类的定义变了,那么存储的偏移量也就变了。这是其中的一种对于硬编码的解决方案。还有一种解决方案就是尽量 不要直接 访问实例变量,而是通过 存取方法 来访问。也就是声明属性@property
。 - 在对象接口的定义中,可以使用 属性 来访问封装在对象中的数据。编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的变量。此过程叫做 “自动合成” ,这个过程由编译器在编译期间执行,所以编译器里看不到这些
systhesized method合成方法
的源代码。编译器还会自动向类中添加适当类型的实例变量,并在属性名称前面加下划线。 - 可以使用
@synthesize
语法来指定实例变量的名字@synthesize firstName = _myFirstName;
。 - 如果不想让编译器自动合成存取方法,则可以使用
@dynamic
关键字来阻止编译器自动合成存取方法。并且在编译访问属性的代码时,编译器也不会报错。因为他相信这些代码可以在runtime
时找到。 - 属性特质 属性可以拥有的特质分为四类
- 原子性
- 在默认情况下,由编译器所合成的方法会通过锁机制保证其原子性,如果属性具备
nonatomic
特质,则不使用同步锁,一般情况下在iOS开发中,都将属性声明为nonatomic
修饰的,因为原子性将会耗费大量资源并且也不能保证“线程安全”,若要实现“线程安全”则需要更深层的锁机制才行。 -
atomic
与nonatomic
的区别在于:具备atomic
的get
方法会通过锁机制来确保操作的原子性,也就是如果两个线程同时读取同一属性,无论何时总是能看到有效的值。而若不加锁,当其中一个线程在改写某属性的值时,另一个线程也可以访问该属性,会导致数据错乱。
- 在默认情况下,由编译器所合成的方法会通过锁机制保证其原子性,如果属性具备
- 读/写权限
-
readwrite
特质的属性,若该属性由@synthesize
实现,则编译器会自动生成这两个方法。 -
readonly
特质的属性只拥有读方法。只有在该属性由@synthesize
实现时,编译器才会为其添加获取方法。
-
- 内存管理语义
-
assign
:只针对“纯量类型”(CGFloat
或NSInteger
等) -
strong
:表明该属性定义了一种 “拥有关系” ,即为这种属性设置新值时,设置方法会先保留新值,再释放旧值,然后再将新值设置上去。 -
weak
:表明该属性定义了一种 “非拥有关系” ,即为这种属性设置新值时,设置方法会既不保留新值,也不释放旧值,此特质同assign
类似,然而在属性所指的对象遭到摧毁时,该属性值也会清空(即指向nil)。 -
copy
:此特质所表达的从属关系同strong
类似,只是,设置方法并不保留新值,而是将其“拷贝”(copy)。当属性类型为NSString*
时,经常使用此特性来保证其封装性。因为传递给set
方法的新值有可能指向一个可变字符串,由于可变字符串是字符串的子类,所以字符串属性指向他并不会报错,而此时,一旦可变字符串的值改变了,字符串的值也会偷偷的跟着改变,会导致在我们不知情的情况下,NSString*
属性的值就改变了,所以应该拷贝一份可变字符串的不可变值immutable
的字符串,确保对象中的字符串不会无意间变动。 -
unsafe_unretained
:此特质所表达的语义同assgin
相同,但它适用于对象类型,该特征表达了一种 “非拥有关系” ,当目标对象遭到摧毁时,不会自动指向nil(不安全)
-
- 方法名
-
@property (nonatomic, getter=isOn) BOOL on;
通过如下方式来改变get
方法的方法名。
-
- 原子性
第7条 在对象内部尽量直接访问实例变量
- 由于不经过
Objective-C
的 “方法派发” ,所以直接访问实例变量的速度比较快。 - 直接访问实例变量时,不会调用其
setter
方法,这就绕过了为相关属性所定义的 “内存管理语义” 。比方说:在ARC环境下直接访问一个声明为copy的属性,将不会拷贝该属性。而是直接丢弃旧值保留新值。 - 如果直接访问实例变量,则不会触发KVO通知。这样做是否产生问题还要看具体的问题。
- 通过属性来访问实例变量有助于排查与之相关的错误,因为可以给
getter/setter
新增断点,来监控其值。 - 在对象内部读取数据时,应该直接通过实例变量来读取,写数据时,应该通过属性来写。
- 在初始化或dealloc方法中,总是应该直接通过实例变量来读写数据。
- 当使用懒加载方法加载数据时,需要通过属性来读数据。
第8条 理解“对象等同性”这一概念
- 按照 “ == ” 操作符比较出来的结果未必使我们想要的,因为它实际上是在比较两个实例变量所指向的对象是否为同一值,换句话说,它实际上比较的是实例变量所指向堆内存中的对象地址是否为同一个。而当我们要必要两个对象是否相同时,往往想要比较的是两个对象所代表的逻辑意义上的是否相等。
- 所以这个时候需要使用
NSObject
协议中声明的isEqual
方法来判断两个对象的等同性。NSObject
协议中有两个用于判断等同性的关键方法:- (BOOL)isEqual:(id)object;
- (NSInteger)hash;
而NSObject
类对这两个方法的默认实现只是简单的比较两个对象的地址是否相等。 - 当自定义相等时,必须理解这两个方法的使用条件以及意义。
- 当
isEqual
判定两个对象相等时,那么hash
方法也必须返回同样的值; - 而
hash
方法也返回同样的值时,isEqual
未必判定两个对象相等;
- 当
// 一种实现hash的比较高效的方法。
- (NSInteger)hash {
NSInteger firstNameHash = [_firstName hash];
NSInteger lastNameHash = [_lastName hash];
Nsinteger age = _age;
return firstNameHash^ lastNameHash^ age;
}
- 当自己实现判断等同性方法时,当覆写
isEqual
方法时,有一个逻辑的判断:如果当前受测的参数与接收该消息的对象都属于同一个类,则调用自己编写的方法,否则交给超类来判断。
第9条 以“类族模式”隐藏实现细节
类簇可以隐藏抽象基类,是一种很有用的设计模式,OC框架中普遍使用此模式。比如
UIButton
类中有一个+ (UIButton)buttonWithType:(UIButtonType)type
类方法。这个方法可以让你传递一个参数给它,然后它会自动根据你传递的参数类型自动生成对应的Button
。-
这个设计模式的在
iOS
中的实现方法就是先定义抽象的基类。在基类的头文件中定义各种Button
类型。然后使用工厂方法返回用户所选择的类型的实例。然后分别实现各个实例。示例如下:// 首先定义UIButton类型种类 typedef NS_ENUM(NSInteger, UIButtonType) { UIButtonTypeCustom = 0, // no button type UIButtonTypeSystem NS_ENUM_AVAILABLE_IOS(7_0), // standard system button UIButtonTypeDetailDisclosure, UIButtonTypeInfoLight, UIButtonTypeInfoDark, UIButtonTypeContactAdd, UIButtonTypePlain API_AVAILABLE(tvos(11.0)) __IOS_PROHIBITED __WATCHOS_PROHIBITED, // standard system button without the blurred background view UIButtonTypeRoundedRect = UIButtonTypeSystem // Deprecated, use UIButtonTypeSystem instead }; // 再实现具体的类型方法,伪代码如下 @interface UIButton : UIControl <NSCoding> @property(nullable, nonatomic,readonly,strong) UILabel *titleLabel NS_AVAILABLE_IOS(3_0); @property(nullable, nonatomic,readonly,strong) UIImageView *imageView NS_AVAILABLE_IOS(3_0); + (UIButton)buttonWithType:(UIButtonType)type; - (void)setTitle:(nullable NSString *)title forState:(UIControlState)state; @end @implementation UIButton + (UIButton)buttonWithType:(UIButtonType)type { switch(type) { case 0: return [UIButtonCustom new]; break; case 1: return [UIButtonSystem new]; break; case 2: return [UIButtonDetailDisclosure new]; break; ... } } - (void)setTitle:(nullable NSString *)title forState:(UIControlState)state { // 空实现 } @end // 然后再具体实现每个"子类" @interface UIButtonCustom : UIButton @end @implementation - (void)setTitle:(nullable NSString *)title forState:(UIControlState)state { // 实现各自不同的代码 } @end
需要注意的是,这种方法下,因为 OC 语言中没有办法指名一个基类是抽象的,所以基类接口一般没有名为
init
的成员方法,这说明该基类并不应该直接被创建。而UIButton
中实际上拥有这种方法,所以实际上UIButton
也并不完全符合策略模式。当你所创建的对象位于某个类簇中,你就需要开始当心了。因为你可能觉得自己创建了某个类,实际上创建的确实该类的子类。所以不可以使用
isMemberOfClass
这个方法来判断你所创建的这个类是否是该类,因为它实际上可能会返回 NO 。所以明智的做法是使用isKindOfClass
这个方法来判断。COCOA框架中的类簇:
NSArray
和NSMutableArray
,不可变类定义了对所有数组都通用的方法,而可变类定义了值适用于可变数组的方法。两个类共同属于同一个类簇。这意味着两者在实现各自类型的数组时,可以共用实现代码。并且还能把可变数组复制成不可变数组,反之亦然。-
我们经常需要向类簇中新增子类,而当我们无法获取创建这些类的“工厂方法”的源代码,我们就无法向其中新增子类类型。但是其实如果遵守以下几种方法,还是可以向其中添加的。
- 子类应该继承自类簇的抽象基类
- 子类应该定义自己的数据存储方式
- 子类应该覆写超累文档中指名需要覆写的方法
第10条 在既有类中使用关联对象存放自定义数据
有时需要在对象中存放相关信息,这是我们通常会从对象所属的类中继承一个类,然后改用这个子类对象。但是并非所有情况下都可以这样做,有时实例可能是由某种特殊机制所创建,而开发者无法令这种机制创建出自己所写的子类实例。
OC
中有一项强大的特性可以解决这个问题,那就是关联对象。-
可以给一个对象关联许多的其他对象,这些对象之间可以用过
key
来进行区分。存储对象时,可以指明 “存储策略” ,用以维护相应的 “内存管理” 。存储策略类型如下:(加入关联对象成为了属性,那么它就会具备跟存储策略相同的语义)typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) { OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */ OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. * The association is not made atomically. */ OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied. * The association is not made atomically. */ OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object. * The association is made atomically. */ OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied. * The association is made atomically. */ };
-
下列方法可以管理关联对象
// 通过给定的 key 和 value 和 objc_AssociationPolicy policy 为 object 设定 关联对象 值 void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy) // 通过给定的 key 来从 object 中读取 关联对象 的值 id getAssociatedObject(id object, void *key); // 移除指定的 object 的全部 关联对象 void objc_removeAssociatedObjects(id object);
我们可以把某个对象想象成是某个
NSDictionary
把关联到该对象的值理解为字典中的条目。 于是,存取相关联的对象的值就相当于在NSDictionary
上调用setObject: forKey:
和objectForKey:
。然而两者之间有个重要的差别,就是设置关联对象时,使用的key
指针指向的时不限制类型的指针,而NSDictionary
当设置时,就知道该对象的类型了。所以一旦在两个key
上调用isEqual
方法,NSDictionary
可以返回YES,就可以认为两个key
相等。而 关联对象 却不是这样。 所以我们通常会把 关联对象 的key
值设定为 静态全局变量。关联对象的用法举例:可以使用关联对象,给类的分类在Runtime时期动态添加属性,因为
Category
原本是不支持属性的。这种方法可以用在夜间模式时,给UIView
的分类动态添加属性。注意:只有在其他做法不可行时才会选用关联对象,因为这种做法通常会引入难以查找的bug
第11条 理解objc_msgSend的作用
在对象上调用方法是 OC 中经常使用的功能, 用 OC 的术语来说就是 “传递消息” 。消息有“name” 和 “selector” 可以接受参数, 并且有返回值。
C 语言使用 static binding 也就是说,在编译时就已经确定了运行时所调用的函数。于是会直接生成所调用函数的指令,而函数指令实际上是硬编码在指令之中的。只有 C 语言的编写者使用多态时, C 语言才会在某一个函数上使用 dynamic binding
-
而在 OC 中, 如果向某对象传递消息,就会使用 dynamic binding 机制来决定需要调用的方法。在底层,所有方法都是普通的 C 语言函数,然而对象收到消息之后,究竟该调用那个方法完全取决于运行时期。甚至可以再程序运行时改变,这些特性使得 OC 成为一门真正的动态语言。
-
给对象发送消息可以写成
id returnValue = [someObject messageName:parameter];
其中,翻译成容易理解的语言就是id returnValue = [receiver(接收者) selector(选择子):parameter(选择参数)];
。编译器看到这条消息之后,会将其直接转化成一条 C 语言函数调用,这条函数就是消息传递机制中的核心函数objc_msgSend
, 其原型如下:void objc_msgSend(id self, SEL cmd, ...)
。这是一个参数可变的函数。第二个参数SEL
代表选择子,后续参数是消息的参数(也就是选择子的选择参数)。编译器会把刚刚的那条消息转化成如下函数:// 原消息 id returnValue = [someObject messageName:parameter]; /* 转化后的消息 -> 所谓的消息接受者,也就是说是这个消息是作用到谁身上的,比如[self method]; 这条消息啊的接受者就是 self **/ id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
-
objc_msgSend
函数会依据接收者(receiver) 与 选择子(selector)的类型来调用适当的方法。为了完成这个操作:- 该方法需要在接收者所属的类中搜寻其“方法列表”
list of methods
。 - 如果能找到与选择子名称
messageName
相符合的方法的话,就跳至其实现代码。并且会将匹配结果缓存在“快速映射表”fast map
中,每个类都有一个这样的缓存,如果稍后还向该类发送与选择子相同的消息,那么执行起来就会很快,直接在fast map
中找即可。当然,这种方法还是不如“静态绑定”快速,但是只要将选择子selector
缓存起来了,就不会慢很多了。实际上message dispatch
并不是应用程序的瓶颈所在。 - 如果找不到的话,就沿着集成体系一路向上找,等找到合适的方法再跳转。
- 如果最终还是找不到相符的方法,就执行
message forwarding
消息转发 操作。
- 该方法需要在接收者所属的类中搜寻其“方法列表”
-
前面只讲了部分消息的调用过程,其他边界情况则需要交给 OC 环境中的另一些函数来处理
// 当待发消息要返回结构体时,可以交给这个函数来处理。 objc_msgSend_stret // 如果返回是浮点数,这个函数处理 objc_msgSend_fpret // 要给超类发送消息时,这个函数处理 objc_msgSendSuper
之所以当
objc_msgSend
函数根据selector
和recevier
来找到应该调用的方法的 实现代码 后, 会 “跳转” 到这个方法的实现, 是因为 OC 对象的每个方法都可以看做是简单的 C 函数。其原型
如下:<return_type> Class_selector(id self, SEL _cmd, ...)
,其中,每个Class
都有一张表格, 其中的指针都会指向这种函数, 而选择子selector
的则是查表时所用的key
。objc_msgSend
函数正是通过这张表格来寻找应该执行的方法并跳转至它的实现的。需要注意的是
原型
的样子和objc_msgSend
函数很像。这不是巧合,而是为了利用 尾调用优化 技术,使得跳转至指定方法这个操作变得更加简单些。如果某个函数的最后一项操作是调用另一个函数,则就可以运用 尾调用优化 技术。编译器会生成调转至另一函数所需要的指令码,而且不会向调用堆栈中推入新的“栈帧”frame
。 只有当函数的最后一个操作是调用其他函数时,才可以这样做。这项优化对 OC 十分的关键,如果不这样做,这样每次调用 OC 方法之前,都需要为调用objc_msgSend
准备栈帧,我们可以在stack trace
中看到这种frame
。 此外,如果不优化,还会过早的发生“栈溢出”stack overflow
现象。
-
消息有接受者
receiver
,选择子selector
及参数parameter
所构成, 给某对象 “发送消息”invork a message
也就是相当于在该对象上调用方法 call a method
发给某个对象的全部消息都是要由
动态派发系统 dynamic message dispatch system
来处理的,该系统会查看对应的方法,并执行其代码。
第12条 理解消息转发机制
上一条讲了对象的消息传递机制,这一条将讲述当对象无法解读收到的消息时的转发机制。
-
如果想令类能理解某条消息,我们必须实现对应的方法才行。但是如果我们向一个类发送一个我们没有实现的方法,在编译器时并不会报错。因为在运行时可以继续向类中添加方法,所以编译器在编译时还无法通知类中到底有没有某个方法的实现。当对象接收到无法解读的消息后,就会启动 “ 消息转发
message forwarding
” 机制,而我们就应该经由此过程告诉对象应该如何处理未知消息。而当你没有告诉对象应该如何处理未知消息时,对象就会启动 消息转发 机制。最后就会一层层的将消息转发给 NSObject 的默认实现。如下表示:// 这就是 NSObject 对消息转发的默认实现。 // 消息的接收者类型是 __NSCFNumber ,但是他并无法理解名为 lowercaseString 的选择子,就会抛出异常 /* 出现这种情况并不奇怪。因为 __NSCFNumber 实际上是 NSNumber 为了实现 “无缝桥接” 而使用的 内部类 配置 NSNumber 对象时也会一并创建此对象。 在本例中,消息转发过程以程序崩溃结束。但是实际上,我们在编写自己的类时,可以在转发过程中设置挂钩,就可以当程序执行 消息转发 时,处理所转发的消息,避免程序的崩溃。 **/ 2017-12-01 11:30:19.942493+0800 NEUer[17853:2011205] -[__NSCFNumber lowercaseString:]: unrecognized selector sent to instance 0x87 2017-12-01 11:30:19.964307+0800 NEUer[17853:2011205] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber lowercaseString:]: unrecognized selector sent to instance 0x87'
-
消息转发的过程 => 分为两大阶段
- 第一大阶段
动态方法解析
: 首先,询问接收者 receiver
所属的类, 能否动态添加方法来处理这个未知选择子 unknown selector
, 这个过程叫做 动态方法解析。- 对象当接收无法解读的消息时,首先调用
+ (BOOL)resolveInstanceMethod:(SEL)selector
这个方法的参数就是 未知选择子。返回值为 BOOL 表示能否在 Runtime 新增一个实例方法来处理这个选择子。使用这个方法的前提是:这个 未知选择子 的相关实现代码已经写好了,只等着运行的时候在 Runtime 时期动态插入类中即可。
- 对象当接收无法解读的消息时,首先调用
- 第二大阶段
完整的消息转发机制 full forwarding mechanism
: 当 接收者 无法解析这个 未知选择子 时, 询问 接收者 是否拥有备援的接收者 replacement receiver
,又分为两小阶段- 第一小阶段:如果有,则 接收者 就把消息转发给它,消息转发结束。
- 这一小阶段的过程如下:当前 接收者 还有一次机会来处理 未知选择子。那就是使用
-(id)forwardingTargetForSelector:(SEL)selctor;
这个方法的参数表示 未知选择子。 如果当前接收者 能够找到 备援对象 则可以将 备援对象 返回,如果找不到, 就返回 nil 。 通过这种方案,我们可以使用 __“组合” __ 来模拟 多重继承 的某些特性。在一个对象的内部, 可能还有一系列其他的对象,而该 对象 可以经过这个方法使得它的内部的某个可以处理这个消息的对象返回。在外界看来,就好像这个对象自己处理了这个未知方法一样。 - 需要注意的是:在这个阶段 接收者 没有权利去操作这一步所转发的消息,他只能全盘交给 备援的接收者 来处理这个消息。
- 这一小阶段的过程如下:当前 接收者 还有一次机会来处理 未知选择子。那就是使用
- 第二小阶段:如果没有 备援的接收者, 则 启动 完整的消息转发机制 。 Runtime 系统会把与消息有关的全部细节都封装到 NSInvocation 对象中, 再给接收者最后的一次机会,让他设法解决当前还未处理的这个消息。其中,这个 NSInvocation 对象包含 选择子, 目标, 参数。 在触发 NSInvocation 对象时, “消息转发系统” 将亲自出吗,把消息转发给目标对象(也就是目标接收者)。
- (void)forwardInvocation:(NSInvocation *)invocation;
。- 当这个方法简单的实现:例如只是改变接收者目标,那么它的效果就会跟使用 备援的接收者 效果一样。
- 这个方法的比较有意义的实现方式为:在触发消息之前,先在 invocation 中改变消息的内容,不如追加另外一个参数,或切换选择子。
- 当在实现这个方法的时候,如果发小某个调用不应该由本类处理,则需要调用超类的同名方法。这样的话,继承体系中每个类都有机会处理此调用请求,直到到 NSObject 类。 如果最后调用了 NSObject 类的方法,该方法会接着调用
doesNotRecognizeSelector
来抛出异常。如果抛出了这个异常,就表明在整个消息转发的大过程中,没有人能处理这个消息!就会使程序崩溃。
- 第一小阶段:如果有,则 接收者 就把消息转发给它,消息转发结束。
- 第一大阶段
接收者 在每个步骤均有机会处理消息,步骤越往后,处理这个消息的代价就越大。最好能在第一步就完成,这样 Runtime 系统就将这个方法缓存起来了。回顾 第11条 说道:"当 OC 中某个对象调用某个函数实际上就是给该对象传递消息,这是一个使用 动态绑定 的过程。在这个过程中使用
objc_msgSend
这个函数,该函数会依据接收者(receiver) 与 选择子(selector)的类型来调用适当的方法。为了完成这个操作:它需要首先在这个类的list of method
中找相应的方法,然后如果找到了这个方法,继而找到它的实现,然后再把这个方法放到fast map
中。" 这样就实现了 Runtime 时期的缓存。在此之后,如果这个类再次收到了这个选择子,那么根本无需启动消息转发机制了。
第13条 用“方法调配技术”调试“黑盒方法”
我们都知道我们可以在 Runtime 时期,动态选择要调用的方法。实际上我们也可以在 Runtime 时期,动态的把给定选择子名称 (SEL) 的方法进行改变。这个功能使我们可以在不使用继承就可以直接改变这个类本身的功能。这样一来,新功能就可以在这个类中的所有实例都得到应用。这个功能就叫做
方法调配 method swizzling
。类的方法列表会把选择子名称映射到相关方法的实现上。使得“动态消息派发系统”可以据此找到应该调用的方法。这种方法以函数指针的形式来表示。这种指针就叫做
IMP
原型如下id (*IMP)(id, SEL)
-
原始方法表的布局yuanshifangfabiao.png
-
当使用
method swizzling
改变内存中选择子与方法实现的映射后,就变成了这样newfangfabiao.png
此时,对于这个类的所有实例,这两个方法的实现都改变了。
// 交换方法实现的方法。 void method_exchangeImplementation(Method 1, Method 2); // 获取方法的实现。 Method class_getInstanceMethod(Class aClass, SEL aSelector);
-
在实际应用中,这样交换两个方法没什么实际用途。
method swizzling
主要的作用在于:可以在不知道原本方法的内部具体实现的情况下,为原本的方法添加新的附加功能。示例如下:-
新方法可以添加至一个 NSString 的一个 Category 中:
@interface NSString (SLYMyAdditions) - (NSString *)sly_myLowerCaseString; @end @implementation NSString (SLYMyAdditions) - (NSString *)sly_myLowerCaseString { /* 在这里调用了 sly_myLowerCaseString 这个方法, 一眼看上去好像是一个递归的循环调用,使这个方法永远都不会结束,但是实际上,这个方法在 Runtime 时期就已经绑定到 NSString 本身的 lowercaseString 方法上去了。所以这个分类的具体目的就是在实现原本 lowercaseString 功能的同时,打印一些额外信息。在我们的实际开发中,这也正是 method swizzling 的主要用途。 **/ NSString *lowercase = [self sly_myLowerCaseString]; NSLog(@"%@ --> %@", self, lowercase); return lowercase; } @end
-
具体的交换方法代码如下:(一般来说,
method swizzling
应该在 load 方法中执行具体的交换)// 具体交换两个方法实现的范例: Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString)); Method swappedMethod = class_getInstanceMethod([NSString class], @selector(sly_myLowerCaseString)); method_exchangeImplementation(originalMethod, swappedMethod); // 从现在起,这两个方法的实现与其方法名就互换了。
-
需要注意的是,这个功能虽然强大,但是不能滥用。一般来说都是在开发调试程序时才需要在 Runtime 时期修改方法实现。
第14条:理解“类对象”的用意
首先来理解 OC 对象的本质:所有 OC 对象的实例都是指向某块内存数据的指针。但是对于通用的对象类型 id 由于其本身已经是指针了,所以我们可以不加 * 。
-
描述 OC 对象所用的数据结构定义在 Runtime 的头文件里, id 的定义如下:
/* 每个对象结构体的首个成员是 Class 类的变量,该变量定义了对象所属的类,通常称为 is a 指针。 **/ typedef struct objc_object { Class isa; } *id;
-
Class 类的实现如下:
typedef struct objc_class *Class; struct objc_class { Class isa; // 每个 Class 对象中也定义了一个 is a 指针,这说明 Class 本身也是一个 OC 对象,这个 isa 指针指向的是类对象所属的类型,是另外一个类,叫做 metaclass, 用来表述类对象所需要具备的元数据。“类方法”就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。 Class super_class; // 指向 Class 的超类 const char *name; // 该类对象的名称 long version; long info; long instance_size; struct objc_ivar_list *ivars; // 该类对象的变量列表 struct objc_method_list **methodLists; struct objc_cache *cache; struct objc_protpcol_list *protocols; }
假设有个名为SomeClass的子类从NSObject中继承而来,则其继承体系如图
第12条则讲述了消息转发的原理:如果类无法立即响应某个选择子,那么就会启动消息转发流程。然而,消息的接收者究竟是何物?是对象本身吗?运行期系统如何知道某个对象的类型呢?对象类型并非在编译期就绑定好了,而是要在运行期查找。而且,还有个特殊的类型叫做id,它能指代任意的Objective-C对象类型。一般情况下,应该指明消息接收者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而类型为id的对象则不然,编译器假定它能响应所有消息。
编译器无法确定某类型对象到底能解读多少种选择子,因为运行期还可向其中动态新增。然而,即便使用了动态新增技术,编译器也觉得应该能在某个头文件中找到方法原型的定义,据此可了解完整的“方法签名”(method signature),并生成派发消息所需的正确代码。“在运行期检视对象类型”这一操作也叫做“类型信息查询”(introspection,“内省”),这个强大而有用的特性内置于Foundation框架的NSObject协议里,凡是由公共根类(common root class,即NSObject与NSProxy)继承而来的对象都要遵从此协议。在程序中不要直接比较对象所属的类,明智的做法是调用“类型信息查询方法”。
-
isMemberOfClass:
能够判断出对象是否为某个特定类的实例,而isKindOfClass:
则能够判断出对象是否为某类或其派生类的实例.// 例如: NSMutableDictionary *dict = [NSMutableDictionary new]; [dict isMemberOfClass:[NSDictionary class]]; ///< NO [dict isMemberOfClass:[NSMutableDictionary class]]; ///< YES [dict isKindOfClass:[NSDictionary class]]; ///< YES [dict isKindOfClass:[NSArray class]]; ///< NO // 像这样的类型信息查询方法使用isa指针获取对象所属的类,然后通过super_class指针在继承体系中游走。由于对象是动态的,所以此特性显得极为重要。
-
不可以直接使用两个对象是否相等来比较
// 例如: id object = /* ... */; if ([object class] == [SLYSomeClass class]) { // 'object' is an instance of EOCSomeClass }
因为消息可能执行了消息转发机制,所以不可以这样对对象的类进行比较。比方说,某个对象可能会把其收到的所有选择子都转发给另外一个对象。这样的对象叫做“代理”(proxy),此种对象均以NSProxy为根类。而如果使用了
isKindOfClass:
这个方法进行比较,则可以比较,因为isKindOfClass:
这样的类型信息查询方法,那么代理对象就会把这条消息转给“接受代理的对象”(proxied object)。也就是说,这条消息的返回值与直接在接受代理的对象上面查询其类型所得的结果相同。也就可以得到正确的结果。