静态绑定:编译的时候就已经确定了调用的方法。函数地址其实是硬编码在指令中。
动态绑定:运行期才确定要调用的方法。
1.消息调用
调用过程过程
(1)发送sendMessage:params消息给object对象;
(2)OC消息通过编译器转换成了C语言函数;
消息传递机制中的核心函数,叫做objc_msgSend,其“原型”如下:
void object_msgSend(id self, SEL cmd, ...)
OC中给对象发消息可以这样来写:
id returnValue = [someObject messageName: parameter];
someObject是接收者; messageName是选择子(方法名),选择子+参数合起来称为消息
编译器会将其转化为C语言的如下函数:
id returnValue = objc_msgSend(someObject, @selector(message:), params);
(3)在接收者someObject所属的类中,查询选择子selector(message:);
(4)如果没有找到,就去super class中查找;
(5)如果最终还是没有,就启动消息转发机制,将消息转发给别的对象。
(6)objc_msgSend会将匹配结果缓存在“快速映射表中”(fast map),每个类都有一个表,方便再次查找时快速查找。
2.消息转发机制
原理
(1)如果类想了解某条消息,它需要实现相对应的方法才行。
(2)由于编译期间不会进行动态绑定方法,所以如果没有实现对应方法的话,是不会抱错的,因为可以运行期间添加方法。
(3)如果运行期间也没有添加相应的方法的话,会启动“消息转发”直接,来让其他对象处理这条消息。
(4)但是如果其他对象也不能处理的话,程序就会抱错:
unrecognized selector sent to instance XXX
因为消息的接收者(instance)并没有实现对应的方法,不能理解这条消息(selector)。
消息转发过程(分为两个阶段)
第一阶段:动态方法解析
(1)对象收到无法解析的message后,首先调用所属类的下列方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel
+ (BOOL)resolveClassMethod:(SEL)sel
该方法会决定,是否新增一个实例方法来处理sel这个选择子;
前提是:该方法的视线代码已经写好了,只要在运行时动态插入到类里面即可。
(2)备援接收者
调用如下方法
- (id)forwardingTargetForSelector:(SEL)aSelector
若能当前接收者能找到备援对象,则将其返回;若找不到,就返回nil。
一个对象内部,可能有其他对象(实例变量),可以通过上面的方法,将内部能处理aSelector的对象返回,这样就好想该对象亲自处理了aSelector一样。
我们无法操作经由这一步所转发的消息,如果想要在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了
第二阶段:完整的消息转发机制
(1)创建 NSInvocation对象:该对象封装了selector、target、pramaters
(2)调用方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation
该方法多用于改变消息内容,比如追加pramaters,改变selector.
(3)如果本类不能处理,则应调用super class中的同名方法,直至NSObject
(4)如果调用了NSObject类的方法,就会继续调用
doesNotRecognizeSelector:
然后抛出异常,表明selector最终未能得到处理
(5)步骤越往后,处理消息的代价就越大。尽量在第一步就处理完,然后将该方法缓存起来,以便之后快速处理。
下面是一个代码示例
比如我们想给UIButton添加属性,比如name或order,用来描述button。
(1)首先定义一个类继承自UIButton
@interface XZButton : UIButton
@property (nonatomic ,strong) NSString *name;
@property (nonatomic ,strong) NSNumber *order;
@end
(2)在XZButton.m文件中导入下面的头文件,这样才能使用running time的函数。
#import <objc/runtime.h>
(3)注意使用 @dynamic,告诉编译器不用自动生成存取方法。
@dynamic name,order;
(4)声明一个NSMutableDictionary属性,用来存储要添加的属性,这里要注意了,既然是使用字典存储,那么如果你想添加NSInteger属性的话是会抱错的,因为字典不允许添加整型做为value(?)
@property (nonatomic ,strong) NSMutableDictionary *backingStore;
(5)init 方法中初始化字典
- (id)init{
if (self = [super init]) {
_backingStore = [NSMutableDictionary dictionary];
}
return self;
}
(6)注意这步是重点,调用了resolveInstanceMethod方法,也就是上面说的,询问receiver能否动态添加对应的方法来处理selector。
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString hasPrefix:@"set"]) {
class_addMethod(self,sel,(IMP)xZAutoDictionarySetter,"v@:@");
}else{
class_addMethod(self, sel, (IMP)xZAutoDictionaryGetter, "@ @:");
}
return YES;
}
(7)上面通过sel来判断存取方法,然后在下面实现对应的存取的方法
//set
void xZAutoDictionarySetter(id self,SEL _cmd,id value){
XZButton *typeSelf = (XZButton *)self;
NSMutableDictionary *backingStore = typeSelf.backingStore;
NSString *selectorString = NSStringFromSelector(_cmd);
NSMutableString *key = [selectorString mutableCopy];
//remove :
[key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
//remove set
[key deleteCharactersInRange:NSMakeRange(0, 3)];
//首字母小写
NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
[key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
if (value) {
[backingStore setValue:value forKey:key];
}else{
[backingStore removeObjectForKey:key];
}
}
//get
id xZAutoDictionaryGetter(id self, SEL _cmd){
XZButton *typeSelf = (XZButton *)self;
NSMutableDictionary *backingStore = typeSelf.backingStore;
NSString *key = NSStringFromSelector(_cmd);
return [backingStore valueForKey:key];
}
(8)注意,这里添加的get和set方法名不能重复,也就是不同的类中不能使用相同的方法名。否则会报错( duplicate symbol)。这里我也没太搞懂,不知道是不是因为是C函数,所以不能重名,一般的OC方法,在不同的类中是可以重名的,有明白的高人还请讲解一下
(9)使用
XZButton *button = [[XZButton alloc] init];
button.name = @"xuz";
button.order = [NSNumber numberWithInteger:14];
NSLog(@"button's name is %@ order is %ld",button.name,(long)[button.order integerValue]);
3.消息处理
(1)方法调配:处理selector的方法可以在运行期改变。既不需要源码,也不用通过子类继承来复写方法改变这个类本身的功能。并且新功能对本类的所有实例生效,而不是仅限于复写了相关方法的那些子类实例。
(2)类的方法列表会把selector映射到相关的实现方法之上,可以通过这张表来找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做IMP,原型如下:
id (*IMP) (id,SEL,....)
(3)映射表如下
(4)OC运行期间可以向表中添加新的selector(并实现对应的方法);也可以更改selector映射的IMP指针指向的方法;还可以交换两个selector映射的指针。
(5)举例子,如何交换两个方法的实现,这里创建了Nsstring的分类ExchangeString,并实现了一个方法,用来和lowercaseString交换实现的方法
#import "NSString+ExchangeString.h"
#import <objc/runtime.h>
@implementation NSString (ExchangeString)
- (NSString *)eoc_myLowercaseString{
//获取selector对应的IMP, 注意这里是class_getInstanceMethod(实例方法),我一开始写的是class_getClassMethod(类方法),没有交换成功,所以一直循环调用
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
//交换selector对应的IMP
method_exchangeImplementations(originalMethod, swappedMethod);
NSString *lowerCase = [self eoc_myLowercaseString];
NSLog(@"%@ - > %@",self,lowerCase);
return lowerCase;
}
使用:
NSString *testString = @"This is the testString";
NSString *resultString = [testString eoc_myLowercaseString];
结果
对象
(1)OC中对象并非在编译期就绑定好了,而是在运行期查找的。
(2)正常情况下,应该指明receiver的类型,如果想它发送无法解读的message的话,会报错。
(3)特殊情况id类型的对象,id 指代任意的OC对象类型。编译器假定它能响应任何message
(4)其实每个OC对象都是指向某块内存地址的指针。比如:
NSString *pointerVariable = @"some thing";
这里的pointerVariable可以理解为指向NSString的实例变量some thing的指针。
id genericTypedString = @"Some thing";
因为这里id类型,本身就已经是指针了,所以不用加*。
所有OC对象都是分配在堆上的,如果不使用*,那么会把对象分配在栈上,编译器会报错
栈和堆的区别:< >。
(5)OC对象所用的结构体定义在运行期的程序库头文件中,比如id类型:
typedef struct objc_object *id;
struct objc_object{
Class isa;
};
首个结构体的首个成员是Class类的变量,isa指针指向对象所属的类。
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;
};
isa指针:指向类所属的元类(meta),用来表述类对象本身所具备的的元数据,类方法即存在于元类中。
class指针:指向本类的超类。
name:类名。
version:类的版本号。
info:类的详情。
instance_size:该类实例对象的大小。
ivars:指向该类的成员变量的列表(类的属性列表)。
methodLists:指向该类的实例方法列表,它将方法选择器和方法实现地址联系起来。methodLists 是指向 ·objc_method_list 指针的指针,也就是说可以动态修改 *methodLists 的值来添加成员方法,这也是 Category 实现的原理,同样解释了 Category 不能添加属性的原因(为什么不能添加属性,因为ivars指向的是属性列表,修改ivars,并不能添加属性?只是改变了指向地址?)。
cache:Runtime 系统会把被调用的方法存到 cache 中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高。
protocols:指向该类的协议列表。
类的继承体系图如下:
上图实线是 super_class 指针,虚线是 isa 指针。根元类的超类是NSObject,而 isa 指向了自己(元类:即类对象所属的类型,也就是说所有的元类,它的类型都是NSObject?)。NSObject 的超类为 nil,也就是它没有超类。
(6)类型查询
isMemberOfClass:能够判断对象是否为某个特定类的实例。
isKindOfClass:能判断对象是否为某个类或其派生类的实例。
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict isMemberOfClass:[NSMutableDictionary class]];//yes
[dict isMemberOfClass:[NSDictionary class]];//no
[dict isKindOfClass:[NSMutableDictionary class]];//yes
[dict isKindOfClass:[NSDictionary class]];//yes
使用isa获得实例所属的类,然后通过super_class指针在继承体系中查询。因为对象是动态的,所以必须通过类型查询才能了解对象的真实类型。
从collection中获取的对象时,通常会使用类型查询,这些对象一般都不是强类型的,通常是id,所以如果不判断一下的话,有可能会报错。
如果是代理模式的话,那么用class方法查出来的类型和isKindOfClass查询处的结果会不一样,前者返回的是发起代理的对象,后者返回的是接受代理的对象。要尽量使用类型查询,这样会得到准确的对象类型。