【Effective Objective - C】—— 对象,消息,运行期
- 理解“属性”这一概念
-
- 定义变量
- @dynamic关键字
- 属性特质
-
- 原子性
- 读/写权限
- 内存管理语义
- 方法名
- 要点
- 在对象内部尽量直接访问实例变量
-
- 直接访问与属性访问的区别:
- 惰性初始化
- 要点
- 理解“对象等同性”这一概念
-
- “==”和“isEqual:”区别
- 概念
- 容器中可变类的等同性
- 要点
- 以“类族模式”隐藏实现细节
-
- 类方法
- 创建类族
- Cocoa里的类族
- 判断某个对象是否位于类族:
- 向已有类新增子类
- 要点
- 在既有类中使用关联对象存放自定义数据
-
- 关联对象的出现
- 关联对象
- 要点
- 理解objc_msgSend的作用
-
- 消息转发过程
- 边界情况
- 要求
- 理解消息转发机制
-
- 无法解读消息
-
- 1.消息转发阶段
- 2. 动态方法解析
- 3. 备援接收者
- 4.完整的消息转发
- 要点
- 用“方法调配技术”调试“黑盒方法”
-
- 方法调配
- 动态消息派发系统和IMP
- 如何交换方法实现
- 要点
- “类对象”的用意
-
- id类型:
- Class对象:
- 在类继承体系中查询类型信息:
- 要点:
理解“属性”这一概念
- “属性”是OC的一项特性,用于封装对象中的数据。OC对象通常会把其需要的数据保存为各种实例变量。实例变量通过“存取方法”来访问。其中,“获取方法”用于读取变量值,而“设置方法”用于写入变量值。切此特性引入了一种新的“点语法”,使开发者可以更为容易地依照类对象来访问存放于其中的数据。
定义变量
对于一个类,可以这样定义属性:
@interface EOCPerson : NSObject { @public NSString *_name; NSString *_name1; @private NSString *_someInternalData; } @end
我们对其第一个位置在添加一个实例变量:
@interface EOCPerson: NSObject { @public NSString *_dateOfBirth; NSString *_firstName; NSString *_lastName; @private NSString *_someInternalData; } @end
我们新添加的实例变量就会代替原第一个位置实例变量的偏移量。
这样的话,如果代码使用了编译器计算出来的偏移量,那么在修改类定义之后必须重新编译,否则就会出错。例如:某个代码库中的代码使用了一份旧的类定义。如果和其相链接的代码使用了新的类定义,那么运行时就会出现不兼容现象。对此类问题,OC的解决方法是,把实例变量当做一种存储偏移量所用的“特殊变量”,交由“类对象”保管。偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也就变了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。这就是稳固的“应用程序二进制接口”
这个问题还有一种解决方法,就是尽量不要直接访问实例变量,而应该通过存取方法来做。这时@property语法就派上用场了。这种规范的命名方法OC会自动创建出存取方法。
简单来说,以下两部分代码的效果是相同的:
@interface EOCPerson: NSObject @property NSString *firstName; @property NSString *lastName; @end
@interface EOCPerson: NSObject (NSString *)firstName; - (void)setFirstName: (NSString *)firstName; (NSString *)lastName; - (void)setLastName: (NSString *)lastName; @end
其中,编译器自动编写访问这些属性所需的方法的过程叫做:“自动合成”。这个过程由编译器在编译期执行,所有编译器例看不到这些“合成方法”的源代码。另外,编译器还会自动向类中添加适当类型的实例变量,并且在属性名前加下划线,以此作为实例变量的名字。还有,也可以在类的实现代码里通过@synthesize语法来指定实例变量的名字。
@dynamic关键字
如果我们不想令编译器自动合成存取方法,那我们应该怎么做呢?那就是使用@dynamic关键字了,它会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。然后我们就需要在运行期动态创建存取方法了。
如下代码所示:
@interface EOCPerson : NSManageObject @property NSString *firstName; @end @implementation @dynamic firstName; @end
这样编译器就不会自动合成存取方法。
属性特质
使用属性时还有一个问题要注意,就是各种特质设定也会影响编译器所生成的存取方法。比如下面这个属性就指定了三项特性 :
原子性
默认情况下,有编译器所合成的方法会通过锁定机制确保其原子性。如果属性具备nonatomic特质,则不使用同步锁。如果不具备,那它就是“原子的”,但是仍然可以在属性特质中写明这一点,编译器不会报错。如果是自己定义存取的方法,那么就应该遵从与属性特质相符的原子性。
- atomic:默认值,原子性访问,==单线程访问,表示如果有多个线程同时调用setter的话,不会出现某一个线程执行setter全部语句之前,另一个线程开始执行setter情况,安全性高于nonatomic,性能低于nonatomic,==但atomic 并不代表线程安全,只是说对同一对象的set和get的操作是顺序执行的。值得我们注意的一点是atomic是默认值,假如属性不具备nonatomic特质,那么它就默认具备atomic。
- nonatomic:非原子性访问,可以多线程并发访问,oc中大多数都申明nonatomic属性。
读/写权限
- 具备readwrite(读写)特质的属性拥有“获取方法”与“设置方法”。若该属性由@synthesize实现,则编译器会自动生成这两个方法。
- 具备readonly(只读)特质的属性仅拥有获取方法,只有当该属性由@synthesize实现时,编译器才会为其合成获取方法。你可以用此特质把某个属性对外公开为只读属性,然后在 分类 中将其重新定义为读写属性。
内存管理语义
属性用于封装数据,而数据则要有“具体的所有权语义”,如果自己编写存取方法,就必须同有关属性所具备的特质相符。
- assign:默认值,直接赋值, 主要是对基本数据类型使用:NSInteger,CGFloat 和C语言的 int double float char …
- retain:先release旧的对象,新对象的计数加1,并返回地址给引用者,主要对NSObject与其子类中使用。
- strong:强引用,ARC模式下与retain同作用。此特质表明该属性定义了一种**“拥有关系”**。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。
- weak:弱引用,ARC模式下与assign同作用。此特质表明该属性定义了一种**“非拥有关系”**。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。然后在属性所指对象遭到摧毁时,属性值也会清空(nil out)。
- unsafe_unretained:此特质的语义和assign相同,但是它适用于“对象类型”(object type),该特质表达一种**“非拥有关系”**(“不保留”,unretained),当目标对象遭到摧毁时,属性值不会自动清空(“不安全”,unsafe),这一点与weak不同。
- copy:创建一个新对象,将旧对象的值赋值给新对象,release旧对象。 copy与retain的区别为:retain是指针拷贝,copy是内容拷贝,其主要对字符串NSString使用。
方法名
可通过如下特质来指定存取方法的方法名:
getter=指定“获取方法”的方法名。如果某属性时BOOL型,而你想为其获取方法加上“is”前缀,那么就可以用这个办法来指定。
要点
(1)可以用@property语法来定义对象中所封装的数据。
(2)通过“特质”来指定存储数据所需的正确语义。
(3)在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
(4)开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能。
在对象内部尽量直接访问实例变量
笔者强烈建议大家在读取实例变量的时候,采用直接访问的形式,而在设置实例变量的时候通过属性来做。
直接访问与属性访问的区别:
- 由于不经过Objective-C的〝方法派发” (methoddispatch,参见第11条)步骤,所以 直接访问实例变量的速度当然比较快。 在这种情况 下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
- 直接访问实例变量时,不会调用其〝设置方法”,这就绕过了为相关属性所定义的 “内 存 管 理 语 义 ” 。比方说 ,如果在ARC下直接访 问一个声明为copy的属性 ,那么并不会拷贝该属性,只会保留新值并释放旧值。
- 如果直接访问实例变量,那么不会触发“键值观测"(Key-ValueObserving, Kvo)。通 知。这样做是否会产生问题,还取决于具体的对象行为。
- 通过属性来访问有助于排查与之相关的错误,因为可以给 “获取方法〞和/ 或 “设置 方法〞中新增 “断点”(breakpoint),监控该属性的调用者及其访问时机。
对于上述的理解:给实例变量赋值的时候通过点语法来操作,读取实例成员的内容时以下划线的方式直接访问。
惰性初始化
也叫做“延迟初始化”。在惰性初始化的情况下,必须通过“获取方法”来访问属性,否则,实例变量就永远不会初始化。一般用于:一个属性不常用,而且创建该属性的成本较高的情况。
- (EOCBrain *) brain { if (!_brain) { _brain = [Brain new]; } return _brain; }
若没有调用“获取方法” 就直接访问实例变量,则会看到尚末设置好的brain,所以说, 如果使用了“情性初始化” 技术,那么必领通过存取方法来访问brain属性。
要点
- 在对象内部读取数据时,应该直接通过实例变量来读,而写人数据时,则应通过属性 来写。
- 在初始化方法及dealloe 方法中,总是应该直接通过实例变量来读写数据。
- 有时会使用惰性初始化技术配置某份数据,这种情况下,需要通过属性来读取数据。
理解“对象等同性”这一概念
“==”和“isEqual:”区别
== 是看地址来进行判断,地址不一致即返回false
isEqual:是专门用于判断的方法,不一定是看地址,也可以是其他的标准。
在NSObject类中, ==与isEqual:没有明显区别,但在NSString中,已经完成了重写,只要字符串字符序列相同,isEqual:方法就返回true。
概念
等同性来比较对象是一个非常有用的功能。不过,按照==操作比较出来的结果未必是我们想要的,因为该操作比较的是两个指针本身,而不是其所指的对象。应该使用NSObject协议中声明的“isEqual”:方法来判断两个对象的等同性。一般来说,两个类型不同的对象总是不相等的。如果知道两个受测对象属于同一个类,那么就可以使用这种方法。
以下面代码为例:
NSString *foo = @"Badger 123"; NSString *bar = [NSString stringWithFormat:@"Badger %i", 123]; BOOL equalA = (foo == bar);//NO BOOL equalB = [foo isEqual:bar];//YES BoOL equalC = [foo isEqualToString:bar];//YES
可以看到== 与等同性判断方法之间的差别。NSString类实现了一个自己独有的等 同性判断方法,名叫 “isEqualToString: ” 。传递给该方法的对象必领是NSString,否则结果 末定义(u deined)。调用该方法比调用“ isEqual:〞方法快,后者还要执行额外的步骤,因为它不知道受测对象的类型。
NSObject 协议中有两个用于判断等同性的关键方法:
- (BOOL) isEqual: (id) object; - (NSUInteger) hash;
NSObject 类对这两个方法的默认实现是: 当且仅当其“ 指针值” (pointervalue)日完全相等时,这两个对象才相等。若想在自定义的对象中正确後写这些方法,就必领先理解其约定 (contract)。如果“ isEqual:〞方法判定两个对象相等,那么其hash 方法也必须返回同 一个 值。但是,如果两个对象的hash 方法返回同一个值,那么“isEqual:〞方法末必会认为两者相等 。
容器中可变类的等同性
当把某个对象放入collection后,就不应该再改变其哈希码了。因为collection会把各个对象按照其哈希码分装到不同的“箱子数组”中。如果某对象在放入“箱子”之后,哈希码又发生变化,那么其所处的这个箱子对他来说就是“错误”的。
要点
- 若想检测对象的等同性,请提供“isEqual:”与hash方法。
- 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
- 不要盲目地逐个检测每条属性,而是应该依照具体需求来指定检测方案。
- 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。
以“类族模式”隐藏实现细节
类方法
“类族”是一种很有用的模式,可以隐藏“抽象基类”背后的实现细节。Objective-C的系统框架中普遍使用此模式。比如,ios 的用户 界面框架(user interface framework)UIKit中就有一个名为UIButton的类。想创建按钮,需要调用下面这个“ 类方法” ( class method )。
+ (UIButton*)buttonWithType:(UIButtonType)type;
该方法所返回的对象,其类型取决于传人的按钮类型(button type)。然而,不管返回什 么类型的对象,它们都继承自同 一个基类:UIButton。这么做的意义在于:UIButton 类的使 用者无领关心创建出来的按钮具体属于哪个 子类,也不用考虑按钮的绘制方式等实现细节。 使用者只需明白如何创建按钮,如何设置像“标题” (title)这样的属性,如何增加触摸动作 的目标对象等问题就好。
创建类族
首先要定义抽象基类,也就是一个新的类,其中可以包括你的类型选取,使用枚举器和switch语句来完成,并且还的定义你的类的相关方法,再创建一个新的类,继承你之前的类,并且完成之前的定义方法,使用覆盖的原理,完成这些方法。这种“工厂模式”是创建类族的办法之一。
如果你想创建的类中没有init初始化的方法,那么这就是在暗示你该类的实例也许不应该由用户直接创建。总而言之,以后创建对象一定不要被其的表象迷惑住了,你可能觉得自己创建了某个类的实例,然而实际上创建的却是其子类的实例。
定义该类:
//定义员工类型 typedef NS_ENUM(NSUInteger, EOCEmployeeType) { EOCEmployeeTypeDeveloper, EOCEmployeeTypeDesiner, EOCEmployeeTypeFinance }; @interface EOCEmployee : NSObject //定义属性 @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSUInteger salary; //定义方法 + (EOCEmployee *)employeeWithType:(EOCEmployeeType)type; - (void)doADaysWork; @end
实现这个类:
@implementation EOCEmployee + (EOCEmployee *)employeeWithType:(EOCEmployeeType)type { switch (type) { case EOCEmployeeTypeDeveloper: return [EOCEmployeeTypeDeveloper new]; break; case EOCEmployeeTypeDesiner: return [EOCEmployeeTypeDesiner new]; break; case EOCEmployeeTypeFinance: return [EOCEmployeeTypeFinance new]; break; } } - (void)doADaysWork { // Subclasses implement this. } @end
每个“实体子类”(concrete subclass)都是基类继承而来:
@interface EOCEmployeeTypeDeveloper : EOCEmployee @end @@implementation EOCEmployeeTypeDeveloper - (void)doADaysWork { [self writeCode]; } @end
Cocoa里的类族
系统框架中有许多类族,就用我们经常使用的NSArray和NSMutableArray来说,这样来看,它是两个抽象基类,但是他们两个拥有相同的方法,这个方法可能就是他们共同类族中的方法,而可变数组的特殊方法就是只适用于可变数组的方法,其他的共同方法可能就是类族中的方法。
在使用NSArray的alloc方法来获取实例时,该方法首先会分配一个属于某个类的实例,此实例充当一个“占位数组”,也就是说,你把这个位置是先分配给其类族的,后来其类族才将这个位置分配给你创建的具体数据类型的。所以像这些类的背后其实是一个类族,在对一些if条件进行判断的时候一定要注意,例如:
id maybeAnArray = /* ... */; if ([maybeAnArray class] == [NSArray class]) { //Will never be hit }
判断某个对象是否位于类族:
id maybeAnArray = /*...*/ if(maybeAnArray isKindOfClass:[NSArray class]) { //will be hit }
向已有类新增子类
你若是想向NSArray这种已有类新增子类,那就得遵循以下规则:
- 子类应该继承自类族中的抽象基类。
- 子类应该定义自己的数据存储方式。
- 子类应当覆写超类文档中指明需要覆写的方法。
要点
- 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
- 系统框架中经常使用类族。
- 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。
在既有类中使用关联对象存放自定义数据
关联对象的出现
- 在iOS开发里,分类是不能添加成员变量的,只允许给分类添加属性,所以出现了关联对象
- 一个类添加属性@property,实际上是做了3个事情:
- 添加成员变量
- 添加setter,getter方法声明
- 添加setter,getter方法实现
- 给一个分类添加属性
- 只添加了setter,getter方法的声明
- 所以在OC里需要使用关联对象给分类添加成员变量
关联对象
可以通过关联对象为分类添加成员变量
//添加关联对象 void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy); //获取关联对象 id objc_getAssociatedObject(id object, const void *key); //移除所有关联对象 void objc_removeAssociatedObjects(id object);
对象/属性/值/关联策略
对象/属性/值/关联策略 objc_setAssociatedObject(self,“str”,urlString,OBJC_ASSOCIATION_COPY); objc_getAssociatedObject(self,“str”);
OBJC_ASSOCIATION_ASSIGN//等价于 @property(assign)。 OBJC_ASSOCIATION_RETAIN_NONATOMIC//等价于 @property(strong, nonatomic)。 OBJC_ASSOCIATION_COPY_NONATOMIC//等价于@property(copy, nonatomic)。 OBJC_ASSOCIATION_RETAIN//等价于@property(strong,atomic)。 OBJC_ASSOCIATION_COPY//等价于@property(copy, atomic)。
值得注意的是关联对象虽然好用,但是我们也是在没有办法继承类的情况下为了方便才使用的,本质上面关联对象的简便已经经过了层层筛选不得才使用关联对象。
要点
- 可以通过“关联对象”机制来把两个对象连起来。
- 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”。
- 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug。
理解objc_msgSend的作用
在对象上调用方法是OC中经常使用的功能。用OC的术语来说,这叫做“传递消息”。消息有“名称”或“选择子”,可以接受参数,而且可能还有返回值。我们之前用C语言写出来的函数就是“静态绑定”的函数,就是说,他在编译期就能决定运行时所调用的函数。但是若是我们使用一个函数指针来实现函数调用的话,这时他就成为一个“动态绑定”了,因为所调用的函数直到运行期才能确定。
在OC中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有的方法都是普通的C语言函数,然而对象收到消息之后究竟该调用那个方法则完全取决于运行期决定,甚至可以在程序运行时改变,这些特性使得OC成为一门真正的动态语言。
消息转发过程
来看下面一个对象发送消息:
id returnValue = [someObject messageName:parameter];
在这个例子中
someObject叫做“接收者”(receiver),messageName叫做 “选择子”(selector)。 选择子与参数合起来称为 “消息” (message)。编译器看到此消息后,将其转换为一条标准的 C语言两数调用,所调用的函数乃是消息传递机制中的核心两数,叫做objc_msgSend,其 “ 原型” ( prototype )如下:
void objc_msgsend(id self, SEL cmd, ...)
这是个“ 参数个数可变的两数” ( variadic function)。能接受两个或两个以上的参数。第一 个参数代表接收者,第二个参数代表选择子(SEL 是选择子的类型),后续参数就是消息中的 那些参数,其顺序不变。选择子指的就是方法的名字。“选择子〞与“方法” 这两个词经常 交替使用。编译器会把刚才那个例子中的消息转换为如下函数:
id returnValue = objc msgSend(someobject, @selector (messageName:), parameter);
objc_msgSend 两数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作, 该方法需要在接收者所属的类中搜寻其“方法列表” (list of methods),如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找 到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行 “消息转发”(mesage forwarding)操作。
边界情况
前面讲的这部分内容只描述了部分消息的调用过程,其他“边界情况” (edge case )则 需要交由Objective-C 运行环境中的另 一些函数来处理:
- objc_msgSend_stret。如果待发送的消息要返回结构体,那么可交由此两数处理。只有 当 C P U 的 寄 存 器 能 够 容 纳 得 下消 息 返 回 类 型 时 , 这 个两 数 才 能 处 理 此 消 息 。 若 是 返 回 值 无 法 容 纳 于 C P U 奇 存 器 中 ( 比 如 说 返 回 的 结 构 体 太 大 了 ), 那 么 就 由 另 一 个 函 数 执 行派发。此时,那个两数会通过分配在栈上的某个变量来处理消息所返回的结构体。
- objc _msgSend_fpret。如果消息返回的是浮点数,那么可交由此晒数处理。在某些架 构 的 C P U 中 调 用 函 数 时 , 需 要 对 " 浮 点 数 寄 存 器 " (floating - point register)做 特 殊 处 理 , 也就是说,通常所用的objc_msgSend 在这种情况下并不合适。这个两数是为了处理 ×86 等架构CPU 中某些令人稍觉惊讶的奇怪状况。
- objc_msgSendSuper。如果要给超类发消息,例如[super message:parameter],那么就 交由此两数处理。也有另外两个与obje_msgSend_stret 和objc_msgSend_foret 等效的 函数,用于处理发给super 的相应消息。
要求
- 消息由接收者、选择子及参数构成。给某对象“发送消息”也就相当于在该对象上“调用方法”。
- 发给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码。
理解消息转发机制
无法解读消息
在上一条中提到了消息转发,这一条主要讲解消息转发的过程:
当对象接收到无法解读的消息后,就会启动“消息转发”机制,程序员可经由此过程告诉对象应该如何处理未知消息。
上面这段异常信息是由NSObject 的“doesNotRecognizeSelector:” 方法所抛出的,**此异常表明 : 消息接收者的类型是__NSCFNumber, 而该接收者无法理解名为lowercaseString的选择子。 **本例所列举的这种情况并不奇怪,因为NSNumber 类里本来就没有名为 lowercaseString 的方法。控制台中看到的那个__NSFCNumber 是 为了实现 “ 无缝桥接” (toll- free bridging,第49 条将会详解此技术)而使用的内部类(intermal class) ,配置NSNumber 对象时 也会一 并创建此对象。在本例中, 消息转发过程以应用程序崩溃而告终。
1.消息转发阶段
消息转发分为两大阶段,第一阶段先征询接收者,所属的类,看其是否能动态添加方法,处理当前这个“未知的选择子”,这叫做“动态方法解析”。第二阶段涉及“完整的消息转发机制”。
2. 动态方法解析
对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:
+ (BOOL)resolveInstanceMethod:(SEL)selector
==该方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。==在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法,假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另一个方法,该方法与“resolveInstanceMethod:”类似,叫做“resolveClassMethod”。
使用这种办法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面 就可以了。 此方案常用来实现@dynamic属性(参见第6条), 比如说,要访问CoreData框架中NSManagedObjects对象的属性时就可以这么做,因为实现这些属性所需的存取方法 在编译期就能确定。
3. 备援接收者
==当接收者还有第二次机会能处理未知的选择子,在这一步中运行期系统会问它:能不能把这条消息转给其他接收者来处理。==与该步骤对应的处理方法如下:
- (id)forwardingTargetForSelector:(SEL)selector;
方法参数代表未知的选择子,若当前接收者能找到各授对象,则将其返回,若找不到, 就 返 回 nil。 通过此方案,我们可以用“组合”(composition) 来模拟出“多重继承”( multipleinheritance )的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法 将能够处理某选择 子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这些消息似的。
请注意,我们无法操作经由这 一步所转发的消息。若是想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了。
4.完整的消息转发
如果转发算法已经来到这一步的话,那么唯一能做的就是启用完整的消息转发机制 了。 首先创建NSInvocation 对象,把与尚未处理的那条消息有关的全部细节都封于其中。 此对象包含选择子、目标(target )及参数。在触发NSInvocation 对象时,“ 消息派发系统”
( message-dispatchsystem )将亲自出马,把消息指派给目标对象。
此步骤会调用 下列方法来转发消息:
- (void)forwardInvocation:(NSInvocation*)invocation
这个方法可以 实现得很简单: 只需改变调用目标,使消息在新目标 上得以调用即可。然 而这样实现出来的方法与“备援接收者〞方案所实现的方法等效, 所以很少有人采用这么简 单的实现方式。比较有用的实现方式为: 在触发消息前, 先以某种 方式改变消息内容, 比如 追加另外一个参数,或是改换选择子,等等。
实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样的话, 继承体系中的每个类都有机会处理此调用请求, 直至NSObject。 如果最后调用了NSObject 类的方法,那么该方法还会继而调用“doesNotRecognizeSelector:” 以拋出异常, 此异常表明选择 子最终未能得到处理。
消息转发全过程:
要点
- 若对象无法响应某个选择子,则进人消息转发流程。
- 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加人类中。
- 对象可以把其无法解读的某些选择子转交给其他对象来处理。
- 经过上述两步之后,如果还是没办法处理选择 子,那就启动完整的消息转发机制。
用“方法调配技术”调试“黑盒方法”
黑盒机制,不少人都有理解,就是一个方法你知道怎么用,如何用,并且常常都能使用它但是对于该方法的内部机制却不知道是什么样子,这一条就介绍了如何利用现有的方法去调试黑盒方法变成我们所能用的。
方法调配
对于OC对象收到消息之后使用何种方法在运行期进行解析,与给定的选择子名称相对应的方法是可以在运行期改变的! 这是OC语言强大的特性,我们就可以不知道源代码并且不需要通过继承子类复写方法来改变某个类本身的功能,这样一来新功能能够在本类实例化的所有实例里面生效,而不是仅限于覆写了相关方法的那些子类的实例。叫做方法调配。
动态消息派发系统和IMP
类的方法列表会把选择子的名称映射到相关的方法实现上面,使得动态消息派发系统能够根据此找到应该调用的方法,这些方法均以函数指针的形式表示,这种指针叫做IMP,原型如下:
id (*IMP) (id, SEL, ...)
例如NSString类可以响应自己所带的选择子,它们的关系类似于key 和 Value .
OC运行期提供的几个方法都能操作这张表,开发者可以向其中新增选择子,也可以改变某选择子所对应的方法实现,还可以交选择子所映射到的指针,我们可以经过操作改变类的方法表。
也就是说我们无需修改子类覆写方法,只需要修改方法表的布局,就会反映到程序所有NSString实例上。
如何交换方法实现
我们添加新功能的本质就是修改之前的方法实现,也就是重写一个方法实现然后实现交换。
方法交换实例
要点
- 在运行期,可以向类中新增或替换选择子所对应的方法实现。
- 使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能。
- 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。
“类对象”的用意
id类型:
一般情况下,应该指明消息接收者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而类型为id的对象则不然,编译器假定它能响应所有信息。
每个OC对象实例都是指向某块内存数据的指针,所以在声明变量时,类型后面要跟一个“”字符。
描述Objective-C对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也在定义在这里:
typedef struct objc_object { Class isa; } *id;
Class对象:
typedef struct objc_class *Class; struct objc_class { Class isa; Class super_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_protocol_list *protocols; };
此结构体存放类的“元数据”。其中的super_class,它定义了本类的超类。类对象所属的类型(也就是isa指针所指向的类型)是另一个类,叫做“元类”(metaclass)。并且每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。
在类继承体系中查询类型信息:
“isMemberOfClass:”能够判断出对象是否为某个特定类的实例(只有与其出创建的类型相同时才返回YES),而“isKindOfClass:”则能够判断出对象是否为某类或其派生类的实例。
要点:
- 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
- 如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知。
- 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。