消息发送机制和运行时

消息发送机制定义

OC的函数调用称为消息发送。
OC的消息发送属于动态调用过程。即在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。(事实证明,在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。而C语言在编译阶段就会报错。

OC是如何实现动态调用

假如在OC中写了这样的一个代码:
[obj makeText];
其中obj是一个对象,makeText是一个函数名称。对于这样一个简单的调用。在编译时RunTime会将上述代码转化成
objc_msgSend(obj,@selector(makeText));
首先我们来看看obj这个对象,iOS中的obj都继承于NSObject。
@interface NSObject <NSObject> { Class isa OBJC_ISA_AVAILABILITY; }
在NSObjcet中存在一个Class的isa指针。然后我们看看Class:

struct objc_class {
  Class isa; // 指向metaclass
  Class super_class ; // 指向其父类
  const char *name ; // 类名
  long version ; // 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取
  long info; // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
  long instance_size ; // 该类的实例变量大小(包括从父类继承下来的实例变量);
  struct objc_ivar_list *ivars; // 用于存储每个成员变量的地址
  struct objc_method_list **methodLists ; // 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法;
  struct objc_cache *cache; // 指向最近使用的方法的指针,用于提升效率;
  struct objc_protocol_list *protocols; // 存储该类遵守的协议
    }
  • 在ObjC中,每个Class实际上都有两个Class,the Class object (类对象)和meta Class(元类), the Class object 定义了instance method,metaClass 定义了class method,所以,每个Class对象实际上是metaClass的一个单例。其实类对象是编译器为每个类生成的有且只有一个的单例。而这个单例的isa指针指的是metaClass。因此,对应类的内存布局的理解是:
    1、每个类实际上都有类对象和元类两个概念
    2、类对象是编译器为每个类生成的有且只有一个的保存关于实例方法的信息的单例
    3、元类则是保存类方法信息的
    4、类对象时元类对象的一个单例 ,类对象的isa指针指向元类
    5、类对象和元类都是基于objc_class结构的
  • Class isa:指向metaclass,也就是静态的Class。一般一个Obj对象中的isa会指向普通的Class,这个Class中存储普通成员变量和对象方法(“-”开头的方法),同时这个Class中的isa指针指向静态Class,也就是metaclass,metaclass中存储static类型成员变量和类方法(“+”开头的方 法)
    Class类中其他的成员这里就先不做过多解释了,下面我们来看看:
  • @selector (makeText)**:这是一个SEL方法选择器。对于一个类中每一个方法包含两个隐式参数:1、方法所属的对象 2、方法的编号(SEL)。SEL其本身是一个Int类型的存放着方法名字的地址,作用是快速的通过方法名(makeText)查找到对应方法的函数指针,然后调用对应方法。
    所以ios类中不能存在2个名称相同的方法,即使参数类型不同。因为SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。

最后,消息发送之后动态查找对应的方法

1 .编译器将代码[obj makeText];转化为objc_msgSend(obj, @selector (makeText));
2 .通过obj的isa指针找到obj对应的class。
3 .在Class中先去cache中通过SEL查找对应函数method(猜测cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度),若 cache中未找到。再去methodList中查找,若methodlist中未找到,则取superClass中查找。
4 .若能找到,通过method中的函数指针跳转到对应的函数中去执行。最后将method加入到cache中,以方便下次查找。
5 .当objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给 方法实现,同时,它还将传递两个隐藏的参数:1、接收消息的对象 2、方法选标
objc_msgSend(receiver, selector, arg1, arg2, ...)

那么,消息发送机制能够给我们的编程带来些什么新鲜血液呢?

  • 获得方法地址
    取得方法的地址,并且直接象函数调用一样调用它,这样就避免了动态绑定。
    如果一个方法会被连续调用很多次,多次调用方法时发送消息的开销就会比较大,使用方法地址来调用方法就会节省开销。但是这只有在指定的消息被重 复发送很多次时才有意义。
 setter(targetList[i], @selector(setFilled:), YES);
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];

方法指针的第一个参数是接收消息的对象(self),第二个参数是方法选标(_cmd)。这两个参数在方法中是隐藏参数,但使用函数的形式来调用方法时必须显示的给出。

  • 动态方法解析
    需要动态地提供一个方法的实现的时候。
    @dynamic propertyName;
    即表示编译器须动态地生成该属性对应地方法。
    可以通过 class_addMethod 方法将一个函数加入到类的方法中。
    void dynamicMethodIMP(id self, SEL _cmd) { // implementation .... }
    可以通过实现 resolveInstanceMethod:和 resolveClassMethod:来动态地实现给定选标的对象方法或者类方法。
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
        if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
        return YES;
}
        return [super resolveInstanceMethod:aSEL];
}
@end

在进入消息转发机制之前,respondsToSelector:和instancesRespondToSelector: 会被首先调用。您可以在这两个方法中为传进来的选标提供一个IMP。如果您实现了resolveInstanceMethod:方法但是仍然希望正常的消息转发机制进行,您只需要返回NO就可以了。
例如,摘自网上的一段示例代码:

@interface SomeClass : NSObject
@property (assign, nonatomic) float objectTag;
@end

@implementation SomeClass
@dynamic objectTag;  //声明为dynamic
//添加setter实现
void dynamicSetMethod(id self,SEL _cmd,float w){
    printf("dynamicMethod-%s\n",[NSStringFromSelector(_cmd)
                                 cStringUsingEncoding:NSUTF8StringEncoding]);
    printf("%f\n",w);
    objc_setAssociatedObject(self, ObjectTagKey, [NSNumber numberWithFloat:w], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//添加getter实现
void dynamicGetMethod(id self,SEL _cmd){
    printf("dynamicMethod-%s\n",[NSStringFromSelector(_cmd)
                                 cStringUsingEncoding:NSUTF8StringEncoding]);
   [objc_getAssociatedObject(self, ObjectTagKey) floatValue];
}

//解析selector方法
+(BOOL) resolveInstanceMethod: (SEL) sel{

    NSString *methodName=NSStringFromSelector(sel);
    BOOL result=NO;
    //动态的添加setter和getter方法
    if ([methodName isEqualToString:@"setObjectTag:"]) {
        class_addMethod([self class], sel, (IMP) dynamicSetMethod,
                        "v@:f");
        result=YES;  
    }else if([methodName isEqualToString:@"objectTag"]){
        class_addMethod([self class], sel, (IMP) dynamicGetMethod,
                        "v@:f");
        result=YES;
    }
    return result;
}
  • 动态加载
    Objective-C 程序可以在运行时链接和载入新的类和范畴类。新载入的类和在程序启动时载入的类并没有区 别。
  • 消息转发
    通常,给一个对象发送它不能处理的消息会得到出错提示,然而,Objective-C 运行时系统在抛出错误之前, 会给消息接收对象发送一条特别的消息来通知该对象。forwardInvocation:消息,该消息的唯一参数是个 NSInvocation 类型的对象——该对象封装了原始的消息和消息的参数。
    所以,我们可以通过实现 forwardInvocation:方法来对不能处理的消息做一些默认的处理,也可以以其它的某种方式来避免错误被抛出。比如,它通常可以用来将消息转发给其它的对象。
    假设,您需要设计一个能够响应 negotiate 消息的对象,那么就可以通过在 negotiate 方法的实现中将 negotiate 消息转发给其它的对象来达到这一目的。
    更进一步,假设您希望您的对象和另外一个类的对象对negotiate的消息的响应完全一致。一种可能的方式就是让您的类继承其它类的方法实现。 然而,有时候这种方式不可行,因为您的类和其它类可能需要
    在不同的继承体系中响应 negotiate 消息。虽然您的类无法继承其它类的negotiate方法,您仍然可以提供一个方法实现,这个方法实现只是简单
    的将 negotiate 消息转发给其他类的对象,就好像从其它类那儿“借”来的一样。
    如下所示:
- negotiate
{
  if ( [someOtherObject respondsTo:@selector(negotiate)] )
  return [someOtherObject negotiate];
  return self;
}

以上方法的缺陷是:如果有很多消息都希望传递给其它对象时,您必须为每一种消息提供方法实现。此外,这种方式不能处理未知的消息。
解决办法就是:使用forwardInvocation:方法。
因为当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过 forwardInvocation:消息通知该对象。通过实现您自己的 forwardInvocation: 方法,您可以在该方法实现中将消息转发给其它对象。要转发消息给其它对象,forwardInvocation:方法所必须做的有:1、决定将消息转发给谁 2、将消息和原来的参数一块转发出去。转发消息后的返回值将返回给原来的消息发送者。
如下所示:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
[anInvocation selector]])
    //消息可以通过 invokeWithTarget:方法来转发
    [anInvocation invokeWithTarget:someOtherObject];
else
    [super forwardInvocation:anInvocation];
 }

所以,forwardInvocation:方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。 或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的"吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:方法也可以对不 同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。

注意: forwardInvocation:方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以, 如果您希望您的对象将 negotiate 消息转发给其它对象,您的对象不能有 negotiate 方法。否则, forwardInvocation:将不可能会被调用。

  • 消息转发和多重继承
    消息转发很象继承,并且可以用来在Objective-C程序中模拟多重继承。
    5BD4A9E5-0D9B-4DF6-A5EA-CB10F1C382E3.png

    在上图中,Warrior 类的一个对象实例将 negotiate 消息转发给 Diplomat 类的一个实例。看起来,Warrior 类似乎和 Diplomat 类一样, 响应 negotiate 消息,并且行为和 Diplomat 一样(尽管实际上是 Diplomat 类响应了该消息)。
    消息转发提供了多重继承的很多特性。然而,两者有很大的不同:多重继承是将不同的行为封装到单个的对象中,有可能导致庞大的,复杂的对象。而消息转发是将问题分解到更小的对象中,但是又以一种对消息发送对象来说完全透明的方式将这些对象联系起来。
  • 消息代理对象
    我理解为一个轻量级的对象(消息代理对象)代表更多的对象进行消息处理。
  • 消息转发和类继承
    尽管消息转发很“象”继承,但它不是继承。例如在 NSObject 类中,方法 respondsToSelector: 和 isKindOfClass:只会出现在继承链中,而不是消息转发链中。例如,如果向一个 Warrior 类的对象询问它能否响应 negotiate 消息,
if ( [aWarrior respondsToSelector:@selector(negotiate)] )

返回值是NO,尽管该对象能够接收和响应negotiate。大部分情况下,NO 是正确的响应。但不是所有时候都是的。例如,如果您使用消息转发来创建一个代理对象以扩展某个类的能力,这儿的消息转发必须和继承一样,尽可能的对用户透明。如果您希望您的代理对象看起来就象是继承自它代表的对象一样,您需要重新实现 respondsToSelector:和 isKindOfClass:方法:

- (BOOL)respondsToSelector:(SEL)aSelector
{
   if ( [super respondsToSelector:aSelector] )
         return YES;
   else {
       /* Here, test whether the aSelector message can *
       * be forwarded to another object and whether                 that *
       * object can respond to it. Return YES if it can. */
   }
   return NO;
}
除了 respondsToSelector:和 isKindOfClass:外,instancesRespondToSelector: 方法也必须重新实现。如果您使用的是协议类,需要重新实现的还有 conformsToProtocol:方法。 类似地,如果对象需要转发远程消息,则 methodSignatureForSelector:方法必须能够返回实际 响应消息的方法的描述。例如,如果对象需要将消息转发给它所代表的对象,您可能需要如下的 methodSignatureForSelector:实现:
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
     NSMethodSignature* signature = [super methodSignatureForSelector:selector];
     if (!signature) {
     signature = [surrogate methodSignatureForSelector:selector];
     }
     return signature;
}

您也可以将消息转发的部分放在一段私有的代码里,然后从 forwardInvocation:调用它。
其实,这一段我还没有摸索透。。。

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

推荐阅读更多精彩内容