IOS开发系列——Objective-c Runtime专题
Objective-c Runtime专题总结
原文http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/
1OC与Runtime的交互方式
OC从三种不同的层级上与Runtime系统进行交互,分别是通过Objective-C源代码,通过Foundation框架的NSObject类定义的方法,通过对runtime函数的直接调用。
1.1Objective-C源代码
大部分情况下你就只管写你的OC代码就行,runtime系统自动在幕后辛勤劳作着。
1.2NSObject的方法
Cocoa中大多数类都继承于NSObject类,也就自然继承了它的方法。最特殊的例外是NSProxy,它十个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类。
有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时获得类的信息,并检查一些特性,比如class返回对象的类;isKindOfClass:和isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。
1.3Runtime的函数
Runtime系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。许多函数允许你用纯C代码来重复实现OC中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写OC代码时一般不会直接用到这些函数的,除非是写一些OC与其他语言的桥接或是底层的debug工作。在Objective-C Runtime Reference中有对Runtime函数的详细文档。
2Runtime术语
id objc_msgSend( id self, SEL op, ... );
2.1SEL
objc_msgSend函数第二个参数类型为SEL,它是selector在OC中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的ID,而这个ID的数据结构是SEL :
typedef structobjc_selector *SEL;
其实它就是个映射到方法的C字符串,你可以用OC编译器命令@selector()或者Runtime系统的sel_registerName函数来获得一个SEL类型的方法选择器。
不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是OC中方法命名有时会带上参数类型( NSNumber一堆抽象工厂方法拿走不谢),Cocoa中有好多长长的方法哦。
2.2Id与objc_object结构体
objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:
typedefstructobjc_object*id;
那objc_object又是啥呢:
structobjc_object{Classisa;};
objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。
2.3Class
之所以说isa是指针是因为Class其实是一个指向objc_class结构体的指针:
typedefstructobjc_class*Class;
而objc_class就是我们摸到的那个瓜,里面的东西多着呢:
structobjc_class{
ClassisaOBJC_ISA_AVAILABILITY;
#if!__OBJC2__
Classsuper_classOBJC2_UNAVAILABLE;
constchar*nameOBJC2_UNAVAILABLE;
longversionOBJC2_UNAVAILABLE;
longinfoOBJC2_UNAVAILABLE;
longinstance_sizeOBJC2_UNAVAILABLE;
structobjc_ivar_list*ivarsOBJC2_UNAVAILABLE;
structobjc_method_list**methodListsOBJC2_UNAVAILABLE;
structobjc_cache*cacheOBJC2_UNAVAILABLE;
structobjc_protocol_list*protocolsOBJC2_UNAVAILABLE;
#endif
}OBJC2_UNAVAILABLE;
可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。
其中objc_ivar_list和objc_method_list分别是成员变量列表和方法列表:
structobjc_ivar_list{
intivar_countOBJC2_UNAVAILABLE;#ifdef __LP64__intspaceOBJC2_UNAVAILABLE;
#endif/* variable length structure */
structobjc_ivarivar_list[1]OBJC2_UNAVAILABLE;
}OBJC2_UNAVAILABLE;
structobjc_method_list{
structobjc_method_list*obsoleteOBJC2_UNAVAILABLE;
intmethod_countOBJC2_UNAVAILABLE;
#ifdef __LP64__
intspaceOBJC2_UNAVAILABLE;
#endif
/* variable length structure */
structobjc_methodmethod_list[1]OBJC2_UNAVAILABLE;
}
2.4元类(Meta Class)
一个ObjC类同时也是一个对象,为了处理类和对象的关系,runtime库创建了一种叫做元类(Meta Class)的东西。当你发出一个类似[NSObject alloc]的消息时,你事实上是把这个消息发给了一个类对象(Class Object),这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类(root meta class)的实例。你会说NSObject的子类时,你的类就会指向NSObject做为其超类。但是所有的元类都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当[NSObject alloc]这条消息发给类对象的时候,objc_msgSend()会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。
上图实线是super_class指针,虚线是isa指针。有趣的是根元类的超类是NSObject,而isa指向了自己,而NSObject的超类为nil,也就是它没有超类。
2.5Method
Method是一种代表类中的某个方法的类型。
typedefstructobjc_method*Method;
而objc_method在上面的方法列表中提到过,它存储了方法名,方法类型和方法实现:
structobjc_method{
SELmethod_nameOBJC2_UNAVAILABLE;
char*method_typesOBJC2_UNAVAILABLE;
IMPmethod_impOBJC2_UNAVAILABLE;
}OBJC2_UNAVAILABLE;
·方法名类型为SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
·方法类型method_types是个char指针,其实存储着方法的参数类型和返回值类型。
·method_imp指向了方法的实现,本质上是一个函数指针,后面会详细讲到。
2.6Ivar
Ivar是一种代表类中实例变量的类型。
typedefstructobjc_ivar*Ivar;
而objc_ivar在上面的成员变量列表中也提到过:
tructobjc_ivar{
char*ivar_nameOBJC2_UNAVAILABLE;
char*ivar_typeOBJC2_UNAVAILABLE;
intivar_offsetOBJC2_UNAVAILABLE;
#ifdef __LP64__
intspaceOBJC2_UNAVAILABLE;
#endif
}OBJC2_UNAVAILABLE;
PS:OBJC2_UNAVAILABLE之类的宏定义是苹果在OC中对系统运行版本进行约束的黑魔法,有兴趣的可以查看源代码。
2.7IMP函数指针
IMP在objc.h中的定义是:
typedefid(*IMP)(id,SEL,...);
它就是一个函数指针,这是由编译器生成的。当你发起一个ObjC消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而IMP这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面会提到。
你会发现IMP指向的方法与objc_msgSend函数类型相同,参数都包含id和SEL类型。每个方法名都对应一个SEL类型的方法选择器,而每个实例对象中的SEL对应的方法实现肯定是唯一的,通过一组id和SEL参数就能确定唯一的方法实现地址;反之亦然。
2.8Cache
在runtime.h中Cache的定义如下:
typedefstructobjc_cache*Cache
还记得之前objc_class结构体中有一个struct
objc_cache *cache吧,它到底是缓存啥的呢,先看看objc_cache的实现:
structobjc_cache{
unsignedintmask/* total = mask + 1 */OBJC2_UNAVAILABLE;
unsignedintoccupiedOBJC2_UNAVAILABLE;
Methodbuckets[1]OBJC2_UNAVAILABLE;
};
Cache为方法调用的性能进行优化,通俗地讲,每当实例对象接收到一个消息时,它不会直接在isa指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了,而是优先在Cache中查找。Runtime系统会把被调用的方法存到Cache中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高。
3消息
OC中发送消息是用中括号([])把接收者和消息括起来,而直到运行时才会把消息与方法实现绑定。
3.1objc_msgSend函数
看起来像是objc_msgSend返回了数据,其实objc_msgSend从不返回数据而是你的方法被调用后返回了数据。下面详细叙述下消息发送步骤:
1.检测这个selector是不是要忽略的。比如Mac OS X开发,有了垃圾回收就不理会retain,release这些函数了。
2.检测这个target是不是nil对象。ObjC的特性是允许对一个nil对象执行任何一个方法不会Crash,因为会被忽略掉。
3.如果上面两个都过了,那就开始查找这个类的IMP,先从cache里面找,完了找得到就跳到对应的函数去执行。
4.如果cache找不到就找一下方法分发表。(Class中的方法列表)
5.如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
6.如果还找不到就要开始进入动态方法解析了,后面会提到。
其实编译器会根据情况在objc_msgSend,objc_msgSend_stret,objc_msgSendSuper,或objc_msgSendSuper_stret四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有”Super”的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。排列组合正好四个方法。
3.2方法中的隐藏参数
我们经常在方法中使用self关键字来引用实例本身,但从没有想过为什么self就能取到调用当前方法的对象吧。其实self的内容是在方法运行时被偷偷地动态传入的。
当objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:
–接收消息的对象(也就是self指向的内容)
–方法选择器(_cmd指向的内容)
之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。在下面的例子中,self引用了接收者对象,而_cmd引用了方法本身的选择器:
- strange {
idtarget = getTheReceiver();
SEL method = getTheMethod();
if( target ==self|| method == _cmd )returnnil;
return[targetperformSelector:method];
}
在这两个参数中,self更有用。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。
而当方法中的super关键字接收到消息时,编译器会创建一个objc_super结构体:
struct objc_super { id receiver;Classclass; };
这个结构体指明了消息应该被传递给特定超类的定义。
3.3获取方法地址
在IMP那节提到过可以避开消息绑定而直接获取方法的地址并调用方法。这种做法很少用,除非是需要持续大量重复调用某方法的极端情况,避开消息发送泛滥而直接调用该方法会更高效。
NSObject类中有个methodForSelector:实例方法,你可以用它来获取某个方法选择器对应的IMP,举个栗子:
void(*setter)(id, SEL, BOOL);
inti;
setter = (void(*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for( i =0; i <1000; i++ )
setter(targetList[i], @selector(setFilled:), YES);
PS:methodForSelector:方法是由Cocoa的Runtime系统提供的,而不是OC自身的特性。
4动态方法解析
你可以动态地提供一个方法的实现。例如我们可以用@dynamic关键字在类的实现文件中修饰一个属性:
@dynamicpropertyName;
这表明我们会为这个属性动态提供存取方法,也就是说编译器不会再默认为我们生成setPropertyName:和propertyName方法,而需要我们动态提供。我们可以通过分别重载resolveInstanceMethod:和resolveClassMethod:方法分别添加实例方法实现和类方法实现。因为当Runtime系统在Cache和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:或resolveClassMethod:来给程序员一次动态添加方法实现的机会。我们需要用class_addMethod函数完成向特定类添加特定方法实现的操作:
void dynamicMethodIMP(idself, SEL _cmd) {
//implementation ....
}
@implementationMyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
if(aSEL ==@selector(resolveThisMethodDynamically)) {
class_addMethod([selfclass], aSEL, (IMP) dynamicMethodIMP,"v@:");
returnYES;
}
return[superresolveInstanceMethod:aSEL];
}
@end
上面的例子为resolveThisMethodDynamically方法添加了实现内容,也就是dynamicMethodIMP方法中的代码。其中“v@:”表示返回值和参数,这个符号涉及Type Encoding
PS:动态方法解析会在消息转发机制浸入前执行。如果respondsToSelector:或instancesRespondToSelector:方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的IMP的机会。如果你想让该方法选择器被传送到转发机制,那么就让resolveInstanceMethod:返回NO。
5消息转发
5.1重定向
在消息转发机制执行前,Runtime系统会再给我们一次偷梁换柱的机会,即通过重载- (id)forwardingTargetForSelector:(SEL)aSelector方法替换消息的接受者为其他对象:
- (id)forwardingTargetForSelector:(SEL)aSelector {
if(aSelector ==@selector(mysteriousMethod:)){
returnalternateObject;
}
return[superforwardingTargetForSelector:aSelector];
}
毕竟消息转发要耗费更多时间,抓住这次机会将消息重定向给别人是个不错的选择,不过千万别返回self,因为那样会死循环。
5.2转发
当动态方法解析不作处理返回NO时,消息转发机制会被触发,这时forwardInvocation:方法会被执行,我们可以重载这个方法来定义我们的转发逻辑:
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if([someOtherObject respondsToSelector: [anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else[superforwardInvocation:anInvocation];
}
该消息的唯一参数是个NSInvocation类型的对象——该对象封装了原始的消息和消息的参数。我们可以实现forwardInvocation:方法来对不能处理的消息做一些默认的处理,也可以将消息转发给其他对象来处理,而不抛出错误。
当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过forwardInvocation:消息通知该对象。每个对象都从NSObject类中继承了forwardInvocation:方法。然而,NSObject中的方法实现只是简单地调用了doesNotRecognizeSelector:。通过实现我们自己的forwardInvocation:方法,我们可以在该方法实现中将消息转发给其它对象。
forwardInvocation:方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。
注意:forwardInvocation:方法只有在消息接收对象中无法正常响应消息时才会被调用。所以,如果我们希望一个对象将negotiate消息转发给其它对象,则这个对象不能有negotiate方法。否则,forwardInvocation:将不可能会被调用。
5.3转发和多继承
转发和继承相似,可以用于为OC编程添加一些多继承的效果。就像下图那样,一个对象把消息转发出去,就好似它把另一个对象中的方法借过来或是“继承”过来一样。
这使得不同继承体系分支下的两个类可以“继承”对方的方法,在上图中Warrior和Diplomat没有继承关系,但是Warrior将negotiate消息转发给了Diplomat后,就好似Diplomat是Warrior的超类一样。
消息转发弥补了OC不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。它将问题分解得很细,只针对想要借鉴的方法才转发,而且转发机制是透明的。
5.4替代者对象(Surrogate Objects)
转发不仅能模拟多继承,也能使轻量级对象代表重量级对象。弱小的女人背后是强大的男人,毕竟女人遇到难题都把它们转发给男人来做了。这里有一些适用案例,可以参看官方文档。
5.5转发与继承
尽管转发很像继承,但是NSObject类不会将两者混淆。像respondsToSelector:和isKindOfClass:这类方法只会考虑继承体系,不会考虑转发链。比如上图中一个Warrior对象如果被问到是否能响应negotiate消息:
if([aWarriorrespondsToSelector:@selector(negotiate)])...
结果是NO,尽管它能够接受negotiate消息而不报错,因为它靠转发消息给Diplomat类来响应消息。
如果你为了某些意图偏要“弄虚作假”让别人以为Warrior继承到了Diplomat的negotiate方法,你得重新实现respondsToSelector:和isKindOfClass:来加入你的转发算法:
-(BOOL)respondsToSelector:(SEL)aSelector{if([superrespondsToSelector:aSelector])returnYES;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.*/}returnNO;}
除了respondsToSelector:和isKindOfClass:之外,instancesRespondToSelector:中也应该写一份转发算法。如果使用了协议,conformsToProtocol:同样也要加入到这一行列中。类似地,如果一个对象转发它接受的任何远程消息,它得给出一个methodSignatureForSelector:来返回准确的方法描述,这个方法会最终响应被转发的消息。比如一个对象能给它的替代者对象转发消息,它需要像下面这样实现methodSignatureForSelector::
-(NSMethodSignature*)methodSignatureForSelector:(SEL)selector{
NSMethodSignature*signature=[supermethodSignatureForSelector:selector];
if(!signature){
signature=[surrogatemethodSignatureForSelector:selector];
}
returnsignature;
}
6壮的实例变量(NonFragile ivars)
在Runtime的现行版本中,最大的特点就是健壮的实例变量。当一个类被编译时,实例变量的布局也就形成了,它表明访问类的实例变量的位置。从对象头部开始,实例变量依次根据自己所占空间而产生位移:
上图左边是NSObject类的实例变量布局,右边是我们写的类的布局,也就是在超类后面加上我们自己类的实例变量,看起来不错。但试想如果那天苹果更新了NSObject类,发布新版本的系统的话,那就悲剧了:
我们自定义的类被划了两道线,那是因为那块区域跟超类重叠了。唯有苹果将超类改为以前的布局才能拯救我们,但这样也导致它们不能再拓展它们的框架了,因为成员变量布局被死死地固定了。在脆弱的实例变量(Fragile
ivars)环境下我们需要重新编译继承自Apple的类来恢复兼容性。那么在健壮的实例变量下回发生什么呢?
在健壮的实例变量下编译器生成的实例变量布局跟以前一样,但是当runtime系统检测到与超类有部分重叠时它会调整你新添加的实例变量的位移,那样你在子类中新添加的成员就被保护起来了。
需要注意的是在健壮的实例变量下,不要使用sizeof(SomeClass),而是用class_getInstanceSize([SomeClass
class])代替;也不要使用offsetof(SomeClass,
SomeIvar),而要用ivar_getOffset(class_getInstanceVariable([SomeClass
class], "SomeIvar"))来代替。
7Objective-C Associated Objects
在OS X 10.6之后,Runtime系统让OC支持向对象动态添加变量。涉及到的函数有以下三个:
voidobjc_setAssociatedObject(idobject,constvoid*key,idvalue,objc_AssociationPolicypolicy);
idobjc_getAssociatedObject(idobject,constvoid*key);
voidobjc_removeAssociatedObjects(idobject);
这些方法以键值对的形式动态地向对象添加、获取或删除关联值。其中关联政策是一组枚举常量:
enum{
OBJC_ASSOCIATION_ASSIGN=0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC=1,
OBJC_ASSOCIATION_COPY_NONATOMIC=3,
OBJC_ASSOCIATION_RETAIN=01401,
OBJC_ASSOCIATION_COPY=01403
};
这些常量对应着引用关联值的政策,也就是OC内存管理的引用计数机制。
8总结
我们之所以让自己的类继承NSObject不仅仅因为苹果帮我们完成了复杂的内存分配问题,更是因为这使得我们能够用上Runtime系统带来的便利。可能我们平时写代码时可能很少会考虑一句简单的[receiver
message]背后发生了什么,而只是当做方法或函数调用。深入理解Runtime系统的细节更有利于我们利用消息机制写出功能更强大的代码,比如Method Swizzling等。
9参考链接
–Objective-C Runtime Programming Guide
–Understanding the Objective-C Runtime
Objective-C Runtime Programming Guide
深入理解Objective-C的Runtime机制
http://www.csdn.net/article/2015-07-06/2825133-objective-c-runtime/1
Objective-C Runtime运行时之一:类与对象
http://southpeak.github.io/blog/2014/10/25/objective-c-runtime-yun-xing-shi-zhi-lei-yu-dui-xiang/
Objective-C Runtime运行时之二:成员变量与属性
Objective-C Runtime运行时之三:方法与消息
Objective-C Runtime运行时之四:Method Swizzling
Objective-C Runtime运行时之五:协议与分类
Objective-C Runtime运行时之六:拾遗
http://southpeak.github.io/blog/2014/11/09/objective-c-runtime-yun-xing-shi-zhi-liu-:shi-yi/