由class_getMethodImplementation函数引发的思考

昨天逛简书的时候在一篇文章的评论下面get到一个挺有意思的问题,也没有见到楼主或其它简友给出合理的解释,细想之后遂对其产生了浓烈的兴趣。下面贴出这位简友所提的问题:

    + (void)load{
         Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
         Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
         IMP imageNameIMP = class_getMethodImplementation(self, @selector(imageNamed:));
         IMP ln_imageNameIMP = class_getMethodImplementation(self, @selector(ln_imageNamed:));
         NSLog(@"imageNameIMP = %p",imageNameIMP);
         NSLog(@"ln_imageNameIMP = %p",ln_imageNameIMP);
    }  

//以下是打印日志

 (lldb) po imageNamedMethod
 0x000000010eb16238

 (lldb) po ln_imageNamedMethod
 0x000000010cc69250

 (lldb) po imageNameIMP
 (libobjc.A.dylib`_objc_msgForward)

 (lldb) po ln_imageNameIMP
 (libobjc.A.dylib`_objc_msgForward)

 Printing description of imageNameIMP:
 (IMP) imageNameIMP = 0x000000010d24c740 
 (libobjc.A.dylib`_objc_msgForward)
 Printing description of ln_imageNameIMP:
 (IMP) ln_imageNameIMP = 0x000000010d24c740 
 (libobjc.A.dylib`_objc_msgForward)
 (lldb) 

问题:两个函数对应的两个selector的地址是不同的,这里没问题。但是两个selector对应的IMP(函数实现)为什么是一样的呢?(需要额外补充一下,这段代码原本是运行时里面实现方法交换的一个demo,这段代码是写在为UIImage创建的一个分类里面的,load方法会在加载类的时候就调用,另外ln_imageNamed是一个类方法,也已经在分类中实现)

这个问题需要对Runtime知识有一定的了解,不过也没关系,这段代码并不复杂,其中的类和方法我们大概知道什么意思就能看懂。Method类实际上是一个objc_method类型的指针,代表着类定义的一个方法。objc_method是一个struct类型,里面包含了三种数据类型:SEL、IMP和char*。下面是runtime.h文件里面对objc_method的定义:

image.png

SEL本质上来说是一个字符串,实际上就是方法名字去掉返回值和参数之后剩下的东西。char *包含的是方法的返回值和参数类型信息。IMP是一个函数指针,指向的是一个方法的具体的函数实现。class_getClassMethod可以获取到一个类的类方法的Method,class_getMethodImplementation可以获取到一个方法的IMP。看到这里,我们知道了Method类型里面是包含有一个方法的IMP信息的。那么如果我不通过 class_getMethodImplementation去取IMP呢,我从Method里面去取的话,两个IMP的地址会不会是一样的呢?然后我看了一下runtime.h文件里面的内容,找到下面一个函数method_getImplementation,看命名方式应该是从Method类型里面取出IMP的一个函数。

 IMP imageNameIMP_method = method_getImplementation(imageNameMethod);
 IMP ln_imageNamedIMP_method = method_getImplementation(ln_imageNamedMethod);
 NSLog(@"imageNameIMP_method = %p",imageNameIMP_method);
 NSLog(@"ln_imageNamedIMP_method = %p",ln_imageNamedIMP_method);

以下是打印出来的结果:

image.png

两个IMP指针的地址又不一样了。这是要搞晕宝宝吗?

image.png

思前想后,我觉的问题还是出在class_getMethodImplementation这个方法身上,然后查苹果官方文档,上面是这么描述这个方法的:

Returns the function pointer that would be called if a particular message were sent to an instance of a class.
class_getMethodImplementation may be faster than method_getImplementation(class_getInstanceMethod(cls, name)).

如果向一个类的实例发送一条消息,该函数会返回该条消息的IMP。class_getMethodImplementation可能比method_getImplementation更高效。

The function pointer returned may be a function internal to the runtime instead of an actual method implementation. For example, if instances of the class do not respond to the selector, the function pointer returned will be part of the runtime's message forwarding machinery.

返回的指针可能会是一个方法的IMP,也可能是runtime内部的一个函数。比如说,如果一个类的对象不能响应一个selector,这个函数指针返回的就会是runtime里面消息转发机制的一部分。

这段说明提到了重要的一点,就是说如果一个类没有实现我们要查找的方法,这个函数返回的就是runtime里面的某个函数。如果是这样的话,那么打印的两个IMP相同倒有点可能性了。而且上面我们也打印了两个IMP,都是一个叫做_objc_msgForward的函数。这个函数又是干什么的呢,我在message.h里面找到了这个函数的定义:

image.png

这句话的意思是说,如果消息的接收者不能响应该消息,使用这个函数去转发该消息。这就进一步印证了我们的猜想,看来class_getMethodImplementation根本就没有查找到imageNamed:和ln_imageNamed:这两个方法,所以才返回了_objc_msgForward函数,这两个函数的指针肯定是一样的。有人可能会说不可能啊,imageNamed:是系统的方法,怎么可能会查不到?而且我还是在load里面打印的这两个方法的IMP,会不会是类还没有加载完?有兴趣的同学可以自己去试一下,不管你在哪个地方打印,都会是这个结果,而且我可以明确告诉你,imageNamed:这个方法,确实在UIImage类的方法列表里面查不到。

那么为什么在UIImage类的方法列表里面查询不到呢?我们调用这个方法的时候又是从哪里查询到的呢?答案是元类。

在OC中,所有的类都是一种对象,反过来,对象也都有其对应的类。我们平时代码中所创建的对象比如说一个view,它的类就是UIView。UIView实际上也是一个对象,叫做类对象,那么类对象的类又是什么呢?就是元类。元类也是一个对象,它的类又是什么呢?根元类。根元类也是一个对象,它的类又是什么呢?它自己。这些我们在下面都可以验证。

为了方便我们理解,这里有一张引用自网上的图片来更直观的显示他们之间的层级关系。

1361289384_8487.png

实线代表继承关系,虚线代表类与对象的关系。对象是类的实例,类是元类的实例,元类是根元类的实例,根元类的类是自己。子类继承自父类,父类继承自根类,根类的父类为nil。子元类继承自父元类,父元类继承自根元类,根元类的父类是根类。

每个类对象都有对应的元类,每个类(根类除外)都有一个superclass,同样每个元类也有一个superclass,并且子类与子元类、父类与父元类分别在同一层次。

怎么验证元类的存在和上面所描述的层级关系呢?
这里需要用到两个函数:Class objc_getMetaClass(const char* name)和Class class_getSuperclass(id obj),第一个可以获取一个类的元类,第二个可以获取一个类的父类。这里为了方便,我们直接用NSObject来验证,因为NSObject是OC的根类,继承关系没那么复杂。

 NSLog(@"NSObject的地址是 %p", [NSObject class]);
 NSLog(@"NSObject的元类的地址--根元类的地址是 %p", objc_getMetaClass("NSObject"));
 NSLog(@"根元类的元类的地址是 %p", objc_getMetaClass([NSStringFromClass(objc_getMetaClass("NSObject")) UTF8String]));
 NSLog(@"根元类的父类的地址是 %p", class_getSuperclass(objc_getMetaClass("NSObject")));

以下是打印结果:

image.png

NSObject的元类地址不为空,可以证明元类确实存在。根元类的元类的地址和根元类的地址相同,可以证明根元类的类是自己。根元类的父类的地址和NSObject的地址相同,证明根元类的父类是根类。有兴趣的同学也可以用OC中其它的类去验证这种层级关系,不过可能需要多取几次。

那么实例对象、类对象、元类对象到底有什么区别呢?

实例对象:实例对象拷贝了实例所属的类的成员变量,但不拷贝类定义的方法。当调用实例方法时,需要到类的方法列表里面去寻找方法的函数指针。

类对象:是一个功能完整的对象,它没有自己的实例变量。(这里要与类的成员变量区别开来,类的成员变量是属于实例对象的,而不是属于类对象的。类方法才是属于类对象自己的)。类对象中存储着成员变量和实例方法列表。

元类对象:OC的类方法是元类存在的根本原因。因为元类对象中存储着类对象调用的方法也就是类方法。元类的定义和创建都是编译器自动完成的,无需人为干涉,而且大部分时候都是倾向于隐藏的。

现在我们再来看一看imageNamed:这个方法,这是一个类方法,而class_getMethodImplementation我们传的参数里面的Class却是UIImage。在类的方法列表里面查找类方法,肯定是查询不到的,我们应该在UIImage的元类里面查询。(关于方法查询这方面的知识,涉及到OC的消息机制和消息转发,我的另一篇文章有详细的讲到。)
现在我们来对代码进行一部分修改:

+ (void)load{
    Method imageNameMethod = class_getClassMethod(self, @selector(imageNamed:));
    Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
    IMP imageNameIMP = class_getMethodImplementation(objc_getMetaClass("UIImage"), @selector(imageNamed:));
    IMP ln_imageNamedIMP = class_getMethodImplementation(objc_getMetaClass("UIImage"), @selector(ln_imageNamed:));
    IMP imageNameIMP_method = method_getImplementation(imageNameMethod);
    IMP ln_imageNamedIMP_method = method_getImplementation(ln_imageNamedMethod);
    NSLog(@"imageNameIMP_method = %p",imageNameIMP_method);
    NSLog(@"ln_imageNamedIMP_method = %p",ln_imageNamedIMP_method);
    NSLog(@"imageNameIMP = %p",imageNameIMP);
    NSLog(@"ln_imageNamedIMP = %p",ln_imageNamedIMP);
}

下面是打印结果:

image.png

可以看到这次打印的两个IMP的地址不相同了,并且与我们从Method里面取到的IMP的地址一一对应。这也进一步印证了类方法存储在元类的方法列表里面的说法。

最后,再提供一个例子来给大家练练手:

image.png

这段代码的运行结果是什么?会崩溃吗?

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,690评论 0 9
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,182评论 0 7
  • Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理。这种动态语言的...
    有一种再见叫青春阅读 579评论 0 3
  • Objective-C语言是一门动态语言,他将很多静态语言在编译和链接时期做的事情放到了运行时来处理。这种动态语言...
    tigger丨阅读 1,388评论 0 8
  • 我很丑 感觉有些难过 幸好还有些萌 这么想着又变得有些开心。
    十玖阅读 116评论 0 0