学习OC编程了一直没有怎么接触过runtime,可是这个又很重要,最近学习下,做个小结。
事情还得从OC独有消息发送说起。OC底层的执行机制跑的是C语言,其实整个iOS系统的底层大部分都是由C语言写的,因此,OC中方法调用即发送消息最终都要变为C语言代码执行(个人理解是这样)。C语言中的方法执行是通过函数指针执行的,runtime机制完成的任务就是把消息发送转为函数指针。事情的开始就是下面这条语句。
[self test]; //示例方法调用
[receiver message];//抽象形式
经过编译器会变成:
objc_msgSend(receiver, selector)
如果有参数则是:
objc_msgSend(receiver, selector, arg1, arg2, ...)
以下所有的内容(包括后面的博客)都是把上面代码转为函数指针然后执行代码的过程。
先准备预备知识。
在runtime中有三个名字,消息,方法,方法选择器。
①message(消息)message的具体定义很难说,因为并没有真正的代码描述,简单的讲message 是一种抽象,包括了函数名+参数列表,他并没有实际的实体存在。
②method(方法)method是真正的存在的代码。如:- (int)meaning { return 42; }
③selector(方法选择器)selector 通过SEL类型存在,描述一个特定的method 或者说 message。在实际编程中,可以通过selector进行检索方法等操作。
这三个名词了解下。真正重要的是以下三个:SEL,IMP和id。
1.SEL
又叫方法选择器,在OC中如下定义(这些定义的查看可以包含头文件objc/objc.h,记得没错应该是)。
struct objc_selector *SEL;
关于objc_selector这个结构体我也不清楚,但是据网上查阅SEL它是个char*指针,代表方法的名字。可以再代码中用如下方法验证。
SEL selector = @selector(message); //@selector不是函数调用,只是给编译器的一个提示
NSLog (@"%s", (char *)selector); //print message
还记得在初学OC时就教导我们,OC的方法的命名只与冒号前面的名称有关,与参数类型无关,与参数名无关。
//下面的两种命名会报错
-(void)setWidth:(int)width;
-(void)setWidth:(double)width
//要改为的命名
-(void)setWidthIntValue:(int)width;
-(void)setWidthDoubleValue:(int)width
每次给方法想名字都很麻烦!每次名字都很长!都是为了这个SEL,它就是用字符串来区分方法的。编译器会根据每个方法的方法名为方法生成唯一的SEL,这些SEL组成了一个Set集合,这个Set简单的说就是一个经过了优化过的hash表。SEL实际上就是根据方法名hash化了的一个字符串(注意,在下面方法选择Cache中选择方法时也用到了hash表)。而Set的特点就是唯一,也就是SEL是唯一的,因此,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,但是,有一个问题,就是数量增多会增大hash冲突而导致的性能下降(或是没有冲突,因为也可能用的是perfect hash)。但是不管使用什么样的方法加速,如果能够将总量减少(多个方法可能对应同一个SEL),那将是最犀利的方法。
到这里就清楚了SEL就是编译器根据方法名hash化的一个字符串,程序中所有的SEL都放在一个set集合中,而且相同的名称的方法对应同一个SEL。而方法由那个对象执行或者说是接收这个方法由isa指针和一下的IMP来决定。
2,IMP
IMP在objc.h中是如下定义的。
typedef id (*IMP)(id, SEL, ...);
这个比SEL要好理解多了,熟悉C语言的同学都知道,这其实是一个函数指针。这里插播一下方法的调用。方法调用或者说是函数执行就是执行一段代码,关键是确定这段函数的代码地址。C语言中(runtime就是C写的)所有的函数调用都是转换为函数指针后才能确定代码位置再执行代码。so,函数指针的作用即指向目标函数代码地址。而runtime中如何确定代码地址呢,就是用IMP。
IMP如何获得正确的函数代码地址呢?用SEL和id。方法名虽然在整个程序空间中可以重复,但是在单个类中却必须唯一,即在每个类中SEL能确定一个唯一的方法。id就是来确定类的。这两个参数都确定了就可以确定函数指针,也就可以确定代码地址了,runtime就可以像我们平时调用C语言函数一样执行OC的方法了。至于SEL后面的……是用来表示执行方法的参数的。
举个例子:
//定义一个IMP(函数指针),是不是有点像block
void (* performMessage)(id,SEL);
//通过methodForSelector方法根据SEL获取对应的函数指针
performMessage = (void (*)(id,SEL))[self methodForSelector:@selector(message)];
//通过取到的IMP(函数指针)跳过runtime消息传递机制,直接执行message方法
performMessage(self,@selector(message));
3,id
objc_msgSend
第一个参数类型为id
,对它都不陌生,它是一个指向类实例的指针:
typedef struct objc_object *i
那objc_object
又是什么鬼呢:
struct objc_object { Class isa; };
objc_object
结构体包含一个isa
指针,根据isa
指针就可以顺藤摸瓜找到对象所属的类,即找到相应的class。
另外再补充一些知识。
class:
之所以说isa
是指针是因为Class
其实是一个指向objc_class
结构体的指针:
1
| |
而objc_class
就是我们摸到的那个瓜,里面的东西多着呢:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。
其中objc_ivar_list
和objc_method_list
分别是成员变量列表和方法列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
如果你C语言不是特别好,可以直接理解为objc_ivar_list
结构体存储着objc_ivar
数组列表,而objc_ivar
结构体存储了类的单个成员变量的信息;同理objc_method_list
结构体存储着objc_method
数组列表,而objc_method
结构体存储了类的某个方法的信息。
最后要提到的还有一个objc_cache
,顾名思义它是缓存,它在objc_class
的作用很重要,在后面会讲到。
不知道你是否注意到了objc_class
中也有一个isa
对象,这是因为一个 ObjC 类本身同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。当你发出一个类似[NSObject alloc]
的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。你会说 NSObject
的子类时,你的类就会指向 NSObject
做为其超类。但是所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当 [NSObject alloc]
这条消息发给类对象的时候,objc_msgSend()
会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。
上图实线是 super_class
指针,虚线是isa
指针。 有趣的是根元类的超类是NSObject
,而isa
指向了自己,而NSObject
的超类为nil
,也就是它没有超类。
Method
Method
是一种代表类中的某个方法的类型。
1
| |
而objc_method
在上面的方法列表中提到过,它存储了方法名,方法类型和方法实现:
1 2 3 4 5 | |
- 方法名类型为
SEL
,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。 - 方法类型
method_types
是个char
指针,其实存储着方法的参数类型和返回值类型。 method_imp
指向了方法的实现,本质上是一个函数指针,后面会详细讲到。
Ivar
Ivar
是一种代表类中实例变量的类型。
1
| |
而objc_ivar
在上面的成员变量列表中也提到过:
1 2 3 4 5 6 7 8 | |
PS:OBJC2_UNAVAILABLE
之类的宏定义是苹果在 Objc 中对系统运行版本进行约束的黑魔法,有兴趣的可以查看源代码。