文章目录
- 对象,消息,运行期
- 属性
- 属性特质
- 原子性
- 方法名
- 在对象内部尽量直接访问实例变量
- 对象等同性
- 特定类所具有的等同性判断
- 等同性判定的执行深度
- 容器中可变类的等同性
- 以“类族模式”隐藏实现细节
- 创建类族
- Cocoa里的类族
- 在既有类中使用关联对象存放自定义数据
- 关联对象用法举例
- 理解objc_msgSend的作用
- 理解消息转发机制
- 动态方法解析
- 备援接收者
- 完整的消息转发机制
- 消息转发全流程
- 要点
- 用“方法调配技术”调试“黑盒方法”
- 理解类对象的用意
- 在继承体系中查询类型信息
对象,消息,运行期
- 用Objective-C等面向对象语言来编程时,“对象”就是“基本构造单元”。开发者可以通过对象来存储并传递数据。
- 在对象之间传递数据的并执行任务的过程叫做“消息传递”
- 当应用程序运行起来后,为其提供相关支持的代码叫做“Objective-C运行期环境”,它提供了一些使得对象之间能够传递消息的重要函数。并且包含创建类实例所用的全部逻辑。
属性
- “属性”是Objective-C的一项特性,用于封装对象中的数据。Objective-C对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”来访问。“获取方法”用于读取变量值,“设置方法”用于写入变量值。
- 开发者可以令编译器自动编写与属性相关的存取方法。此特性引入了一种新的“点语法”,使得开发者可以依照类对象来访问其中数据。
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCOPerson : NSObject{
@public
NSString* _firstName;
NSString* _lastName;
@private
NSString* _someInteralData;
}
@end
NS_ASSUME_NONNULL_END
- 这种写法的问题是:对象布局在编译器就已经固定了。只要碰到访问_firstName的代码,编译器就把其替换为:“偏移量”,这个“偏移量”就是“硬编码”,表示该变量距离存放对象的内存区域的起始地址有多远。
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCOPerson : NSObject{
@public
NSDate* _dateOfBirth;
NSString* _firstName;
NSString* _lastName;
@private
NSString* _someInteralData;
}
@end
NS_ASSUME_NONNULL_END
- 原来表示_firstName的偏移量现在却指向_dateOfBirth。把偏移量硬编码其中的那些代码都会读取到错误的信息
==如果代码使用了编译期计算出来的偏移量,那么在修改类定义之后必须重新编译,否则出错。如某个代码库中的代码使用了一份久的类定义,如果和其相链接的代码使用了新的类的定义,那么运行期就会出现不兼容现象。Objective-C对此的解决办法是把实例变量当作一种存储偏移量所用的“特殊变量”,交由“类对象”保管。偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也就变了。这样的话,无论何时访问实例变量,总能正确使用偏移量。甚至可以在运行期向类中增加新的实例变量。这就是稳固的“应用程序二进制接口”(ABI),ABI中定义了许多内容,其中一项就是生成代码时所应遵循的规范。
- 所以说,不一定要在接口中把全部实例变量都声明好,可以将某些变量从接口的public区段移走,以便保护与类实现有关的内部信息。
- 还有一种解决方法,不要直接访问实例变量,通过存取方法来访问。在对象的接口定义中,可以使用属性,能够访问封装在对象里的数据。因此可以吧属性当作一种简称,意思是说,编译器会自动写出一套存取方法,用以给定类型中具有名称的变量。
@interface EOCOPerson : NSObject
@property NSString* firstName;
@property NSString* lastNme;
@end
@interface EOCOPerson : NSObject
- (NSString*)firstName;
- (void)setFirstName:(NSString*)firstName;
- (NSString*)lastName;
- (void)setLastName:(NSString*)lastName;
@end
这两段代码意义相同。
- 要访问属性,可以使用点语法。编译器会把点语法转换为对存取方法的调用,使用点语法的效果和直接调用存取方法相同。因此,使用点语法和直接调用存取方法之间没有差别
- 如果使用了属性,那么编译器会自动编写访问这些属性所需的方法,此过程叫“自动合成”。编译器还要自动向类中添加适当类型的实例变量,并且在属性名称下加下划线,以此作为实例变量的名字,也可以在类的实现代码里通过@synthesize语法来指定实例变量的名字。
- 如果不想通过编译器自动合成存取方法,可以自己实现。如果只实现了其中一个存取方法,那么另外一个由编译器来合成。还有一种方法能阻止编译器自动合成存取方法,就是使用@dynamic关键字,他会告诉编译器不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。在编译器访问属性代码时,即使编译器发现没有定义存取方法,也不会报错。
属性特质
属性的各种特质设定也会影响编译器所生成的存取方法。这个属性指定了三项特质
@property (nonatomic, readwrite, copy) NSString* firstName;
原子性
默认情况下,编译器合成的方法会通过锁定机制确保其“原子性”。如果属性具备nonatomic特性,则不适用同步锁。尽管没有atomic的特质(如果属性不具备nanatomic特质,那它就是“原子的”(atomic)),但是仍然可以在属性特质中写明这一点,编译器不会报错。若所自己定义的存取方法,那么就应该遵从与属性特质相符合的原子性。
在并发编程中,如果某操作具备整体性,也就是说,系统的其他部分无法观察到其中间步骤所生成的临时结果,而只能看到操作前和操作后的结果,那么该操作就是“原子的”或者说该操作具备了“原子性”
读写权限
- 具备readwrite(读写)特质的属性拥有“获取方法”(getter)和“设置方法”(setter)(也叫做获取器,设置器),若属性由@synthesize实现,编译器自动生成这两个方法。
- 具备readonly(只读)特质的属性仅拥有获取方法,只有当该属性由@synthesize实现时,编译器才为其合成获取方法。
内存管理语义
- 属性用于封装数据,而数据要有“具体的所有权语义”。用设置方法设置一个新值时,是应该保留“retain”呢,还是只将其赋值给底层实例变量就好,编译器在合成存取方法时,要根据此特性来决定所生成的代码。如果自己编写存取方法,那么就必须同有关属性所具备的特质相符合
assign | “设置方法”只会执行针对“纯量类型”(scalar type,例如CGFloat或NSInteger等)的简单赋值操作 |
---|---|
strong | 表明该属性定义了一种“拥有关系”,为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后将新值设置上去 |
weak | 表明该属性定义了一种“非拥有关系”,为这种属性设置新值时,设置方法既不会保留新值,也不会释放旧值。此特质同assign相似,在属性所指的对象遭到摧毁时,属性值也会清空 |
unsafe_unretained | 语义和assign相同,但是它适用于“对象类型”,表示一种非拥有关系,(不保留,unretained),当目标对象遭到摧毁时,属性不会自动清空(不安全,unsafe)这一点和weak有区别 |
copy | 所属关系和strong类似。然而设置方法并不保留新值,而是将其拷贝。 |
方法名
可通过如下特质来指定存取方法的方法名
- getter=指定“获取方法”的方法名
@property (nonatomic, getter = isOn) BOOL on;
- setter=指定“设置方法”方法名。
- ==如果自己来实现这些存取方法,那么应该在“设置方法”中拷贝相关对象,否则会误导该属性的使用者。==如果在其他方法里设置属性值,那么同样要遵守属性定义中宣称的语义。
// EOCOPerson.h
// text。12.14
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCOPerson : NSObject
@property (copy) NSString* firstName;
@property (copy) NSString* lastName;
- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName;
@end
NS_ASSUME_NONNULL_END
// EOCOPerson.m
// text。12.14
//
//
#import "EOCOPerson.h"
@implementation EOCOPerson
- (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName{
if (self = [super init]){
_firstName = [firstName copy];
_lastName = [lastName copy];
}
return self;
}
如在刚才的EOCOPerson中,由于字符串可能会改变,所以要把相关属性的“内存管理语义”声明为copy。该类中新增了一个“初始化方法”。在实现这个自定义类的初始化时,一定要遵守属性中宣称的copy语义,因为属性定义就相当于类和待设置的属性值之间的契约。
@property (copy, readonly) NSString* firstName;
@property (copy, readonly) NSString* lastName;
如果是只读属性,编译器不会为其创建对应的“设置方法”,但我们仍要写上这些属性的语义,以此表明初始化方法在设置这些属性值时所用的方式。
- atomic的获取方法会通过锁定机制来确保其原子性,也就是说,如果两个线程读写同一属性,那么不论何时,总能看到有效的属性值。若是不加锁,或者说使用nonatomic语义,那么当其中一个正在改写某属性时,另外一个线程也许会突然闯入,将尚未修改好的值读取。
- 所有的属性声明都是nanatomic,是因为:在IOS中使用同步锁的开销较大,会带来性能问题。一般情况并不要求属性必须是“原子的”,因为这并不能保证“线程安全”,若要实现线程安全,还得采用更为深层的锁定机制
在对象内部尽量直接访问实例变量
在对象之外访问实例变量时,总是通过属性来做。在读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候通过属性
// EOCOPerson.h
// text。12.14
//
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCOPerson : NSObject
@property (nonatomic, copy) NSString* firstName;
@property (nonatomic, copy) NSString* lastName;
- (NSString*)fullName;
- (void)setFullName:(NSString*)fullName;
@end
NS_ASSUME_NONNULL_END
// EOCOPerson.m
// text。12.14
//
//
#import "EOCOPerson.h"
@implementation EOCOPerson
- (NSString*)fullName{
return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
- (void)setFullName:(NSString *)fullName{
NSArray* components = [fullName componentsSeparatedByString:@" "];
self.firstName = [components objectAtIndex:0];
self.lastName = [components objectAtIndex:1];
}
@end
在fullName的获取与设置方法中,我们使用点语法,通过存取方法来访问相关实例变量。
// EOCOPerson.m
// text。12.14
//
//
#import "EOCOPerson.h"
@implementation EOCOPerson
- (NSString*)fullName{
return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];
}
- (void)setFullName:(NSString *)fullName{
NSArray* components = [fullName componentsSeparatedByString:@" "];
_firstName = [components objectAtIndex:0];
_lastName = [components objectAtIndex:1];
}
@end
这两种写法有几个区别
- 由于不经过Objective-C的方法发展,所以直接访问实例变量的速度较快。在这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
- 直接访问实例变量时,不会调用其“设置方法”,绕过了相关属性定义的“内存管理语义”
- 直接访问实例变量,不会触发“键值观测”(KVO通知),这样做会不会产生问题,还要取决于具体的对象行为。
- 通过属性来访问有助于排查与之相关的错误,因为可以给“获取方法”,“设置方法”中新增断点,监控该属性的调用者及其访问时机
有一种合理的折中方案,在写入实例变量时,通过设置方法来做,读取实例变量时,直接访问,此办法既能提高读取操作的速度,又能控制对属性的写入操作。使用这种方法时,需要注意以下几点
- ⚠️在初始化方法中应该如何设置属性值,这种情况下总应该直接访问实例变量,因为子类可能会“覆写”设置方法。
- ⚠️“惰性初始化”。在这种情况下,必须通过“获取方法”来访问属性,否则,实例变量永远不会初始化。如果使用了“惰性初始化”,那么必须通过存取方法访问属性。
对象等同性
根据“等同性”来比较对象是一个非常有用的功能。不过,按照==操作符比较出来的结果不一定是我们需要的,因为该操作比较的是两个指针本身,而不是其所指的对象。应该使用isEqual:方法来判断两个对象的等同性。一般来说,两个类型不同的对象总是不相等的。某些对象提供了特殊的“等同性判断方法”,如果已经知道两个手册对象属于同一个类,就可以使用这种方法。
可以看到==与等同性判断方法之间的差别,NSStirng类实现了一个自己独有的等同性判断方法,叫做“isEqualToString:”。传递给该方法的对象必须是NSString,否则结果未定义。调用此方法比“isEqual:”方法快。
特定类所具有的等同性判断
NSArray和NSDictionary类也具有特殊的等同性判定方法,前者名为“isEqualToArray”,后者名为“isEqualToDictionary”。如果和其比较的对象不是数组或字典,那么这两个方法会各自抛出异常
- 如果经常需要判断等同性,那么可能会自己来创建等同性判定方法,因此无需检测参数类型,能大大提升检测速度。
- 在编写判定方法时,也应一并覆写“isEqual:”方法。
等同性判定的执行深度
创建等同性判定时,需要决定是根据整个对象来判断等同性,还是根据其中几个字段来判断。NSArray的检测方法为先看两个数组所含对象个数是否相同,若相同,则在每个对应位置上调用“isEqual:”方法。如果对应位置上的对象均相等,那么这两个数组相等。这叫做“深度等同性判定”
有时也可以通过标识符来判断等同性。尤其是此属性声明为readonly时。因为只要两者标识符相同,肯定表示同一个对象,因而必然相等。这样的话,无需比较对象每条数据,只要标识符相同,说明这两个对象是由一个数据源创建的,其余数据也必然相同。
容器中可变类的等同性
如果在容器中放入可变类对象,把某个对象放入collection之后,就不在改变其哈希码了,collection会把各个对象按照其哈希码分装到不同的“箱子数组”中。如果对象在放入“箱子数组”之后哈希码又变了,那么其现在所处的这个箱子对它来说就是“错误”的。要想解决这个问题,要确保哈希码不是根据对象的可变部分计算出来的,或者是保证放入collection之后就不再改变对象内容了。
因为刚才加入的那个数组对象和set中已有的数组对象相等,所以set不会改变。
arrayC与set中已有的对象不相等,set中此时有两个对象。
set中可以包含两个相同的数组!因为我们修改了set中已有的对象,所以出现了这个情况。
- setB又只剩下一个对象了,此时的setB看上去好像是一个空set开始,通过逐个向其中添加新对象而创建出来的。
- 如果我们要把对象放入collection,就要知道会发生什么后果。
以“类族模式”隐藏实现细节
“类族”(类簇)是一种很有用的模式,可以隐藏“抽象基类”背后的实现细节。
- +(UIButton*)buttonWithType:(UIButtonType)type;
- 该方法所返回的对象,其类型取决于传入的按钮类型(button type)。但是不论返回什么对象,它都继承自基类UIButton
- 使用:“类族模式”,该模式可以灵活应用多个类,将他们的实现细节隐藏在抽象基类后,保持接口整洁。用户无需自己创建子类实例,只需调用基类方法来创建。
创建类族
如果基类实现了一个“类方法”,该方法根据待创建的雇员类别分配好对应雇员类实例。
如果对象所属的类位于某个类族中,那么在查询其类型信息时就要当心。可能你觉得自己创建了某个类的实例,然而实际上创建的却是其子类的实例。
Cocoa里的类族
系统框架里有许多类族,大部分collection类都是“类族”,例如NSArray与其可变版本NSMutableArray。实际上有两个抽象基类,一个用于不可变数组,另一个用于可变数组。尽管具备的公用接口的类有两个,但仍然可以合作起来算作一个类族。不可变的类定义了所有数组都通用的方法,可变的类定义了对可变数组适用的方法。两个类属于同一类族,这意味着二者在实现各自类型数组是可以共用实现代码。,
-
在使用NSArray的alloc方法获取实例时,该方法会首先分配一个属于某个类的实例,充当“占位数组”。该数组稍后回转换为另一个类的实例,而那个类则是NSArray的实例子类。像NSArray这样的类的背后其实是个类族
-
我们经常向类族中新增实体子类。需要遵守一些规则
-
子类应该继承自类族中的抽象基类。
-
子类应该定义自己的数据存储方式
-
子类应该覆写超类文档中指明需要覆写的地方
-
在每个抽象基类中,都有一些子类必须覆写的方法。
-
在类族中实现子类时所需遵循的规范一般都会定义在基类的文档中。
在既有类中使用关联对象存放自定义数据
有时需要在对象中存放相关信息,这时我们通常会从对象所属类中继承应该子类,然后改用这个子类对象。有时候类的实例可能是由某种机制所创建的,开发者无法令这种机制创建出自己所写的子类实例。“关联对象”可以解决此问题。
-
可以给某对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候,可以指明“存储策略”,用以维护相应的“内存管理语义”
-
我们可以把某对象想象成NSDictionary,把关联对象的值理解为字典中的条目,于是,存取关联对象的值就相当于在NSDictionary对象上调用[object setObject:value forKet:key]与[object objectForKey:key]方法。设置关联对象时用的键(key)是个“不透明指针”(其所指向的数据结构不局限于特定类型的指针)如果在两个键上点用isEqual:方法返回的值是YES,那么NSDictionary就认为二者相等,然而在设置关联对象值时,若想令两个键匹配到同一个值,二者必须是完全相同的指针才行。所以在设置关联对象值时,通常使用静态全局变量做键。
关联对象用法举例
UIAlertView类用于向对象展示警告信息。当用户按下关闭按钮时,要委托协议(delegate protocol)来处理此动作。要想设置好这个委托协议,就得把创建警告视图和处理按钮动作的代码分开。
- 如果想在同一个类里处理多个警告信息,我们在delegate方法中检查传入的alertView阐述,选择适合的代码块执行。
- 但是我们也可以通过管联对象来实现不同alertView实现不同功能。创建完警告视图之后,设定一个与之关联的块,等到执行delegate方法的时候再将其读出来。
- 这种方法很有效,但是应该只在其他方法行不通时采用这个方法。如果滥用这种方法,很快会令代码失控,难于测试。
- ”保留环“产生的原因很难查明,因为关联对象之间的关系并没有正式的定义,其内存管理语义是在关联对象的时候才定义的,不是在借口中提前定义好的。
理解objc_msgSend的作用
在对象上调用方法是Objective-c常用的功能,这叫做“消息传递”。消息有“名称”或者“选择子”,可以接受参数,可能有返回值
由于OC是C的超集,所以最好先理解C语言的函数调用方式。C语言使用“静态绑定”,在编译器就能决定运行时所调用的函数。
- 不考虑内联的话,编译器在编译代码的时候就知道程序中的所有函数,会直接生成调用这些函数的指令。
- 在OC中,如果向某对象传递消息,那就回使用动态绑定来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,对象收到消息后,究竟调用那个方法由运行期决定,甚至可以在程序运行时改变。
- 给对象发送消息
id returnValue = [someObject messageName:parameter];
someObject是接收者,messageName是选择子。选择子与参数合称“消息”。编译器看到此消息后,将其转换为一体哦啊标准的C语言函数调用。调用的函数是消息传递机制中的核心函数。叫做objc_msgSend
void objc_msgSend(id self, SEL cmd, ...)
- 这个是参数个数可变的函数。可接受两个或两个以上的参数,第一个参数代表接收者。第二个参数代表“选择子”(SEL是选择子的类型),后续参数是消息中的那些参数,顺序不变。选择子就是方法的名字。“选择子”与“方法”这两个词经常交替使用。
objc_msgSend函数会依据接收者与选择子的类型来调用合适的方法。该方法需要在接收者所属的类中搜寻其“方法列表”,如果能找到与选择子名称相符的方法,就跳转是心啊代码。如果找不到,沿着继承体系往上查找。等到找到合适的方法后进行跳转。如果还是找不到,执行“消息转发”操作
objc_msgSend_stret | 待发送的消息要喊回结构体 |
---|---|
objc_msgSend_fpret | 消息返回浮点数 |
objc_msgSendSuper | 给超类发消息,也有另外两个与objc_msgSend_stret和objc_msgSend_fpret等效的函数,用于处理发给super相应的消息 |
- objc_msgSend一旦找到应该调用的方法实践之后,就会跳转过去。因为OC对象的每个方法都可以视为简单的C函数。
- 如果某函数的最后一项是调用另外一个函数,那么就可以运用“尾调用优化”。编译器会生成调转至另外一函数所需的指令码,而且不会向调用堆中推入新的“栈帧”。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用时,才能执行“尾调用优化”。
理解消息转发机制
若想令类能理解某条消息,我们必须以程序码实现出对应的方法。
- 消息转发分为两大阶段。第一阶段线征询接收者,所属的类,看能否动态添加方法,以处理这个“未知的选择子”。这叫着“动态方法解析”。第二阶段设计“完整的消息转发机制”。如果训醒期系统研究把第一阶段执行完了,那么接收者自己就无法再已动态新增方法的手段来相应包含选择子的消息了。
动态方法解析
对象在收到无法解读的消息后,首先调用其所属的类的下列方法。
- (BOOl)(BOOL)resolveInstanceMethod:(SEL)sel
- 该方法的参数就是那个未知的选择子,其返回值为Boolean类型。表示这个类是否能新增一个实例方法用于处理此选择子。使用这种方法的前提是:相关方法的实现代码写好,只等着运行的时候动态插在类里面
备援接收者
当前接收者还有第二次机会能处理未知的选择子。在这一步中,运行期系统会问它,能不能把这条消息转给其他接收者来处理。
- (id)forwardingTargetForSelector:(SEL)aSelector
- 方法参数代表未知选择子,若当前接收者能找到背援对象,则将其返回,若找不到,就返回nil。通过此方法,我们可以用“组合”来模拟出“多重继承”的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可以经由此方法将能够处理的某些选择子的相关内部对象返回。
- 我们无法操作经由这一步所转发的消息。若是想在发送给备援接收者之前先修改的消息内容,那就得通过完整的消息转发机制。
完整的消息转发机制
如果转发算法已经来到这和一步,唯一能做的就是启用完整的消息转发机制。创建NSInvocation对象,把尚未处理的那条消息有关的全部细节都封于其中。此时对象包含选择子,目标,参数。
- -(void)forwardInvocation:(NSInvocation *)anInvocation
- 这个方法可以实现得很简单。只需改变调用目标,使得消息在新目标上得以调用。然而这样写出来的方法与“备援接收者”方案所实现的方法等效,所以很少有人采用这么简单的实现方式。
- 实现此方法时,若发现某调用操作不应该由本类处理,则调用超类的同名方法。
消息转发全流程
- 接收者在每一步中均有机会处理消息。步骤越往后处理的代价越大。最好能在第一步处理完。这样的话,运行期系统就可以将此方法缓存起来了
// EOCAutoDictionary.h
// text12.12
//
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCAutoDictionary : NSObject
@property (nonatomic, strong) NSString* string;
@property (nonatomic, strong) NSNumber* number;
@property (nonatomic, strong) NSDate* date;
@property (nonatomic, strong) id opaqueObject;
@end
NS_ASSUME_NONNULL_END
// EOCAutoDictionary.m
// text12.12
//
//
#import "EOCAutoDictionary.h"
#import "objc/runtime.h"
@interface EOCAutoDictionary()
@property (nonatomic, strong) NSMutableDictionary* backingStore;
@end
@implementation EOCAutoDictionary
@dynamic string,number,date,opaqueObject;
- (id)init{
if (self = [super init]){
_backingStore = [[NSMutableDictionary alloc] init];
}
return self;
}
id autoDictionaryGetter(id self, SEL _cmd);
void autoDictionarySetter(id self, SEL _cmd, id value);
+(BOOL)resolveInstanceMethod:(SEL)selector //sel是引起异常的那个函数
{
NSString *selectorString = NSStringFromSelector(selector);//获取sel方法的名字 转换为string
IMP impSet = class_getMethodImplementation(self, @selector(autoDictionarySetter));
if ([selectorString hasPrefix:@"set"])//如果名字里面有set 则动态添加autoDictionarySetter
class_addMethod(self, selector, impSet, "v@:@");//OC的写法添加imp
else//如果不带则添加autoDictionaryGetter
class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:");//C的方法添加imp
return YES;
return [super resolveInstanceMethod:selector];
}
@end
- 当开发者手册在EOCAutoDictionary实例上访问某个属性时,运行期系统还找不到对应选择子,因为所需选择子没有直接实现,也没有合成出来。
要点
- 若对象无法响应某个选择子,则进入消息转发机制
- 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时在将其加入类中
- 对象可以把其无法解读的某些选择子交给其他对象来处理
- 经过上述两步如果还是没办法处理选择子,启动完整的消息转发机制
用“方法调配技术”调试“黑盒方法”
给定的选择子名称相对应的方法也可以在运行期改变。我们不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类本身的功能。
- 类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做IMP
id (*IMP)(id, SEL, ...)
NSString类可以响应lowcreaseString,uppercaseString,capitalizedString等选择子。
开发者可以向其中新增选择子,也可以改变某选择子所对应方法实现,还可以交换两个选择子所映射到的指针。
- 在新的映射表中。多了一个名为newSelector的选择子,capitalizedString的实现也变了,lowercaseString和uppercaseString实现互换了。这些修改君无法编写子类,只要修改了“方法表”的布局,就会反映到程序中所有的NSString实例之上。
void method_exchangeImplementations(Method m1, Method m2)
Method class_getInstanceMethod(Class aClass, SEL aSelector)
Msthod originalMethod = class_getInstanceMethod([NSStringclass], @selector(lowercaseString));
Msthod swappedMethod = class_getInstanceMethod([NSStringclass], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
- 使用另一份实现原来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原油实现中添加新功能。
- 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用
理解类对象的用意
oc实际上是一门及其动态的语言,如果类无法立即响应某个选择子,那么就会启动消息转发流程。然而,消息的接收者究竟是何物?对象类型是在运行期查找,id类型可以对应OC的仁义对象类型。
- 在运行期检测对象类型,这一操作也叫“类型信息查询”。在程序中不宜直接比较对象所属的类,明智的做法是调用“类型信息查询方法”
- 每个OC对象实例都是指向某块内存数据的指针。所有的OC对象都是如此,若是想把对象所需的内存分配到栈上,编译器会报错
- 对于通用的id类型,其本身已经是指针了
在继承体系中查询类型信息
- 可以用类型信息查询方法来检视类继承体系。“isMemberOfClass:”能够判断出对象是否为某个特定的类的实例,而“isKindOfClass:”能判断对象是否为某类或其派生类的实例
- 这样的类型信息查询方法使用isa指针获取对象所属的类,然后通过super_class指针在继承体系中游走
- 从collection中获取对象时,通常会查询类型信息,这些对象不是强制类的,把他们从collection中取出来时,其类型通常是id。如果想知道具体类型,可以使用类型信息查询方法。
- 也可以使用比较类对象是否等同的方法来做。使用==操作符。
- 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些类对象可能实现了消息转发功能