Objective-c 类的结构
Objective-c 作为一种动态语言,除了需要一个编译器来编译之外,还需要有一套运行时环境来动态的创建类和对象。这就是我们要了解的Runtime。Runtime的核心是消息传递(Messaging),了解这一核心能够更好的利用语言的特点,适当的时候进行扩展。
与静态语言不同的是,Objective-c在调用方法的时候并不是直接跳转到编译时生成的函数地址执行代码,而是会把这个方法以消息的形式发送给object。能否由object执行或者转发给别的对象处理或者不做处理,都是在运行时决定的。
打开objc/runtime.h可以看到,系统声明了一个objc_class的结构体
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
从结构体中可以看出,一个运行时类关联了它的父类指针,类名,版本号,成员变量,方法列表(实例方法),缓存,协议。
其中成员变量,方法列表都是一个结构体类型
//成员变量和成员变量列表
struct objc_ivar {
char * _Nullable ivar_name OBJC2_UNAVAILABLE;
char * _Nullable ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
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 {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
struct objc_method_list {
struct objc_method_list * _Nullable 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;
} OBJC2_UNAVAILABLE;
objc_ivar_list和objc_method_list是分别用来存储成员变量和方法的列表,这两个结构体内部有objc_ivar,和objc_method这两个结构体,这两个结构体存储的是对应的成员变量和方法。其中objc_method这个结构体有SEL method_name 和 IMP method_imp这两个变量,分别对应方法的名称和实现。
SEL是selector在oc中的表示,本质是一个映射到方法的一个C字符串。
IMP是一个函数指针,指向一个函数的实现
这里还要再了解一个c结构体objc_object
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
这个结构体对应的是我们类的实例,结构体内部只有一个isa属性,指向的正是objc_class这个结构体,而objc_class这个结构体中也有一个isa指针说明,类本身也是一种对象,这个isa指向的是类对象的元类Meta Class,Meta Class 表述了类对象本身所具备的元数据。那既然类本身作为一个对象存在,那类方法是不是就相当于是类对象的实例方法那?我们通过下面的代码来测试一下:
Test *test = [[Test alloc] init];
unsigned int count = 0;
Method *meths = class_copyMethodList(object_getClass([test class]), &count);
for (NSInteger i = 0; i < count; i++) {
Method meth = meths[i];
SEL sel = method_getName(meth);
const char *name = sel_getName(sel);
NSLog(@"%s", name);
}
free(meths);
runtime.h中提供了很多方便的方法使我们可以轻易的获取方法列表或者成员变量,其中以class_开头的是跟objc_class相关的方法,以object_开头的是跟objc_object相关的方法,分别对应我们的类对象和实例对象。通过上面的object_getClass方法可以获取需要的类,传入的一个obj对象,如果传入的是test则打印出来的方法名是实例方法,如果传入的是test的类则打印出来的是类方法,通过这个测试也验证了上面的猜测。
了解了类的构成之后,那么在调用方法的时候是怎么处理的那?oc的运行时特性又是怎么体现出来的那?
消息发送
事实上,在编译时,oc的调用方法会被翻译成一个c的函数调用,objc_msgSend(receiver, selector, arg1, arg2, ...).
从上面的分析可以看出,发送一条消息应该执行下列步骤:
1. 通过obj的isa找到实例的class
2. 在class的方法列表中找对应的方法
3. 如果没找到就去superclass里继续找
4. 一旦找到就去执行它的实现IMP
一个正常的方法通常都是这样被调用的,而且由于不同的方法调用频率是不一样的,所以系统提供了一个cache也就是上面看到的objc_cache来做缓存,提高查询效率。
动态方法解析,消息转发和方法签名
上面的例子只是针对正常的情况,那如果该方法没找到怎么办,这时就会抛出unrecognized selector sent to … 的异常,这也是我们在开发中经常遇到的情况。那怎么避免这种情况那,这就用到了oc的动态特性,在一场抛出前,runtime会给三次补救的机会:
1.动态方法解析
查看NSObject的API发现有下面这两个方法+resolveInstanceMethod:和+resolveClassMethod:分别对应实例方法和类方法。在没有找到方法的情况下,如果实现了这两个方法中的一个,那么系统就会重新启动一次消息发送的过程。
以上面的Test类为例,如果一个Test的实例调用一个method1:方法,而Test类没有实现的话使用下面的代码可以避免程序闪退:
void method2(id obj, SEL _cmd)
{
NSLog(@"do something");
}
+(BOOL)resolveInstanceMethod:(SEL)aSEL
{
if(aSEL == @selector(method1:))
{
class_addMethod([self class],aSEL,(IMP)method2,@"v@:");
return YES;
}
return [super resloveInstanceMethod];
}
2.如果resolve方法返回NO,就会进行下一步,消息转发(Message Forwarding)
同样runtime提供了一个方法,-forwardingTargetForSelector: ,用这个方法可以获得把消息转发给其他对象的机会,该方法返回一个实现了被调用的方法的实例:
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(method1:))
{
return xxxObject;
}
return [super forwardingTargetForSelector:aSelector];
}
3.如果这个方法也返回nil,则会启动最后一步,方法签名,首先runtime会发送一个-methodSignatureForSelector:消息来获取参数和返回值,如果返回nil则发送doesNotRecognizeSelector:消息,这是程序也就崩溃了。如果返回了一个函数签名,runtime就会创建一个NSInvocation对象,发送-forwardInvocation:消息给当前对象,并传入NSInvocation:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature *signature = [super methodSignatureForSelector:selector];
if(!signature) {
if([xxxObj respondsToSelector:selector]) {
return [xxxObj methodSignatureForSelector:selector];
}
}
return signature;
}
- (void)forwardInvocation:(NSInvocation*)invocation
{
if ([xxxObj respondsToSelector:[invocation selector]]) {
[invocation invokeWithTarget:xxxObj];
}else {
[self doesNotRecognizeSelector:sel];
}
}
这就是Objective-c关于消息发送的流程,利用这个流程和运行时的特性可以对语音进行扩展和解决一些问题。
Refer:
Objective C Runtime
iOS开发-Runtime详解() - 一个低调的iOS开发 - 博客园