昨天逛简书的时候在一篇文章的评论下面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的定义:
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);
以下是打印出来的结果:
两个IMP指针的地址又不一样了。这是要搞晕宝宝吗?
思前想后,我觉的问题还是出在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里面找到了这个函数的定义:
这句话的意思是说,如果消息的接收者不能响应该消息,使用这个函数去转发该消息。这就进一步印证了我们的猜想,看来class_getMethodImplementation根本就没有查找到imageNamed:和ln_imageNamed:这两个方法,所以才返回了_objc_msgForward函数,这两个函数的指针肯定是一样的。有人可能会说不可能啊,imageNamed:是系统的方法,怎么可能会查不到?而且我还是在load里面打印的这两个方法的IMP,会不会是类还没有加载完?有兴趣的同学可以自己去试一下,不管你在哪个地方打印,都会是这个结果,而且我可以明确告诉你,imageNamed:这个方法,确实在UIImage类的方法列表里面查不到。
那么为什么在UIImage类的方法列表里面查询不到呢?我们调用这个方法的时候又是从哪里查询到的呢?答案是元类。
在OC中,所有的类都是一种对象,反过来,对象也都有其对应的类。我们平时代码中所创建的对象比如说一个view,它的类就是UIView。UIView实际上也是一个对象,叫做类对象,那么类对象的类又是什么呢?就是元类。元类也是一个对象,它的类又是什么呢?根元类。根元类也是一个对象,它的类又是什么呢?它自己。这些我们在下面都可以验证。
为了方便我们理解,这里有一张引用自网上的图片来更直观的显示他们之间的层级关系。
实线代表继承关系,虚线代表类与对象的关系。对象是类的实例,类是元类的实例,元类是根元类的实例,根元类的类是自己。子类继承自父类,父类继承自根类,根类的父类为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")));
以下是打印结果:
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);
}
下面是打印结果:
可以看到这次打印的两个IMP的地址不相同了,并且与我们从Method里面取到的IMP的地址一一对应。这也进一步印证了类方法存储在元类的方法列表里面的说法。
最后,再提供一个例子来给大家练练手:
这段代码的运行结果是什么?会崩溃吗?