读过《Runtime的初步认识——结构体与类》的小伙伴们应该对objc_class结构体的构造有所了解了
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
在这里,我们可以找到
实例变量struct objc_ivar_list *ivars
,
方法列表struct objc_method_list **methodLists
,
缓存方法列表struct objc_cache *cache
。
在这里插一嘴。我们之前在《Runtime的初步认识——消息机制》中介绍过,在Objective-C里面调用一个方法的时候,runtime层会将这个调用翻译成
objc_msgSend(id self, SEL op, ...)
而objc_msgSend
具体有事如何分发的呢?我们来看下runtime层objc_msgSend
的源码。(runtime的源代码可以在 http://opensource.apple.com//tarballs/objc4/ 下载)
/********************************************************************
*
* id objc_msgSend(id self, SEL _cmd,...);
*
********************************************************************/
ENTRY _objc_msgSend
MESSENGER_START
CALL_MCOUNTER
// load receiver and selector
movl selector(%esp), %ecx
movl self(%esp), %eax
// check whether selector is ignored
cmpl $ kIgnore, %ecx
je LMsgSendDone // return self from %eax
// check whether receiver is nil
testl %eax, %eax
je LMsgSendNilSelf
// receiver (in %eax) is non-nil: search the cache
LMsgSendReceiverOk:
movl isa(%eax), %edx // class = self->isa
CacheLookup WORD_RETURN, MSG_SEND, LMsgSendCacheMiss
xor %edx, %edx // set nonstret for msgForward_internal
MESSENGER_END_FAST
jmp *%eax
// cache miss: go search the method lists
LMsgSendCacheMiss:
MethodTableLookup WORD_RETURN, MSG_SEND
xor %edx, %edx // set nonstret for msgForward_internal
jmp *%eax // goto *imp
// message sent to nil: redirect to nil receiver, if any
LMsgSendNilSelf:
// %eax is already zero
movl $0,%edx
xorps %xmm0, %xmm0
LMsgSendDone:
MESSENGER_END_NIL
ret
// guaranteed non-nil entry point (disabled for now)
// .globl _objc_msgSendNonNil
// _objc_msgSendNonNil:
// movl self(%esp), %eax
// jmp LMsgSendReceiverOk
LMsgSendExit:
END_ENTRY _objc_msgSend
这TMD什么鬼??? 保证看完你是这个反应。。。Apple为了高度优化这些方法的性能,这些方法都是汇编写成的。不过虽然我们看不懂汇编。但是通过注释我们也能了解消息分发的大概逻辑了。(这个地方我们可以先简单了解到这,感兴趣的可以继续研究,一起分享交流)接下来我们切回正题。
当我们向对象发送消息的时候,OC会到缓存方法列表中开始找方法的指针,如果缓存列表中找不到,就会到方法列表中找,如果本类的方法列表中找不到,就会到父类里面找,直到找到方法的指针或者最终的父类NSObject
也找不到方法的指针为止。当找不到方法指针的时候,编译器会发出[XXXX 某方法]unrecognized selector sent to instance 0x100400d90
的警告。
当找到方法指针的时候,OC会将会在内存中找到方法指针所指向的那个代码块,并运行它。
我们知道,程序之所以能运行,是因为方法和变量都是存在程序的内存中。所以如果我们改变了方法指针指针所指向的内存地址的内容或者直接改变了方法指针指向的地址,我们就可以改变了方法的实现。
Runtime中的方法交换
Runtime给了我们一个函数来实现方法交换,你只需要导入objc/Runtime.h
文件即可使用这个函数。
这个函数是
/**
* Exchanges the implementations of two methods.
* 交换两个方法的实现
*
* @param m1 Method to exchange with second method.
* @param m2 Method to exchange with first method.
*
* @note This is an atomic version of the following:
* 这个函数的实现如下:
* \code
* IMP imp1 = method_getImplementation(m1);
* IMP imp2 = method_getImplementation(m2);
* method_setImplementation(m1, imp2);
* method_setImplementation(m2, imp1);
* \endcode
*/
void method_exchangeImplementations(Method m1, Method m2);
这个方法的注释官方已给出, 我们只要关注他的参数以及返回值(由于返回值为void, 所以在此没有多余的解释, 主要解释两个参数)。
Method
是Objective-C语言中的一个结构体, 在runtime.h
头文件中有定义. 在这个函数中, Method
顾名思义就是要交换的方法. 我们可以通过下面这个函数来获取一个类的Method
。
Method class_getInstanceMethod(Class cls, SEL name);
现在这两个参数是我们平时看的见的参数。综上所述,我们只要将两组要交换的方法的SEL
和该方法所在的Class
传入进去即可实现方法交换。
由此最终的代码会变成:
Method m1 = class_getInstanceMethod([M1 class], @selector(method1name));
Method m2 = class_getInstanceMethod([M2 class], @selector(method2name));
method_exchangeImplementations(m1, m2);
如果你不知道方法交换的最终效果,现在我们用一个很简单的例子来说明这个问题。
比如我们现在有两个类的文件,每个类都有自己的方法和实现。
@interface classOne : NSObject
@end
@implementation classOne()
- (void)methodOne {
NSLog(@"methodOne");
}
@end
@interface classTwo : NSObject
@end
@implementation classTwo()
- (void)methodTwo {
NSLog(@"methodTwo");
}
@end
正常情况下
如果我们调用[[classOne new] methodOne]
则会输出methodOne
。
同理如果调用[[classTwo new] methodTwo]
则会输出methodTwo
。
但是如果我们在某一个时刻执行了一次
下面的代码
Method method1 = class_getInstanceMethod([classTwo class], @selector(methodTwo));
Method method2 = class_getInstanceMethod([classOne class], @selector(methodOne));
method_exchangeImplementations(method1, method2);
在此之后(直到程序结束前),我们运行[[classOne new] methodOne]
的时候,打印的是methodTwo
。
这个就是runtime的黑科技,慎用~
这个就是runtime的黑科技,慎用~
这个就是runtime的黑科技,慎用~
通过以上内容, 你应该可以深刻体会到OC为什么是一个有运行时特色的语言了
还有个问题就是我们到底应该在什么地方调用切换方法的代码呢?
我们还要了解每个类都有个load
方法,这个方法是类加载到内存是调用的,所以我们可以在任意一个类的load
方法里写这个函数。