前言
runtime其实在我们日常开发过程中很少使用到,尤其是像我现在比较初级的程序猿就更用不到了。但是去面试很多面试官都会问这个问题,这就很尴尬了,这就有一种装逼的感觉,问这些对于现在的我们根本用不上的东西,要是很懂,那我们完全就是大牛了嘛。但是,话虽如此,runtime是object-c底层实现核心,如果我们明白了,对于以后我们解决问题,理解问题有很大的帮助,所以出于什么心态我们都应该有必要去了解,甚至在一定时候去深入理解它(这个就是以后你欲求不满,追求技术的最高境界时~~~)。
思考
1 runtime时候干吗的?
2 为什么要有runtime?
开始了解runtime
简单的理解:
众所周知,简单而高深的C语言是面向过程的编程语言,程序是按顺序依次执行,一直到结束。而在这就为了便于说明就不得不提c++这门语言,c++和object-c两门语言都思想差不多,都是面向对象编程的,都是基于C语言加入面向对象的思想,但是c++和object-c在实现机制上有很大的差别,C++是一门静态语言,简而言之就是在编译的时候编译器就已经把函数的地址编码进可执行文件了,这个时候函数调用就知道调用那段代码了。而object-c和C++是相反的,object-c是一门动态语言,编译器无法将函数的地址编码进可执行文件的,所以不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发,而是在运行的时候根据runtime条件判断,再去找相应的调用的那段代码。
总结:
1 Objective-C Runtime是一个将C语言转化为面向对象语言的扩展。
2 Objective-C是基于C语言加入了面向对象特性和消息转发机制的动态语言,这意味着它不仅需要一个编译器,还需要Runtime系统来动态创建类和对象,进行消息发送和转发。
3 [receiver message]真的不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接收者发送message这条消息,而receive将要如何响应这条消息,那就要看运行时发生的情况来决定了。
4 因为Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。
与Runtime交互
Objc 从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码,通过 Foundation 框架的NSObject类定义的方法,通过对 runtime 函数的直接调用。
-
Objective-C源代码
大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。
还记得引言中举的例子吧,消息的执行会使用到一些编译器为实现动态语言特性而创建的数据结构和函数,Objc中的类、方法和协议等在 runtime 中都由一些数据结构来定义,这些内容在后面会讲到。(比如objc_msgSend函数及其参数列表中的id和SEL都是啥) -
NSObject的方法
Cocoa 中大多数类都继承于NSObject类,也就自然继承了它的方法。最特殊的例外是NSProxy,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类,说白了就是领导把自己展现给大家风光无限,但是把活儿都交给幕后小弟去干。
有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时获得类的信息,并检查一些特性,比如class返回对象的类;isKindOfClass:和isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。 -
Runtime的函数
Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。在Objective-C Runtime Reference中有对 Runtime 函数的详细文档。
runtime术语了解
id objc_msgSend ( id self, SEL op, ... );
这是个最基本的用于发送消息的函数。
其实编译器会根据情况在objc_msgSend, objc_msgSend_stret,,objc_msgSendSuper, 或 objc_msgSendSuper_stret 四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有 Super 的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有stret的函数。
例子:
objc_msgSend(person,@selector(sayHello));
简单过程如下:
第一个参数是要发送消息的实例,也就是 person 对象。 objc_msgSend 会先查询它的 methodLists 方法列表,使用第二个参数 sayHello 逐个和 person 的 methodLists 中的每一个方法信息的 SEL 进行对比,如果找到对应的方法,就调用它所对应的函数,也就是 IMP,然后调用这个函数。
-
id和Class
objc_msgSend
第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:
typedef struct objc_object *id;
那objc_object又是啥呢:
struct objc_object { Class isa; };
Class是一个指向objc_class
结构体的指针,而id是一个指向objc_object结构体的指针,其中的isa是一个指向objc_class
结构体的指针。其中的id就是我们所说的对象,Class就是我们所说的类。
PS:isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的isa指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术.
objc_class定义如下:
typedef struct objc_class *Class;
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; // metaclass
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0,可以通过runtime函数class_setVersion或者class_getVersion进行修改、读取
long info OBJC2_UNAVAILABLE; // 类信息,供运行时期使用的一些位标识,如CLS_CLASS (0x1L) 表示该类为普通 class,其中包含实例方法和变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小(包括从父类继承下来的实例变量)
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量地址列表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法地址列表,与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储实例方法,如CLS_META (0x2L),则存储类方法;
struct objc_cache *cache OBJC2_UNAVAILABLE; // 缓存最近使用的方法地址,用于提升效率;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 存储该类声明遵守的协议的列表
#endif
}
/* Use `Class` instead of `struct objc_class *` */
不知道你是否注意到了objc_class
中也有一个isa对象,这是因为一个 ObjC 类本身同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。当你发出一个类似[NSObject alloc]
的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当 [NSObject alloc]
这条消息发给类对象的时候,objc_msgSend()
会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。
由以上代码可见,类与对象的区别就是类比对象多了很多特征成员,类也可以当做一个objc_object
来对待,也就是说类和对象都是对象,分别称作类对象(class object)和实例对象(instance object),这样我们就可以区别对象和类了。
isa:objc_object
(实例对象)中isa指针指向的类结构称为class(也就是该对象所属的类)其中存放着普通成员变量与动态方法(“-”开头的方法);此处isa指针指向的类结构称为metaclass,其中存放着static类型的成员变量与static类型的方法(“+”开头的方法)。
super_class: 指向该类的父类的指针,如果该类是根类(如NSObject或NSProxy),那么super_class就为nil。
类与对象的继承层次关系如下图:
-
SEL
objc_msgSend
函数第二个参数类型为SEL,它是selector在Objc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:
typedef struct objc_selector *SEL;
objc_selector的定义如下:
struct objc_selector {
char *name; OBJC2_UNAVAILABLE;// 名称
char *types; OBJC2_UNAVAILABLE;// 类型
};
其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。
不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是 Objc 中方法命名有时会带上参数类型(NSNumber一堆抽象工厂方法拿走不谢),Cocoa 中有好多长长的方法哦。
如果你知道selector对应的方法名是什么,可以通过NSString* NSStringFromSelector(SEL aSelector)方法将SEL转化为字符串,再用NSLog打印。
-
Method
Method是一种代表类中的某个方法的类型。
typedef struct objc_method *Method;
而objc_method在上面的方法列表中提到过,它存储了方法名,方法类型和方法实现:
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
方法名类型为SEL
,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
方法类型method_types
是个char指针,其实存储着方法的参数类型和返回值类型。
最后一个参数是 IMP 类型,它表示这个 Selector 对应的函数的地址。 对,是函数没错。 Objective-C 中定义的所有类的方法在底层实现上就是一个函数。
- methodLists
struct objc_method_list **methodLists
methodLists 属性表示当前实例的方法列表,它是一个 objc_method_list 类型的结构:
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
这个结构的定义可能大家会有些地方不太明白,比如 space 属性是干什么的。咱们可以暂时抛开这些问题,只关心和消息分发相关的属性 - method_count 属性表示当前这个实例中方法的个数,method_list 结构表示当前实例上面所有方法的列表:
struct objc_method method_list[1]
-
Ivar
Ivar是一种代表类中实例变量的类型。
typedef struct objc_ivar *Ivar;
objc_ivar的数据结构如下:
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE; // 变量名
char *ivar_type OBJC2_UNAVAILABLE; // 变量类型
int ivar_offset OBJC2_UNAVAILABLE; // 基地址偏移字节
#ifdef __LP64__
int space OBJC2_UNAVAILABLE; // 占用空间
#endif
}
可以根据实例查找其在类中的名字,也就是“反射”:
-(NSString *)nameWithInstance:(id)instance {
unsigned int numIvars = 0;
NSString *key=nil;
Ivar * ivars = class_copyIvarList([self class], &numIvars);
for(int i = 0; i < numIvars; i++) {
Ivar thisIvar = ivars[i];
const char *type = ivar_getTypeEncoding(thisIvar);
NSString *stringType = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
if (![stringType hasPrefix:@"@"]) {
continue;
}
if ((object_getIvar(self, thisIvar) == instance)) {//此处若 crash 不要慌!
key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
break;
}
}
free(ivars);
return key;
}
-
IMP
IMP
在objc.h
中的定义是:
typedef id (*IMP)(id, SEL, ...);
它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面会提到。
你会发现IMP指向的方法与
objc_msgSend
函数类型相同,参数都包含id和SEL类型。每个方法名都对应一个SEL类型的方法选择器,而每个实例对象中的SEL对应的方法实现肯定是唯一的,通过一组id和SEL参数就能确定唯一的方法实现地址;反之亦然。
-
Cache
在runtime.h中Cache的定义如下:
typedef struct objc_cache *Cache
还记得之前objc_class结构体中有一个struct objc_cache *cache吧,它到底是缓存啥的呢,先看看objc_cache的实现:
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
Cache为方法调用的性能进行优化,通俗地讲,每当实例对象接收到一个消息时,它不会直接在isa指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了,而是优先在Cache中查找。Runtime 系统会把被调用的方法存到Cache中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高。这根计算机组成原理中学过的 CPU 绕过主存先访问Cache的道理挺像,而我猜苹果为提高Cache命中率应该也做了努力吧。
-
Catagory
这个就是我们平时所说的类别了,很熟悉吧。它可以动态的为已存在的类添加新的方法。
它的定义如下:
typedef struct objc_category *Category;
objc_category的定义如下:
struct objc_category {
char *category_name OBJC2_UNAVAILABLE; // 类别名称
char *class_name OBJC2_UNAVAILABLE; // 类名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 实例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 类方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议列表
}
-
objc_property_t
objc_property_t是属性,它的定义如下:
typedef struct objc_property *objc_property_t;
objc_property是内置的类型,与之关联的还有一个objc_property_attribute_t,它是属性的attribute,也就是其实是对属性的详细描述,包括属性名称、属性编码类型、原子类型/非原子类型等。它的定义如下:
typedef struct {
const char *name; // 名称
const char *value; // 值(通常是空的)
} objc_property_attribute_t;
消息
Objc 中发送消息是用中括号([])把接收者和消息括起来,而直到运行时才会把消息与方法实现绑定。消息发送和转发流程可以概括为:消息发送(Messaging)是 Runtime 通过 selector 快速查找 IMP 的过程,有了函数指针就可以执行对应的方法实现;消息转发(Message Forwarding)是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发处理,则会打日志和抛出异常。
objc_msgSend函数
此函数是消息发送必经之路,但只要一提 objc_msgSend
,都会说它的伪代码如下或类似的逻辑,反正就是获取 IMP 并调用:
id objc_msgSend(id self, SEL _cmd, ...) {
Class class = object_getClass(self);
IMP imp = class_getMethodImplementation(class, _cmd);
return imp ? imp(self, _cmd, ...) : 0;
}
-
objc_msgSend汇编语言编写
当需要发送消息时,编译器会生成中间代码,根据情况分别调用objc_msgSend
,objc_msgSend_stret
,objc_msgSendSuper
, 或objc_msgSendSuper_stret
其中之一。
这也是为什么`` objc_msgSend``` 要用汇编语言而不是 OC、C 或 C++ 语言来实现,因为单独一个方法定义满足不了多种类型返回值,有的方法返回 id,有的返回 int。考虑到不同类型参数返回值排列组合映射不同方法签名(method signature)的问题,那 switch 语句得老长了。。。这些原因可以总结为 Calling Convention,也就是说函数调用者与被调用者必须约定好参数与返回值在不同架构处理器上的存取规则,比如参数是以何种顺序存储在栈上,或是存储在哪些寄存器上。除此之外还有其他原因,比如其可变参数用汇编处理起来最方便,因为找到 IMP 地址后参数都在栈上。要是用 C++ 传递可变参数那就悲剧了,prologue 机制会弄乱地址(比如 i386 上为了存储 ebp 向后移位 4byte),最后还要用 epilogue 打扫战场。而且汇编程序执行效率高,在 Objective-C Runtime 中调用频率较高的函数好多都用汇编写的。
lookUpImpOrForward函数
使用 lookUpImpOrForward 快速查找 IMP。
其实_class_lookupMethodAndLoadCache3
函数(objc_msgSend汇编语言中查找IMP的函数)其实只是简单的调用了 lookUpImpOrForward
函数:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
注意lookUpImpOrForward
调用时使用缓存参数传入为 NO,因为之前已经尝试过查找缓存了。IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)
实现了一套查找 IMP 的标准路径,也就是在消息转发(Forward)之前的逻辑。
消息发送
1 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。
2 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
3 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
4 如果 cache 找不到就找一下方法分发表。
5 如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
6 如果还找不到就要开始进入动态方法解析了,后面会提到。
PS:这里说的分发表其实就是Class中的方法列表,它将方法选择器和方法实现地址联系起来。
其实编译器会根据情况在
objc_msgSend, objc_msgSend_stret
, objc_msgSendSuper
, 或objc_msgSendSuper_stret
四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有”Super”的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。排列组合正好四个方法。
值得一提的是在 i386 平台处理返回类型为浮点数的消息时,需要用到objc_msgSend_fpret
函数来进行处理,这是因为返回类型为浮点数的函数对应的 ABI(Application Binary Interface) 与返回整型的函数的 ABI 不兼容。此时objc_msgSend
不再适用,于是objc_msgSend_fpret
被派上用场,它会对浮点数寄存器做特殊处理。不过在 PPC 或 PPC64 平台是不需要麻烦它的。
PS:有木有发现这些函数的命名规律哦?带“Super”的是消息传递给超类;“stret”可分为“st”+“ret”两部分,分别代表“struct”和“return”;“fpret”就是“fp”+“ret”,分别代表“floating-point”和“return”。
方法中的隐藏参数
我们经常在方法中使用self关键字来引用实例本身,但从没有想过为什么self就能取到调用当前方法的对象吧。其实self的内容是在方法运行时被偷偷的动态传入的。
当objc_msgSend
找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:
接收消息的对象(也就是self
指向的内容)
方法选择器(_cmd
指向的内容)
如:
[person sayHello];
/*
最后调用sayHello时,是sayHello(person,@selector(sayHello));这样
*/
之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。在下面的例子中,self
引用了接收者对象,而_cmd
引用了方法本身的选择器:
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
在这两个参数中,self 更有用。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。
而当方法中的super关键字接收到消息时,编译器会创建一个objc_super
结构体:
struct objc_super { id receiver; Class class; };
这个结构体指明了消息应该被传递给特定超类的定义。但receiver仍然是self本身,这点需要注意,因为当我们想通过[super class]
获取超类时,编译器只是将指向self的id指针和class的SEL传递给了objc_msgSendSuper
函数,因为只有在NSObject类才能找到class方法,然后class方法调用object_getClass()
,接着调用objc_msgSend(objc_super->receiver, @selector(class))
,传入的第一个参数是指向self的id指针,与调用[self class]相同,所以我们得到的永远都是self的类型。
获取方法地址
在IMP那节提到过可以避开消息绑定而直接获取方法的地址并调用方法。这种做法很少用,除非是需要持续大量重复调用某方法的极端情况,避开消息发送泛滥而直接调用该方法会更高效。
NSObject类中有个methodForSelector:
实例方法,你可以用它来获取某个方法选择器对应的IMP,举个栗子:
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);
当方法被当做函数调用时,上节提到的两个隐藏参数就需要我们明确给出了。上面的例子调用了1000次函数,你可以试试直接给target发送1000次setFilled:消息会花多久。
PS:methodForSelector:
方法是由 Cocoa 的 Runtime 系统提供的,而不是 Objc 自身的特性。
动态方法解析
你可以动态地提供一个方法的实现。例如我们可以用@dynamic
关键字在类的实现文件中修饰一个属性:
@dynamic propertyName;
这表明我们会为这个属性动态提供存取方法,也就是说编译器不会再默认为我们生成setPropertyName:
和propertyName
方法,而需要我们动态提供。我们可以通过分别重载resolveInstanceMethod:
和resolveClassMethod:
方法分别添加实例方法实现和类方法实现。因为当 Runtime 系统在Cache和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:
或resolveClassMethod:
来给程序员一次动态添加方法实现的机会。我们需要用class_addMethod函数完成向特定类添加特定方法实现的操作:
ynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
上面的例子为resolveThisMethodDynamically
方法添加了实现内容,也就是dynamicMethodIMP
方法中的代码。其中 “v@:” 表示返回值和参数,这个符号涉及 Type Encoding
PS:动态方法解析会在消息转发机制浸入前执行。如果 respondsToSelector:
或instancesRespondToSelector:
方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的IMP的机会。如果你想让该方法选择器被传送到转发机制,那么就让resolveInstanceMethod:
返回NO。
评论区有人问如何用 resolveClassMethod:
解析类方法,我将他贴出有问题的代码做了纠正和优化后如下,可以顺便将实例方法和类方法的动态方法解析对比下:
头文件:
#import <Foundation/Foundation.h>
@interface Student : NSObject
+ (void)learnClass:(NSString *) string;
- (void)goToSchool:(NSString *) name;
@end
m 文件:
#import "Student.h"
#import <objc/runtime.h>
@implementation Student
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(learnClass:)) {
class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "v@:");
return YES;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(goToSchool:)) {
class_addMethod([self class], aSEL, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
+ (void)myClassMethod:(NSString *)string {
NSLog(@"myClassMethod = %@", string);
}
- (void)myInstanceMethod:(NSString *)string {
NSLog(@"myInstanceMethod = %@", string);
}
@end
需要深刻理解[self class]
与 object_getClass(self)
甚至 object_getClass([self class])
的关系,其实并不难,重点在于 self 的类型:
当 self 为实例对象时,[self class]
与 object_getClass(self)
等价,因为前者会调用后者。object_getClass([self class])
得到元类。
当 self 为类对象时,[self class]
返回值为自身,还是 self。object_getClass(self)
与 object_getClass([self class])
等价。
凡是涉及到类方法时,一定要弄清楚元类、selector、IMP 等概念,这样才能做到举一反三,随机应变。
消息转发
重定向()
在消息转发机制执行前,Runtime 系统会再给我们一次偷梁换柱的机会,即通过重载- (id)forwardingTargetForSelector:(SEL)aSelector
方法替换消息的接受者为其他对象:
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
毕竟消息转发要耗费更多时间,抓住这次机会将消息重定向给别人是个不错的选择,不过千万别返回self,因为那样会死循环。 如果此方法返回nil或self,则会进入消息转发机制(forwardInvocation:
);否则将向返回的对象重新发送消息。
如果想替换类方法的接受者,需要覆写+ (id)forwardingTargetForSelector:(SEL)aSelector
方法,并返回类对象:
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if(aSelector == @selector(xxx)) {
return NSClassFromString(@"Class name");
}
return [super forwardingTargetForSelector:aSelector];
}
转发
当动态方法解析不作处理返回NO时,消息转发机制会被触发。在这时forwardInvocation:
方法会被执行,我们可以重写这个方法来定义我们的转发逻辑:
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
该消息的唯一参数是个NSInvocation类型的对象——该对象封装了原始的消息和消息的参数。我们可以实现forwardInvocation:
方法来对不能处理的消息做一些默认的处理,也可以将消息转发给其他对象来处理,而不抛出错误。
这里需要注意的是参数anInvocation是从哪的来的呢?其实在forwardInvocation:
消息发送前,Runtime系统会向对象发送methodSignatureForSelector:
消息,并取到返回的方法签名用于生成NSInvocation对象。所以我们在重写forwardInvocation:
的同时也要重写methodSignatureForSelector:方法,否则会抛异常。
当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过forwardInvocation:
消息通知该对象。每个对象都从NSObject类中继承了forwardInvocation:
方法。然而,NSObject中的方法实现只是简单地调用了doesNotRecognizeSelector:
。通过实现我们自己的forwardInvocation:
方法,我们可以在该方法实现中将消息转发给其它对象。
forwardInvocation:方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:
方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。
注意: forwardInvocation:
方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以,如果我们希望一个对象将negotiate消息转发给其它对象,则这个对象不能有negotiate方法。否则,forwardInvocation:
将不可能会被调用。
Method Swizzling
之前所说的消息转发虽然功能强大,但需要我们了解并且能更改对应类的源代码,因为我们需要实现自己的转发逻辑。当我们无法触碰到某个类的源代码,却想更改这个类某个方法的实现时,该怎么办呢?可能继承类并重写方法是一种想法,但是有时无法达到目的。这里介绍的是 Method Swizzling ,它通过重新映射方法对应的实现来达到“偷天换日”的目的。跟消息转发相比,Method Swizzling 的做法更为隐蔽,甚至有些冒险,也增大了debug的难度。
交换方法的几种实现方式
- 利用 method_exchangeImplementations 交换两个方法的实现
- 利用 class_replaceMethod 替换方法的实现
- 利用 method_setImplementation 来直接设置某个方法的IMP
这里摘抄一个 NSHipster 的例子:
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);
// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
BOOL didAddMethod =
class_addMethod(aClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(aClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}
@end
上面的代码通过添加一个Tracking
类别到UIViewController
类中,将UIViewController
类的viewWillAppear:
方法和Tracking
类别中xxx_viewWillAppear:方法的实现相互调换。Swizzling 应该在+ load
方法中实现,因为+ load
是在一个类最开始加载时调用。dispatch_once
是GCD中的一个方法,它保证了代码块只执行一次,并让其为一个原子操作,线程安全是很重要的。
如果类中不存在要替换的方法,那就先用class_addMethod
和class_replaceMethod
函数添加和替换两个方法的实现;如果类中已经有了想要替换的方法,那么就调用method_exchangeImplementations
函数交换了两个方法的 IMP,这是苹果提供给我们用于实现 Method Swizzling 的便捷方法。
可能有人注意到了这行:
// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
object_getClass((id)self)
与 [self class]
返回的结果类型都是 Class,但前者为元类,后者为其本身,因为此时 self 为 Class 而不是实例.注意 [NSObject class]
与 [object class]
的区别:
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
PS:如果类中没有想被替换实现的原方法时,class_replaceMethod
相当于直接调用class_addMethod
向类中添加该方法的实现;否则调用method_setImplementation
方法,types参数会被忽略。method_exchangeImplementations
方法做的事情与如下的原子操作等价:
IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);
最后xxx_viewWillAppear:
方法的定义看似是递归调用引发死循环,其实不会的。因为[self xxx_viewWillAppear:animated]
消息会动态找到xxx_viewWillAppear:
方法的实现,而它的实现已经被我们与viewWillAppear:
方法实现进行了互换,所以这段代码不仅不会死循环,如果你把[self xxx_viewWillAppear:animated]
换成[self viewWillAppear:animated]
反而会引发死循环。
runtime下class的各种常用操作
// 获取方法列表
unsigned int methodListCount;
Method *methodList = class_copyMethodList([self class], &methodListCount);
for (unsigned int i = 0; i < methodListCount; i++) {
Method method = methodList[i];
NSLog(@"method----->%@", NSStringFromSelector(method_getName(method)));
}
// 获取成员变量列表
unsigned int ivarCount;
Ivar *ivarList = class_copyIvarList([self class], &ivarCount);
for (unsigned int i = 0; i < ivarCount; i++) {
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSLog(@"Ivar----->%@", [NSString stringWithUTF8String:ivarName]);
}
// 获取协议列表
unsigned int protocolListCount;
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &protocolListCount);
for (unsigned int i = 0; i < protocolListCount; i++) {
Protocol *protocol = protocolList[1];
const char *protocolName = protocol_getName(protocol);
NSLog(@"protocol----->%@", [NSString stringWithUTF8String:protocolName]);
}
// 获取类方法
Method classMethod = class_getClassMethod([self class], @selector(cancelPreviousPerformRequestsWithTarget:));
NSLog(@"class method----->%@", NSStringFromSelector(method_getName(classMethod)));
// 获取实例方法
Method instanceMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
NSLog(@"instance method----->%@", NSStringFromSelector(method_getName(instanceMethod)));
// 替换方法的实现
Method newMethod = class_getInstanceMethod([self class], @selector(newReplaceMethod));
class_replaceMethod([self class], @selector(oldReplaceMethod), method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
[self oldReplaceMethod];
// 交换两个方法
Method oldChangeMethod = class_getInstanceMethod([self class], @selector(oldexchangeMethod));
Method newChangeMethod = class_getInstanceMethod([self class], @selector(newexchangeMethod));
method_exchangeImplementations(oldChangeMethod, newChangeMethod);
[self oldexchangeMethod];
[self newexchangeMethod];
总结
我们之所以让自己的类继承NSObject不仅仅因为苹果帮我们完成了复杂的内存分配问题,更是因为这使得我们能够用上 Runtime 系统带来的便利。可能我们平时写代码时可能很少会考虑一句简单的[receiver message]
背后发生了什么,而只是当做方法或函数调用。深入理解 Runtime 系统的细节更有利于我们利用消息机制写出功能更强大的代码,比如 Method Swizzling 等。