IOS开发系列——Objective-c Runtime专题总结【整理】

Objective-c Runtime专题总结

 

原文  http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/

 

1    OC与Runtime的交互方式

   OC 从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码,通过Foundation 框架的 NSObject 类定义的方法,通过对 runtime 函数的直接调用。

1.1     Objective-C源代码

大部分情况下你就只管写你的OC代码就行,runtime 系统自动在幕后辛勤劳作着。

1.2     NSObject的方法

Cocoa 中大多数类都继承于NSObject 类,也就自然继承了它的方法。最特殊的例外是 NSProxy ,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类。

    有的NSObject 中的方法起到了抽象接口的作用,比如 description 方法需要你重载它并为你定义的类提供描述内容。 NSObject 还有些方法能在运行时获得类的信息,并检查一些特性,比如class 返回对象的类; isKindOfClass: 和isMemberOfClass: 则检查对象是否在指定的类继承体系中;respondsToSelector: 检查对象能否响应指定的消息;conformsToProtocol: 检查对象是否实现了指定协议类的方法;methodForSelector: 则返回指定方法实现的地址。

1.3    Runtime的函数

    Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于 /usr/include/objc 目录下。许多函数允许你用纯C代码来重复实现 OC 中同样的功能。虽然有一些方法构成了 NSObject 类的基础,但是你在写OC 代码时一般不会直接用到这些函数的,除非是写一些 OC 与其他语言的桥接或是底层的debug工作。在Objective-C Runtime Reference 中有对 Runtime函数的详细文档。

2     Runtime术语

id objc_msgSend ( id self, SELop, ... );

 

2.1    SEL

    objc_msgSend 函数第二个参数类型为 SEL ,它是selector 在OC中的表示类型(Swift中是Selector类)。 selector 是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是 SEL :

typedef struct objc_selector*SEL;

 

其实它就是个映射到方法的C字符串,你可以用 OC 编译器命令 @selector() 或者 Runtime 系统的 sel_registerName 函数来获得一个 SEL 类型的方法选择器。

不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是OC 中方法命名有时会带上参数类型( NSNumber 一堆抽象工厂方法拿走不谢),Cocoa 中有好多长长的方法哦。

2.2    Id 与objc_object结构体

objc_msgSend 第一个参数类型为 id ,大家对它都不陌生,它是一个指向类实例的指针:

typedefstructobjc_object*id;

 objc_object 又是啥呢:

structobjc_object{Classisa;};

objc_object 结构体包含一个 isa 指针,根据 isa 指针就可以顺藤摸瓜找到对象所属的类。

2.3    Class

之所以说 isa 是指针是因为 Class 其实是一个指向 objc_class 结构体的指针:

typedefstructobjc_class*Class;

 objc_class 就是我们摸到的那个瓜,里面的东西多着呢:

structobjc_class{     
Classisa  OBJC_ISA_AVAILABILITY; 
#if !__OBJC2__     
Classsuper_class             OBJC2_UNAVAILABLE;     
constchar*name               OBJC2_UNAVAILABLE;     
longversion                    OBJC2_UNAVAILABLE;     
longinfo                        OBJC2_UNAVAILABLE;     
longinstance_size             OBJC2_UNAVAILABLE;     
structobjc_ivar_list*ivarsOBJC2_UNAVAILABLE;     
structobjc_method_list**methodLists    OBJC2_UNAVAILABLE;     
structobjc_cache*cache     OBJC2_UNAVAILABLE;     
structobjc_protocol_list*protocols    OBJC2_UNAVAILABLE;
#endif  
}OBJC2_UNAVAILABLE;

 

 

    可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。

其中 objc_ivar_list  objc_method_list 分别是成员变量列表和方法列表:

structobjc_ivar_list{     
intivar_count               OBJC2_UNAVAILABLE;#ifdef __LP64__     intspace                   OBJC2_UNAVAILABLE;
#endif     /* variable length structure */     
structobjc_ivarivar_list[1]      OBJC2_UNAVAILABLE;
}  OBJC2_UNAVAILABLE;  
structobjc_method_list{     
structobjc_method_list*obsolete      OBJC2_UNAVAILABLE;  
intmethod_count              OBJC2_UNAVAILABLE;
#ifdef __LP64__     
intspace                       OBJC2_UNAVAILABLE;
#endif     
/* variable length structure */     
structobjc_methodmethod_list[1]     OBJC2_UNAVAILABLE;
}

2.4    元类 (MetaClass)

    一个ObjC 类同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西。当你发出一个类似 [NSObjectalloc] 的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。你会说NSObject 的子类时,你的类就会指向 NSObject 做为其超类。但是所有的元类都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当[NSObjectalloc] 这条消息发给类对象的时候, objc_msgSend() 会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。

    上图实线是super_class 指针,虚线是isa指针。 有趣的是根元类的超类是NSObject,而isa指向了自己,而NSObject的超类为nil,也就是它没有超类。

2.5    Method

Method 是一种代表类中的某个方法的类型。

typedefstructobjc_method*Method;

 objc_method 在上面的方法列表中提到过,它存储了方法名,方法类型和方法实现:

structobjc_method{     
SELmethod_name                       OBJC2_UNAVAILABLE;     
char*method_types                  OBJC2_UNAVAILABLE;     
IMPmethod_imp                       OBJC2_UNAVAILABLE;
}OBJC2_UNAVAILABLE;

·      方法名类型为 SEL 前面提到过相同名字的方法即使在不同类中定义它们的方法选择器也相同

·      方法类型 method_types 是个 char 指针其实存储着方法的参数类型和返回值类型

·      method_imp 指向了方法的实现本质上是一个函数指针后面会详细讲到

 

2.6    Ivar

Ivar 是一种代表类中实例变量的类型。

typedefstructobjc_ivar*Ivar;

 objc_ivar 在上面的成员变量列表中也提到过:

tructobjc_ivar{     
char*ivar_name                 OBJC2_UNAVAILABLE;     
char*ivar_type                 OBJC2_UNAVAILABLE;     
intivar_offset                 OBJC2_UNAVAILABLE;
#ifdef __LP64__     
intspace                        OBJC2_UNAVAILABLE;
#endif
}OBJC2_UNAVAILABLE;

PS: OBJC2_UNAVAILABLE 之类的宏定义是苹果在 OC 中对系统运行版本进行约束的黑魔法,有兴趣的可以查看源代码。

 

2.7    IMP 函数指针

IMP  objc.h 中的定义是:

    typedefid(*IMP)(id,SEL,...);

  它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而IMP这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面会提到。

你会发现 IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含 id SEL 类型。每个方法名都对应一个 SEL 类型的方法选择器,而每个实例对象中的SEL 对应的方法实现肯定是唯一的,通过一组 id  SEL 参数就能确定唯一的方法实现地址;反之亦然。

 

2.8    Cache

 runtime.h Cache的定义如下:

typedefstructobjc_cache*Cache

还记得之前 objc_class 结构体中有一个 struct objc_cache *cache 吧,它到底是缓存啥的呢,先看看 objc_cache 的实现:

structobjc_cache{     
unsignedintmask/* total = mask + 1 */   OBJC2_UNAVAILABLE;
unsignedintoccupied                OBJC2_UNAVAILABLE;
Methodbuckets[1]                      OBJC2_UNAVAILABLE;
};

Cache 为方法调用的性能进行优化通俗地讲每当实例对象接收到一个消息时它不会直接在 isa 指向的类的方法列表中遍历查找能够响应消息的方法因为这样效率太低了而是优先在 Cache 中查找Runtime 系统会把被调用的方法存到Cache 理论上讲一个方法如果被调用那么它有可能今后还会被调用下次查找的时候效率更高

 

3    消息

OC中发送消息是用中括号 []把接收者和消息括起来而直到运行时才会把消息与方法实现绑定

3.1    objc_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 {    

id  target = getTheReceiver();    

SEL method = getTheMethod();     

if ( target == self || method == _cmd )   return nil

return [target performSelector:method];

}

在这两个参数中, self 更有用。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。而当方法中的 super 关键字接收到消息时,编译器会创建一个 objc_super 结构体:

struct objc_super { id receiver; Class class; };

这个结构体指明了消息应该被传递给特定超类的定义。

3.3    获取方法地址

  IMP那节提到过可以避开消息绑定而直接获取方法的地址并调用方法。这种做法很少用,除非是需要持续大量重复调用某方法的极端情况,避开消息发送泛滥而直接调用该方法会更高效。

NSObject 类中有个 methodForSelector: 实例方法,你可以用它来获取某个方法选择器对应的 IMP ,举个栗子:

void (*setter)(id, SEL, BOOL);

int i; 

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 关键字在类的实现文件中修饰一个属性:

@dynamic propertyName;

   这表明我们会为这个属性动态提供存取方法,也就是说编译器不会再默认为我们生成 setPropertyName:  propertyName 方法,而需要我们动态提供。我们可以通过分别重载 resolveInstanceMethod:  resolveClassMethod: 方法分别添加实例方法实现和类方法实现。因为当Runtime 系统在 Cache 和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:  resolveClassMethod: 来给程序员一次动态添加方法实现的机会。我们需要用 class_addMethod 函数完成向特定类添加特定方法实现的操作:

void dynamicMethodIMP(id self, SEL _cmd) {    

    // implementation ....

}

@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

  上面的例子为 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:)){        

    return alternateObject;    

}    

    return [super forwardingTargetForSelector:aSelector];

}

  毕竟消息转发要耗费更多时间,抓住这次机会将消息重定向给别人是个不错的选择,不过千万别返回 self ,因为那样会死循环。

 

5.2    转发

  当动态方法解析不作处理返回 NO 时,消息转发机制会被触发,这时forwardInvocation: 方法会被执行,我们可以重载这个方法来定义我们的转发逻辑:

- (void)forwardInvocation:(NSInvocation *)anInvocation {    

if ([someOtherObject respondsToSelector: [anInvocation selector]])

    [anInvocation invokeWithTarget:someOtherObject];    

else [super forwardInvocation: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 类,发布新版本的系统的话,那就悲剧了:

我们自定义的类被划了两道线,那是因为那块区域跟超类重叠了。唯有苹果将超类改为以前的布局才能拯救我们,但这样也导致它们不能再拓展它们的框架了,因为成员变量布局被死死地固定了。在脆弱的实例变量(Fragileivars) 环境下我们需要重新编译继承自 Apple 的类来恢复兼容性。那么在健壮的实例变量下回发生什么呢?

在健壮的实例变量下编译器生成的实例变量布局跟以前一样,但是当 runtime系统检测到与超类有部分重叠时它会调整你新添加的实例变量的位移,那样你在子类中新添加的成员就被保护起来了。

需要注意的是在健壮的实例变量下,不要使用 sizeof(SomeClass) ,而是用class_getInstanceSize([SomeClassclass]) 代替;也不要使用offsetof(SomeClass,SomeIvar) ,而要用ivar_getOffset(class_getInstanceVariable([SomeClassclass], "SomeIvar"))来代替。

 

7    Objective-C Associated Objects

OS X 10.6 之后,Runtime系统让OC支持向对象动态添加变量。涉及到的函数有以下三个:

void objc_setAssociatedObject(idobject,constvoid*key,idvalue,objc_AssociationPolicypolicy);
id objc_getAssociatedObject(idobject,constvoid*key);
void objc_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 系统的细节更有利于我们利用消息机制写出功能更强大的代码,比如 MethodSwizzling 等。

9    参考链接

– Objective-C Runtime Programming Guide

– Objective-C runtime之运行时的基本特点

– Understanding the Objective-C Runtime

 

Objective-C Runtime Programming Guide

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html

 

深入理解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 运行时之二:成员变量与属性

http://southpeak.github.io/blog/2014/10/30/objective-c-runtime-yun-xing-shi-zhi-er-:cheng-yuan-bian-liang-yu-shu-xing/

 

Objective-C Runtime 运行时之三:方法与消息

http://southpeak.github.io/blog/2014/11/03/objective-c-runtime-yun-xing-shi-zhi-san-:fang-fa-yu-xiao-xi-zhuan-fa/

 

Objective-C Runtime 运行时之四:MethodSwizzling

http://southpeak.github.io/blog/2014/11/06/objective-c-runtime-yun-xing-shi-zhi-si-:method-swizzling/

 

Objective-C Runtime 运行时之五:协议与分类

http://southpeak.github.io/blog/2014/11/08/objective-c-runtime-yun-xing-shi-zhi-wu-:xie-yi-yu-fen-lei/

 

Objective-C Runtime 运行时之六:拾遗

http://southpeak.github.io/blog/2014/11/09/objective-c-runtime-yun-xing-shi-zhi-liu-:shi-yi/

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值