Running Time

静态绑定:编译的时候就已经确定了调用的方法。函数地址其实是硬编码在指令中。

动态绑定:运行期才确定要调用的方法。

1.消息调用

调用过程过程

Alt Image Text

(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)。

消息转发过程(分为两个阶段)

Alt Image Text
第一阶段:动态方法解析

(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查询处的结果会不一样,前者返回的是发起代理的对象,后者返回的是接受代理的对象。要尽量使用类型查询,这样会得到准确的对象类型。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,679评论 0 9
  • 1,NSObject中description属性的意义,它可以重写吗?答案:每当 NSLog(@"")函数中出现 ...
    eightzg阅读 4,132评论 2 19
  • 父类实现深拷贝时,子类如何实现深度拷贝。父类没有实现深拷贝时,子类如何实现深度拷贝。• 深拷贝同浅拷贝的区别:浅拷...
    JonesCxy阅读 991评论 1 7
  • 1.OC里用到集合类是什么? 基本类型为:NSArray,NSSet以及NSDictionary 可变类型为:NS...
    轻皱眉头浅忧思阅读 1,363评论 0 3
  • OC基础总结 重新回过头看这些基础知识,对许多知识点都有新的认识,拥有坚实的基础才能更快的成长。 #improt ...
    xx_cc阅读 6,015评论 10 56