曾经觉得Objc特别方便上手,面对着 Cocoa 中大量 API,只知道简单的查文档和调用。还记得初学 Objective-C 时把[receiver message]
当成简单的方法调用,而无视了“发送消息”这句话的深刻含义。其实[receiver message]
会被编译器转化为:
objc_msgSend(receiver, selector)
如果消息含有参数,则为:
objc_msgSend(receiver, selector, arg1, arg2, ...)
如果消息的接收者能够找到对应的selector
,那么就相当于直接执行了接收者这个对象的特定方法;否则,消息要么被转发,或是临时向接收者动态添加这个selector
对应的实现内容,要么就干脆玩完崩溃掉。
现在可以看出[receiver message]
真的不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接收者发送message
这条消息,而receive
将要如何响应这条消息,那就要看运行时发生的情况来决定了。
Objective-C 的 Runtime 铸就了它动态语言的特性,这些深层次的知识虽然平时写代码用的少一些,但是却是每个 Objc 程序员需要了解的。
SEL
objc_msgSend
函数第二个参数类型为SEL
,它是selector
在Objc中的表示类型(Swift中是Selector
类)。selector
是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL
:
typedef struct objc_selector *SEL;
其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()
或者 Runtime 系统的sel_registerName
函数来获得一个SEL类型的方法选择器。不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是 Objc 中方法命名有时会带上参数类型
id
objc_msgSend
第一个参数类型为id
,大家对它都不陌生,它是一个指向类实例的指针:
typedef struct objc_object *id;
那objc_object又是啥呢:
struct objc_object { Class isa; };
objc_object
结构体包含一个isa
指针,根据isa
指针就可以顺藤摸瓜找到对象所属的类。
PS:isa
指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class方法来确定实例对象的类。因为KVO
的实现机理就是将被观察对象的isa指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling的技术,详见官方文档
Class
之所以说isa
是指针是因为Class其实是一个指向objc_class
结构体的指针:
typedef struct objc_class *Class;```
而`objc_class`就是我们摸到的那个瓜,里面的东西多着呢:
```objectivec
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;
可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。
PS:OBJC2_UNAVAILABLE
之类的宏定义是苹果在 Objc 中对系统运行版本进行约束的黑魔法,为的是兼容非Objective-C 2.0的遗留逻辑,但我们仍能从中获得一些有价值的信息,有兴趣的可以查看源代码。
Objective-C 2.0 的头文件虽然没暴露出objc_class
结构体更详细的设计,我们依然可以从Objective-C 1.0 的定义中小窥端倪:
在objc_class
结构体中:ivars
是objc_ivar_list
指针;methodLists
是指向objc_method_list
指针的指针。也就是说可以动态修改*methodLists
的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。而最新版的 Runtime 源码对这一块的描述已经有很大变化,可以参考下美团技术团队的深入理解Objective-C:Category。
PS:任性的话可以在Category中添加@dynamic
的属性,并利用运行期动态提供存取方法或干脆动态转发;或者干脆使用关联度对象(AssociatedObject)
其中objc_ivar_list
和objc_method_list
分别是成员变量列表和方法列表:
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
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;
}
如果你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 alloc] 这条消息发给类对象的时候,objc_msgSend()会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。
上图实线是super_class
指针,虚线是isa
指针。 有趣的是根元类的超类是NSObject
,而isa
指向了自己,而NSObject
的超类为nil
,也就是它没有超类。
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;
} OBJC2_UNAVAILABLE;
- 方法名类型为
SEL
,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。 - 方法类型
method_types
是个char
指针,其实存储着方法的参数类型和返回值类型。 -
method_imp
指向了方法的实现,本质上是一个函数指针,后面会详细讲到。
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
} OBJC2_UNAVAILABLE;
可以根据实例查找其在类中的名字,也就是“反射”:
-(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;
}
class_copyIvarList
函数获取的不仅有实例变量,还有属性。但会在原本的属性名前加上一个下划线。
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
命中率应该也做了努力吧。
Property
@property
标记了类中的属性,这个不必多说大家都很熟悉,它是一个指向objc_property
结构体的指针:
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//这个更常用
可以通过class_copyPropertyList
和protocol_copyPropertyList
方法来获取类和协议中的属性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
返回类型为指向指针的指针,哈哈,因为属性列表是个数组,每个元素内容都是一个objc_property_t
指针,而这两个函数返回的值是指向这个数组的指针。举个例子,先声明一个类:
interface Lender : NSObject {
float alone;
}
@property float alone;
@end
你可以用下面的代码获取属性列表:
id LenderClass = objc_getClass("Lender");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
你可以用property_getName
函数来查找属性名称:
const char *property_getName(objc_property_t property)
你可以用class_getProperty
和protocol_getProperty
通过给出的名称来在类和协议中获取属性的引用:
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
你可以用property_getAttributes
函数来发掘属性的名称和@encode
类型字符串:
const char *property_getAttributes(objc_property_t property)
把上面的代码放一起,你就能从一个类中获取它的属性啦:
id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}
对比下class_copyIvarList
函数,使用 class_copyPropertyList
函数只能获取类的属性,而不包含成员变量。但此时获取的属性名是不带下划线的。