Objective-C语言总是尽可能地将工作从编译链接时推迟到运行时。只要有可能,Objective-C总是使用动态的方式来解决问题。这意味着Objective-C语言不仅需要一个编译器,同时也需要一个运行时系统来执行编译好的代码。Runtime扮演的角色类似于Objective-C语言的操作系统,Objective-C基于该系统来工作。
1、与Runtime的交互
Objective-C程序有三种途径和运行时系统交互:
- 通过Objective-C源代码;
- 通过Foundation框架中NSObject的方法;
- 通过直接调用Runtime的函数。
1.1、通过Objective-C源代码
大部分情况下,你只需编写和编译Objective-C源代码,运行时系统在后台自动运行。
当编译Objective-C类和方法时,编译器为实现语言动态特性将自动创建一些数据结构和函数。这些数据结构包含类定义和协议定义的信息(如objc_msgSend函数)。
1.2、通过NSObject的方法
Cocoa中绝大部分类都是NSObject的子类,都继承了NSObject的方法。
NSObject的某些方法可以从运行时系统中获取信息,对对象进行一定程度的自我检查,如class
返回对象的类;isKindOfClass:
和isMemberOfClass:
检查对象是否在指定的类继承体系中;respondsToSelector:
检查对象能否响应指定的消息;conformsToProtocol:
检查对象是否实现了指定协议类的方法;methodForSelector:
返回指定方法实现的地址。
1.3、通过Runtime的函数
Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态库,头文件存放在/usr/include/objc中。这些函数支持用纯C的函数来实现 Objective-C 中同样的功能。虽然有一些方法构成了NSObject类的基础,但是我们在写 Objc 代码时一般不会直接用到这些函数的。在Objective-C Runtime Reference中有对 Runtime 函数的详细文档。
2、Runtime术语
-
SEL
SEL
是映射到方法的C字符串,它不同于C语言中的函数指针,函数指针直接保存了方法的地址,但SEL
只是方法编号。它的数据结构是这样的:
typedef struct objc_selector *SEL;
可以用 Objc 编译器命令@selector()
或者 Runtime 系统的sel_registerName
函数来获得一个SEL
类型的方法选择器。不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。
-
id
概括来说,id
是一个指向实例的指针,定义如下:
typedef struct objc_object *id;
objc_object
的定义是:
struct objc_object { Class isa; };
struct objc_class {
Class isa;
#if !__OBJC2__
......
#endif
};
可以看到,id 是指向 objc_object 结构体的指针,而 objc_object 包含一个 Class 的结构体指针 isa。
不过isa
指针不总是指向实例对象所属的类,不能依靠它来确定类型,而应该用class
方法来确定实例对象的类。因为KVO的实现原理就是将被观察对象的isa
指针指向一个动态创建的中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术,详见官方文档。
-
Class
Class
是一个指向objc_class
结构体的指针:
typedef struct objc_class *Class;
在 runtime.h 中可以看到
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;
objc_class
结构体就是 Objective-C 的对象系统的基石,其中第一个字段isa
是objc_class
结构体指针,指向该对象所属的类型对象。实际上,在 Objective-C 中,类本身也是一个对象,而一个类的 isa 指针指向它的元类(Meta Class)。元类是一个类对象的类,元类中存储着类方法。
向一个对象发送消息时,runtime会在这个对象所属的那个类的方法列表中查找。而向一个类发送消息时,runtime会在这个类的元类的方法列表中查找。每个类都会有一个单独的元类,因为每个类的类方法基本不可能完全相同。
那么元类是什么?
和类一样,元类也是一个对象,它也有一个 isa 指针指向其所属的类。所有的元类都使用 NSObject 的元类作为它们的所属类。NSObject 的元类是它自己。
与类一样,元类也有自己的父类。meta class 的 super class 是 super class 的 meta class。直到基类的 meta class,它的 super class 指向基类自身。关系如下:
-
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;
可以根据实例查找其在类中的名字,也就是“反射”:
-(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
是一个函数指针,它的定义是:
typedef id (*IMP)(id, SEL, ...);
当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面会提到。
-
Cache
在 runtime.h 中Cache
的定义如下:
typedef struct objc_cache *Cache;
objc_class
结构体中有一个struct objc_cache *cache
,它到底是缓存啥的呢?
Cache
为方法调用的性能进行优化,通俗地讲,每当实例对象接收到一个消息时,它不会直接在isa
指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了,而是优先在Cache
中查找。Runtime 系统会把被调用的方法存到Cache
中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高。
-
Property
@property
标记了类中的属性,它是一个指向objc_property
结构体的指针:
typedef struct objc_property *Property;
可以通过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
指针,而这两个函数返回的值是指向这个数组的指针。
相对于class_copyIvarList
函数,使用class_copyPropertyList
函数只能获取类的属性,而不包含成员变量。但此时获取的属性名是不带下划线的。
你可以用property_getAttributes
函数来发掘属性的名称和@encode类型字符串:
- property_getAttributes 返回的字符串以字母 T 开始,接着是@encode 编码和逗号。
- 如果属性有 readonly 修饰,则字符串中含有 R 和逗号。
- 如果属性有 copy 或者 retain 修饰,则字符串分别含有 C 或者&,然后是逗号。
- 如果属性定义有定制的 getter 和 setter 方法,则字符串中有 G 或者 S 跟着相应的方法名以及逗号(例如,GcustomGetter,ScustomSetter:,,)。
- 如果属性是只读的,且有定制的 get 访问方法,则描述到此为止。
- 字符串以 V 然后是属性的名字结束。
3、消息
本段主要描述如何将发消息转换为objc_msgSend函数调用,如何通过名字来指定一个方法,以及如何使用objc_msgSend函数。
3.1、获得方法地址
使用NSObject类中的methodForSelector:
方法,可以获得一个指向方法实现的指针,通过该指针可以直接调用方法实现。例:
void (*setter) (id, SEL, BOOL);
// methodForSelector:方法会返回一个函数指针
setter = (void (*)(id, SEL, BOOL))[self methodForSelector:@selector(setFilled:)];
for (int i = 0; i < 1000; i ++) {
// 使用函数指针直接调函数,参数一:函数对象,参数二:函数签名,参数三:函数参数
setter(self, @selector(setFilled:), NO);
}
注意,methodForSelector:
是Runtime提供的功能,不是Objective-C语言本身的功能。
3.2、objc_msgSend函数
Objective-C中,消息是在运行的时候才和方法实现绑定的。[receiver message]
会被编译器转换成对objc_msgSend函数的调用。该函数有两个主要参数:receiver
和selector
,再加上参数的话就是objc_msgSend(receiver, selector, arg1, arg2, ...)
。
消息发送的详细步骤:
- 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain ,release 这些函数了。
- 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
- 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
- 如果 cache 找不到就找一下方法分发表。
- 如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
- 找到方法实现之后,然后将消息接收者对象及方法中指定的参数传给找到的方法实现,最后,将方法实现的返回值作为该函数的返回值返回。
- 如果还找不到就要开始进入动态方法解析了,后面会提到。
消息机制的关键在于编译器为类和对象生成的结构,每个类的结构中至少包含两个基本元素:isa
指针和方法列表。
当对象被创建时,它会被分配内存,并初始化实例变量。对象的第一个实例变量是一个指向该对象的类结构体的指针,即isa
。通过isa
指针可以访问它对应的类及相应的父类。方法列表存放方法名字和对应的实现地址。
对象接收消息时,objc_msgSend
先根据该对象的isa
指针找到该对象对应的类的方法表,从表中寻找对应的方法。如果找不到,objc_msgSend
将继续在父类中寻找,直到NSObject类。一旦找到对应方法,objc_msgSend
会以消息接收者对象为参数调用该方法。
为了加快消息的处理过程,运行时系统通常会将使用过的方法放入缓存中。每个类都有一个独立的缓存,同时包括继承的方法和在该类中定义的方法。objc_msgSend
在寻找方法时,会优先在缓存中寻找。如果缓存中已经有了需要的方法,则消息仅仅比函数调用慢一点点。
3.3、使用隐藏的参数
我们经常使用self
来表示当前方法的对象,但是为什么它能表示当前方法对象呢?实际上它是在代码编译时插入方法中的。
当objc_msgSend找到方法对应的实现时,它会直接调用该方法,并将消息中的参数传递给方法实现,同时,它还传递两个隐藏的参数:接收消息的对象(self)和方法选择器(_cmd)。
在方法中可以通过self
来引用消息接收者对象,通过_cmd
来引用方法本身。
3.4、动态方法解析
我们可以通过resolveInstanceMethod:
和resolveClassMethod:
方法动态地添加实例方法和类方法的实现。当Runtime系统在缓存和方法列表中找不到要执行的方法时,会调用resolveInstanceMethod:
和resolveClassMethod:
方法来给我们一次动态添加方法实现的机会。例如,有如下的函数:
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
可以通过resolveInstanceMethod:
将它作为类方法 resolveThisMethodDynamically
的实现:
@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
其中 “v@:” 表示返回值和参数,这个符号涉及 Type Encoding。
动态方法解析会在消息转发之前前执行。如果 respondsToSelector:
或 instancesRespondToSelector:
方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的IMP的机会。如果你实现了resolveInstanceMethod:
方法但是仍然希望正常进行消息转发,只需要返回NO就可以了。
4、消息转发
通常,给一个对象发送它不能处理的消息会得到出错提示,不过,运行时系统在抛出错误之前,还有三次机会拯救程序:
- Method Resolution
- Fast Forwarding
- Normal Forwarding
Method Resolution(动态方法解析)
Runtime系统在运行时会先调用resolveInstanceMethod:
或resolveClassMethod:
方法,让我们添加方法的实现。如果添加方法并返回YES,那系统就会重新启动一次消息发送的过程。如果没有实现或返回NO,会执行 Fast Forwarding 操作。
Fast Forwarding(快速转发)
如果目标对象实现forwardingTargetForSelector:
方法,并且这个方法返回的不是nil或self,也会重启消息发送的过程,把这消息转发给指定对象来处理。否则,就会继续 Normal Fowarding。
Normal Forwarding(“慢速”转发)
如果没有使用 Fast Forwarding 来转发消息,最后只能使用 Normal Forwarding 来进行消息转发。它会调用methodSignatureForSelector:
方法来获取函数的参数和返回值,如果返回为nil,程序会Crash掉,并抛出 unrecognized selector sent to instance 异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation
对象并调用forwardInvocation:
方法进行消息转发。
4.1、转发
如果一个对象收到一条无法处理的消息(如动态方法解析返回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:
方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以,如果你希望你的对象将一个消息转发给其它对象,你的对象就不能有这个方法。否则,forwardInvocation:
将不会被调用。
4.2、转发和多重继承
消息转发很像继承,并且可以用来在Objective-C程序中模拟多重继承。如下图所示,一个对象通过转发来响应消息,看起来就像该对象从别的类那借来了或者”继承“了方法实现一样。
在上图中,Warrior 类的一个对象实例将 negotiate 消息转发给 Diplomat 类的一个实例。看起来,Warrior 类似乎和 Diplomat 类一样, 响应 negotiate 消息,并且行为和 Diplomat 一样,实际上是 Diplomat 类响应了该消息。
消息转发弥补了Objective-C不支持多重继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。
4.3、转发和类继承
尽管消息转发很像继承,但它不是继承。例如在 NSObject 类中,方法respondsToSelector:
和isKindOfClass:
只会出现在继承链中,而不是消息转发链中。例如,如果向一个 Warrior 类的对象询问它能否响应 negotiate 消息:
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...
返回NO,尽管该对象能够接收和响应negotiate
方法。
如果你想要让它看起来真的像是继承了negotiate
方法,必须重新实现respondsToSelector:
和isKindOfClass:
方法:
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector] )
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}
除了respondsToSelector:
和isKindOfClass:
之外,instancesRespondToSelector:
也必须重新实现。如果使用的是协议类,需要重新实现的还有conformsToProtocol:
方法。类似地,如果一个对象转发它接受的任何远程消息,它得给出一个methodSignatureForSelector:
来返回准确的方法描述,这个方法会最终响应被转发的消息。比如一个对象能给它的替代者对象转发消息,它需要像下面这样实现methodSignatureForSelector:
:
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}
5、动态属性关联
在 OS X 10.6 之后,Runtime系统让Objc支持向对象动态添加变量。涉及到的函数有以下三个:
void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
id objc_getAssociatedObject ( id object, const void *key );
void objc_removeAssociatedObjects ( id object );
这些方法以键值对的形式动态地向对象添加、获取或删除关联值。其中关联政策是一组枚举常量:
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
这些常量对应着引用关联值的政策,也就是 Objc 内存管理的引用计数机制。你会发现这里边没有 weak 属性,关于如何关联 weak 属性,请参考《如何使用 Runtime 给现有的类添加 weak 属性》。
6、Method Swizzling
Method Swizzling 就是方法交换,主要有两种使用场景:hook和面向切面编程。
hook一般在+load
方法中使用:
- (void)replacementReceiveMessage:(id)arg1 {
[self replacementReceiveMessage:arg1];
}
+ (void)load {
SEL originalSelector = @selector(ReceiveMessage:);
SEL overrideSelector = @selector(replacementReceiveMessage:);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method overrideMethod = class_getInstanceMethod(self, overrideSelector);
if (class_addMethod(self, originalSelector, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) {
class_replaceMethod(self, overrideSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, overrideMethod);
}
}
APP需要进行数据埋点时,就需要面向切面编程了。假如需要统计按钮点击的情况,就可以把按钮点击的方法进行交换,这样就可以最大限度地减少代码修改和入侵。
要注意的是,在+load
中使用 Method Swizzling 是一件很危险的事情,因为它会影响工程中所有相同类的代码,可能会出现意想不到的Bug。
关于 Method Swizzling 有一个轻量级的库Aspects很值得阅读。