相关推荐recommended
【学习iOS高质量开发】——对象、消息、运行期
作者:mmseoamin日期:2024-01-23

文章目录

  • 一、理解“属性”这一概念
    • 1.如何定义实例变量
    • 2.什么是不兼容现象,如何解决
    • 3.理解@property关键字
    • 4.理解@dynamic关键字
    • 5.属性特质
      • 1.原子性:
      • 2.读/写权限:
      • 3.内存管理语义
      • 7.要点
      • 二、在对象内部尽量直接访问实例变量
        • 1.直接访问和属性访问的区别
        • 2.惰性初始化
        • 3.要点
        • 三、理解“对象等同性”这一概念
          • 1.“==”和“isEqual:”区别
          • 2.判断等同性的方法
          • 3.如何自定义一个等同性判断
          • 4.特定类所具有的等同性判定方法
          • 5.等同性判定的执行深度
          • 6.容器中可变类的等同性
          • 7.要点
          • 四、以“类族模式”隐藏实现细节
            • 1.什么是类族
            • 2.如何创建类族
            • 3.Cocoa里的类族
            • 4.要点
            • 五、在既有类中使用关联对象存放自定义数据
              • 1.什么是关联对象
              • 2.要点
              • 六、理解objc_msgSend的作用
                • 1.认识OC中的消息
                • 2.消息转发过程
                • 3.边界情况
                • 4.要点
                • 七.理解消息转发机制
                • 1.消息转发的阶段
                • 2.动态方法解析
                • 3.备援接收者
                • 4.完整的消息转发
                • 5.消息转发全流程
                • 6.要点
                • 七.用“方法调配技术”调试“黑盒方法”
                  • 1.方法调配
                  • 2.动态消息派发系统和IMP
                  • 3.交换映射关系完成修改
                  • 4.要点
                  • 八.理解“类对象”的用意
                    • 1.id类型
                    • 2.Class对象
                    • 3.在类继承体系中查询类型信息
                    • 4.要点

                      一、理解“属性”这一概念

                      “属性”是OC的一项特性,用于封装对象中的数据。OC对象通常会把其需要的数据保存为各种实例变量。实例变量通过“存取方法”来访问。其中,“获取方法”用于读取变量值,而“设置方法”用于写入变量值。

                      1.如何定义实例变量

                      如何在类接口的public区段中声明一些实例变量,下面提供一个示例:

                      @interface EOCPerson: NSObject {
                      @public 
                      	NSString *_firstName;
                      	NSString *_lastName;
                      @private
                      	NSString *_someInternalData;
                      }
                      @end
                      

                      2.什么是不兼容现象,如何解决

                      假如在_firstName之前添加一个实例变量:

                      @interface EOCPerson: NSObject {
                      @public 
                      	NSString *_dateOfBirth;
                      	NSString *_firstName;
                      	NSString *_lastName;
                      @private
                      	NSString *_someInternalData;
                      }
                      @end
                      

                      那么新添加的实例变量就会代替原第一个位置实例变量的偏移量。

                      这样的话,如果代码使用了编译器计算出来的偏移量,那么在修改类定义之后必须重新编译,否则就会出错。例如:某个代码库中的代码使用了一份旧的类定义。如果和其相链接的代码使用了新的类定义,那么运行时就会出现不兼容现象。如果我们遇到不兼容现象,即改变了其原有的类定义程序所引发的现象,在OC中有两种解决方法,一种是:OC把实例变量当作一种存储偏移量所用的“特殊变量”,交由“类对象”保管。因为偏移量会在运行期查找,所以累的定义若变了,其存储的偏移量也就变了,它就可以找到正确的变量位置,这就是“应用程序二进制接口”(ABI)。另一种就是:尽量不要直接访问实例变量,而应该通过存取方法来做,就是我之前说过的setter、getter方法。

                      【学习iOS高质量开发】——对象、消息、运行期,在这里插入图片描述,第1张

                      3.理解@property关键字

                      setter方法一般命名为set+实例变量的名称,用于设置实例变量的值,getter方法一般命名为实例变量的名称,用于获取实例变量的值。有了这两种方法用户就可以通过这两种方法来实现对实例变量的赋值和获取,但是如果定义的实例变量太多了,每个实例变量都得自己完成这些方法的话未免也太麻烦了,所以这里OC就出现了“@property”关键字,用它创建的变量就是一个属性,它会自行合成一个实例变量相应的setter、getter方法。这时候你如果想再访问其内容的话,你可以使用“点语法”这其实和C语言中的结构体很类似。这个过程由编译器在编译器执行,所以编译器里看不到这些“合成方法”的源代码。

                      简单来说,以下两部分代码的效果是相同的:

                      @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语法来指定实例变量的名字。

                      4.理解@dynamic关键字

                      这个关键字会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。而且,在编译访问属性的代码时,即使编译器发现没有定义存取方法,也不会报错,他相信这些方法能在运行期找到。使用方法如下:

                      @interface EOCPerson : NSManageObject
                      @property NSString *firstName;
                      @end
                      @implementation
                      @dynamic firstName;
                      @end
                      

                      5.属性特质

                      1.原子性:

                      在默认情况下,由编译器所合成的方法通过锁定机制确保其原子性。在并发编程中,如果某操作具备整体性,也就是说,系统其他部分无法观察到其中间步骤所生成的临时结果,而只能看到操作前与操作后的结果,那么该操作就是“原子的”,或者说该操作具备“原子性”。如果属性具备nonatomic特质,则不使用同步锁,如果一个属性不具备nonatomic特质,那它就是“非原子的”。

                      2.读/写权限:

                      • 具备readwrite(读写)特质的属性拥有“获取方法”与“设置方法”。若该属性由@synthesize实现,则编译器会自动生成这两个方法。
                      • 具备readonly(只读)特质的属性仅拥有获取方法,只有当该属性由@synthesize实现时,编译器才会为其合成获取方法。你可以用此特质把某个属性对外公开为只读属性,然后在分类中将其重新定义为读写属性。

                        3.内存管理语义

                        属性用于封装数据,而数据则要有“具体的所有权语义”,如果自己编写存取方法,就必须同有关属性所具备的特质相符。

                        • 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使用。

                          6## .为什么几乎所有的属性都可以使用nonatomic:

                          在iOS中使用同步锁的开销较大,这会带来性能问题。一般情况下并不要求属性必须是“原子的”,因为这并不能保证“线程安全”,若要实现“线程安全”的操作,还需采用更为深层的锁定机制才行。

                          7.要点

                          1. 可以通过@property语法来定义对象中所封装的数据。
                          2. 通过“特质”来指定存储数据所需的正确语义。
                          3. 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
                          4. 开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能。

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

                          1.直接访问和属性访问的区别

                          • 由于不经过Objective-C的〝方法派发” (methoddispatch,参见第11条)步骤,所以

                            直接访问实例变量的速度当然比较快。 在这种情况 下,编译器所生成的代码会直接访 问保存对象实例变量的那块内存。

                          • 直接访问实例变量时,不会调用其〝设置方法”,这就绕过了为相关属性所定义的 “内 存 管 理 语 义 ” 。 比 方 说 , 如 果 在 A

                            R C 下 直 接 访 问 一个 声 明 为 c o p y 的 属 性 , 那 么 并 不 会拷贝该属性,只会保留新值并释放旧值。

                          • 如果直接访问实例变量,那么不会触发“键值观测"(Key-ValueObserving, Kvo)。通

                            知。这样做是否会产生问题,还取决于具体的对象行为。

                          • 通过属性来访问有助于排查与之相关的错误,因为可以给 “获取方法〞和/ 或 “设置 方法〞中新增

                            “断点”(breakpoint),监控该属性的调用者及其访问时机。

                            2.惰性初始化

                            也叫做“延迟初始化”。在惰性初始化的情况下,必须通过“获取方法”来访问属性,否则,实例变量就永远不会初始化。

                            一般用于:一个属性不常用,而且创建该属性的成本较高的情况。

                            3.要点

                            1. 在对象内部读取数据时,应该直接通过实例变量来读,而写人数据时,则应通过属性 来写。
                            2. 在初始化方法及dealloe 方法中,总是应该直接通过实例变量来读写数据。
                            3. 有时会使用惰性初始化技术配置某份数据,这种情况下,需要通过属性来读取数据。

                            三、理解“对象等同性”这一概念

                            1.“==”和“isEqual:”区别

                            是看地址来进行判断,地址不一致即返回false

                            isEqual:是专门用于判断的方法,不一定是看地址,也可以是其他的标准。

                            在NSObject类中,==与isEqual:没有明显区别,但在NSString中,已经完成了重写,只要字符串字符序列相同,isEqual:方法就返回true。

                            2.判断等同性的方法

                            NSObject协议中有两个用于判断等同性的关键方法:

                            -(BOOL)isEqual:(id)object;
                            -(BSUIntrger)hash;
                            

                            NSObject 类 对 这 两 个 方 法 的 默 认 实 现 是 : 当 且 仅 当 其 “ 指 针 值 ” (pointervalue)日 完 全 相 等时,这两个对象才相等。若想在自定义的对象中正确後写这些方法,就必领先理解其约定 (contract)。如果“ isEqual:〞方法判定两个对象相等,那么其hash 方法也必须返回同 一个 值。但是,如果两个对象的hash 方法返回同一个值,那么“ isEqual:〞方法末必会认为两者 相 等 。

                            3.如何自定义一个等同性判断

                            想要自定义一个等同性判断就的实现hash函数和isEqual函数,其中hash函数的返回值对每个对象都有特定的值,这个可以自己确定,不可以是一个常量,因为若是常量的话,不论什么数据返回的值都是相同的,这就没有比较的意义了,所以这里一定是要靠一定计算方法得出的结果,而isEqual函数就可以自行定义,只要能保证它比较的结果是正确的就可以。

                            这里建议hash函数的返回值是一个位运算,因为计算机识别的是二进制,使用位运算计算机计算的会更快些,效率更高。

                            4.特定类所具有的等同性判定方法

                            除了自定义类之外,每个类几乎都有其特定的等同性判定方法,NSArray类为“isEqualToArray:”,NSDictionary类为“isEqualToDictionary:”,若不是同一个类型的话,它就会直接抛出异常,是同一个类型才会开始一一处理。

                            在编写判定方法时,也应一并覆写“isEqual:”方法。后者的常见实现方式为:如果受测的参数与接收该消息的对象都属于同一个类,那么就调用自己编写的判定方法,否则就交由超类来判断。

                            5.等同性判定的执行深度

                            就用NSArray来说,NSArray的检测方式为:先看两个数组所含的对象个数是否相同,若相同,则在每个对应位置的两个对象身上调用其“isEqual:”方法。如果对应位置上的对象均相等,那么这两个数组就相等,这叫做“深度等同性判定”。

                            是否需要在等同性判定方法中检测全部字段取决于受测对象。只有类的编写者才可以确定两个对象实例在何种情况下应判定为相等。

                            6.容器中可变类的等同性

                            当你创建了一个对象,它的地址就相当于一个“箱子”,但是如果某对象在放入“箱子”之后哈希码又变了,那么其现在所处的这个箱子对他来说就是“错误”的。因为它可变了,说不定改变之后的哈希码或者地址就被改变了,所以它就是“错误”的。

                            要想解决这个问题,需要确保哈希码不是根据对象的“可变部分”计算出来的,或是保证放入collection之后就不再改变对象内容了,读者可以从此为出发点,对自己的等同性判断进行相应的完整。

                            7.要点

                            1. 若想检测对象的等同性,请提供“isEqual:”与hash方法。
                            2. 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
                            3. 不要盲目地逐个检测每条属性,而是应该依照具体需求来制定检测方案。
                            4. 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。

                            四、以“类族模式”隐藏实现细节

                            1.什么是类族

                            “类族”是一种很有用的模式,可以隐藏“抽象基类”背后的实现细节。就是像一个黑匣子,用户看不到里边的实现代码,只能知道输入和输出的值。Objective-C的系统框架中普遍使用此模式。比奶,ios 的用户 界面框架(user interface framework)UIKit中就有一个名为UIButton的类。想创建按钮,需 要 调 用 下面 这 个 “ 类方法” ( class method )

                            + (UIButton*)buttonWithType:(UIButtonType)type;
                            

                            该方法所返回的对象,其类型取决于传人的按钮类型(button type)。然而,不管返回什 么类型的对象,它们都继承自同 一个基类:UIButton。这么做的意义在于:UIButton 类的使 用者无领关心创建出来的按钮具体属于哪个 子类,也不用考虑按钮的绘制方式等实现细节。 使用者只需明白如何创建按钮,如何设置像“标题” (title)这样的属性,如何增加触摸动作 的目标对象等问题就好。

                            2.如何创建类族

                            首先要定义抽象基类,也就是一个新的类,其中可以包括你的类型选取,使用枚举器和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
                            

                            3.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这种已有类新增子类,那就得遵循以下规则:

                            • 子类应该继承自类族中的抽象基类。
                            • 子类应该定义自己的数据存储方式。
                            • 子类应当覆写超类文档中指明需要覆写的方法。

                              4.要点

                              1. 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
                              2. 系统框架中经常使用类族。
                              3. 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。

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

                              1.什么是关联对象

                              在写程序的时候,肯定会出现没办法创建其实例对象的情况,当然这种情况我们也就别想着创建一个它的子类了,那这种情况应该怎么办呢,这就要用到下面的“关联对象”了。

                              我们可以给某对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候,可以指明“存储策略”用以维护相应的“内存管理语义”。策略其实就和我们当时定义属性时的修饰符作用相似,下面就是其相应的关联类型:

                              【学习iOS高质量开发】——对象、消息、运行期,在这里插入图片描述,第2张

                              使用下列方法可以管理关联对象:

                              //添加关联对象
                              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);
                              

                              我们可以把某对象想象成NSDictionary,把关联到该对象的值理解为字典中的条目,于是,存取关联对象的值就相当于在NSDictionary对象上调用[object setObject:value forKey:key]与[object objectForKey:key]方法,但是和字典不同的是,它的key是一个“不透明的指针”。如果在两个键上调用“isEqual:”方法的返回值是YES,那么NSDictionary就认为二者相等;然而在设置关联对象值时,若想令一个键匹配到同一个值,则二者必须是完全相同的指针才行。鉴于此,在设置关联对象值时,通常使用静态全局变量做键。

                              2.要点

                              1. 可以通过“关联对象”机制来把两个对象连起来。
                              2. 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”。
                              3. 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug。

                              六、理解objc_msgSend的作用

                              1.认识OC中的消息

                              在对象上调用方法是OC中经常使用的功能。用OC的术语来说,这叫做“传递消息”。消息有“名称”或“选择子”,可以接受参数,而且可能还有返回值。我们之前用C语言写出来的函数就是“静态绑定”的函数,就是说,他在编译期就能决定运行时所调用的函数。但是若是我们使用一个函数指针来实现函数调用的话,这时他就成为一个“动态绑定”了,因为所调用的函数直到运行期才能确定。

                              在OC中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有的方法都是普通的C语言函数,然而对象收到消息之后究竟该调用那个方法则完全取决于运行期决定,甚至可以在程序运行时改变,这些特性使得OC成为一门真正的动态语言。

                              2.消息转发过程

                              下面是一个对象发送消息的例子:

                              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函数会依据接收者与选择子的类型来调用适当的方法,为了完成此操作,该方法需要在接收者所属类中搜寻其“方法列表”,如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”操作。

                              3.边界情况

                              前面讲的这部分内容只描述了部分消息的调用过程,其他“边界情况” (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 的相应消息。

                                4.要点

                                1. 消息由接收者、选择子及参数构成。给某对象“发送消息”也就相当于在该对象上“调用方法”。
                                2. 发给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码。

                                七.理解消息转发机制

                                当对象接收到无法解读的消息后,就会启动“消息转发”机制,程序员可经由此过程告诉对象应该如何处理未知消息。像之前我们会经常遇到的:

                                【学习iOS高质量开发】——对象、消息、运行期,在这里插入图片描述,第3张

                                上面这段异常信息是由NSObject 的“doesNotRecognizeSelector:” 方法所抛出的,此异常 表明 : 消息接收者的类型是__NSCFNumber, 而该接收者无法理解名为lowercaseString的选择子。 本例所列举的这种情况并不奇怪,因为NSNumber 类里本来就没有名为 lowercaseString 的方法。控制台中看到的那个__NSFCNumber 是 为了实现 “ 无缝桥接” 而使用的内部类(intermal class) ,配置NSNumber 对象时 也会一 并创建此对象。在本例中, 消息转发过程以应用程序崩溃而告终。

                                1.消息转发的阶段

                                消息转发分为两大阶段,第一阶段先征询接收者,所属的类,看其是否能动态添加方法,处理当前这个“未知的选择子”,这叫做“动态方法解析”。第二阶段涉及“完整的消息转发机制”。

                                2.动态方法解析

                                对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:

                                + (BOOL)resolveInstanceMethod:(SEL)selector
                                

                                该方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法,假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另一个方法,该方法与“resolveInstanceMethod:”类似,叫做“resolveClassMethod”。

                                使用这种办法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面 就可以了。 此方案常用来实现@dynamic属性, 比如说,要访问CoreData框架中NSManagedObjects对象的属性时就可以这么做,因为实现这些属性所需的存取方法 在编译期就能确定。

                                3.备援接收者

                                当接收者还有第二次机会能处理未知的选择子,在这一步中运行期系统会问它:能不能把这条消息转给其他接收者来处理。与该步骤对应的处理方法如下:

                                - (id)forwardingTargetForSelector:(SEL)selector;
                                

                                方法参数代表未知的选择子,若当前接收者能找到各授对象,则将其返回,若找不到, 就 返 回 nil。 通过此方案,我们可以用“组合”(composition) 来模拟出“多重继承”( multipleinheritance )的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法 将能够处理某选择 子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这些消息似的。

                                请注意,我们无法操作经由这 一步所转发的消息。若是想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了。

                                4.完整的消息转发

                                首先创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封于其中。**此对象包含选择子、目标及参数。**在触发NSInvocation对象时,“消息派发系统”将亲自出马,把消息指派给目标对象。,此步骤会调用下列方法来转发消息:

                                - (void)forwardInvocation:(NSInvocation *)invocation;
                                

                                这个方法可以实现得很简单,只需要改变调用目标,使消息在新目标上得以调用即可。实现此方法时,若发现某调用操作不应由本类来处理,则需调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用NSObject类的方法,那么该方法还会继而调用“doesNotRecognizeSelector:”以抛出异常,此异常表明选择子最终未能得到处理。

                                实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样的话, 继承体系中的每个类都有机会处理此调用请求, 直至NSObject。 如果最后调用了NSObject 类的方法,那么该方法还会继而调用“doesNotRecognizeSelector:” 以拋出异常, 此异常表明选择 子最终未能得到处理。

                                5.消息转发全流程

                                【学习iOS高质量开发】——对象、消息、运行期,在这里插入图片描述,第4张

                                6.要点

                                1. 若对象无法响应某个选择子,则进人消息转发流程。
                                2. 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加人类中。
                                3. 对象可以把其无法解读的某些选择子转交给其他对象来处理。
                                4. 经过上述两步之后,如果还是没办法处理选择 子,那就启动完整的消息转发机制。

                                七.用“方法调配技术”调试“黑盒方法”

                                1.方法调配

                                因为与给定的选择子名称相对应的方法是可以在运行期改变的,我们可以利用这个特性将在本类的所有实例中生效,而不仅限于覆写了相关方法的那些子类实例,这种方法就叫做“方法调配”。

                                2.动态消息派发系统和IMP

                                类的方法列表会把选择子的名称映射到相关的方法实现上面,使得动态消息派发系统能够根据此找到应该调用的方法,这些方法均以函数指针的形式表示,这种指针叫做IMP,原型如下:

                                id (*IMP)(id, SEL, ...);
                                

                                其实这里的选择子和IMP的关系就相当于字典类型中的key和value之间的关系,通过选择子可以找到相应的IMP来完成函数的调用实现之类的。所以,对于我们不知道的类方法我们还想向其中添加新的代码,我们就可以使用此原理。

                                3.交换映射关系完成修改

                                OC中给了我们相应的交换映射的方法,如下:

                                【学习iOS高质量开发】——对象、消息、运行期,我们添加新功能的本质就是修改之前的方法实现,也就是重写一个方法实现然后实现交换。,第5张

                                举例说明:

                                【学习iOS高质量开发】——对象、消息、运行期,在这里插入图片描述,第6张

                                这里是先获取到两个选择子的相应IMP,然后使用交换将其进行交换。我们也可以利用这种方式来完善系统给我们的“黑盒子”:

                                先将我们想增添的内容写到一个新的方法中:

                                【学习iOS高质量开发】——对象、消息、运行期,在这里插入图片描述,第7张

                                然后再实现两个IMP的交换:

                                【学习iOS高质量开发】——对象、消息、运行期,在这里插入图片描述,第8张

                                这里我们是想向之前的lowercaseString中添加一行新的输出代码,但是由于我们不知道其具体实现,所以我么利用交换IMP的方法完成,我们先将自己想添加的代码写入一个新的方法中,并在在此之前我们还调用了这个新方法自己,为什么调用自己呢?那是因为之后我们要实现IMP的交换,那么这里的自己就会代表的是先前的编译器中的lowercaseString方法,这样就完成了我们对系统给定方法的代码更改。

                                通过此方案,开发者可以为那些“完全不知道其具体实现的”黑盒方法增加日志记录功能,这非常有助于程序的调试,但是它只能在调试的程序的时候有用,若是过于滥用,反而会令代码变得不易读懂且难于维护。

                                4.要点

                                1. 在运行期,可以向类中新增或替换选择子所对应的方法实现。
                                2. 使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能。
                                3. 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。

                                八.理解“类对象”的用意

                                1.id类型

                                一般情况下,应该指明消息接收者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而类型为id的对象则不然,编译器假定它能响应所有信息。

                                每个OC对象实例都是指向某块内存数据的指针,所以在声明变量时,类型后面要跟一个“”字符。

                                描述Objective-C对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也在定义在这里:

                                typedef struct objc_object {
                                    Class isa;
                                } *id;
                                

                                2.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)。并且每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。

                                3.在类继承体系中查询类型信息

                                “isMemberOfClass:”能够判断出对象是否为某个特定类的实例(只有与其出创建的类型相同时才返回YES),而“isKindOfClass:”则能够判断出对象是否为某类或其派生类的实例。

                                4.要点

                                1. 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
                                2. 如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知。
                                3. 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。