Runtime

来源:互联网 发布:大数据文献综述研究 编辑:程序博客网 时间:2024/05/03 08:31

本博文总结于南峰子Runtime(博客地址:http://southpeak.github.io/2014/10/25/objective-c-runtime-1/ 

行时之一:类与对象

一、类与对象基础数据结构

1、 Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。

它的定义如下:typedef struct objc_class *Class;

2、isa:需要注意的是在Objective-C中,所有的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类)

3、cache:用于缓存最近使用的方法。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。

4、元类(Meta Class):meta-class是一个类对象的类。

meta-class也是一个类,也可以向它发送一个消息,meta-class的isa指向基类的meta-class,以此作为它们的所属类。即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。


运行时之二:成员变量与属性
一、类型编码(Type Encoding)
作为对Runtime的补充,编译器将每个方法的返回值和参数类型编码为一个字符串,并将其与方法的selector关联在一起。这种编码方案在其它情况下也是非常有用的,因此我们可以使用@encode编译器指令来获取它。当给定一个类型时,@encode返回这个类型的字符串编码。这些类型可以是诸如int、指针这样的基本类型,也可以是结构体、类等类型。事实上,任何可以作为sizeof()操作参数的类型都可以用于@encode()。
二、成员变量、属性
1、Ivar是表示实例变量的类型,其实际是一个指向objc_ivar结构体的指针,其定义如下:
 typedef struct objc_ivar *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     }
2、objc_property_t是表示Objective-C声明的属性的类型,其实际是指向objc_property结构体的指针,
其定义如下:
typedef struct objc_property *objc_property_t;
三、关联对象(associated objc)
如果指定的策略是assign,则宿主释放时,关联对象不会被释放;而如果指定的是retain或者是copy,则宿主释放时,关联对象会被释放。
     staticcharmyKey;
     objc_setAssociatedObject(self, &myKey, anObject, OBJC_ASSOCIATION_RETAIN);

     idanObject = objc_getAssociatedObject(self, &myKey);
我们可以使用objc_removeAssociatedObjects函数来移除一个关联对象,或者使用objc_setAssociatedObject函数          将key指定的关联对象设置为nil。


运行时之二:方法与消息   
一、基础数据类型
1、SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:
typedef struct objc_selector *SEL;
objc_selector结构体的详细定义没有在<objc/runtime.h>头文件中找到。方法的selector用于表示运行时方法的名字。                    Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。
两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么方法的SEL就是一样的。每一个方法都对应着一个SEL。所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。相同的方法只能对应一个SEL。这也就导致Objective-C在处理相同方法名且参数个数相同但类型不同的方法方面的能力很差。
当然,不同的类可以拥有相同的selector,这个没有问题。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。
工程中的所有的SEL组成一个Set集合,Set的特点就是唯一,因此SEL是唯一的。因此,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度上无语伦比!!但是,有一个问题,就是数量增多会增大hash冲突而导致的性能下降(或是没有冲突,因为也可能用的是perfect hash)。但是不管使用什么样的方法加速,如果能够将总量减少(多个方法可能对应同一个SEL),那将是最犀利的方法。那么,我们就不难理解,为什么SEL仅仅是函数名了。
本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。这个查找过程我们将在下面讨论。
我们可以在运行时添加新的selector,也可以在运行时获取已存在的selector,我们可以通过下面三种方法来获取SEL:
     sel_registerName函数
     Objective-C编译器提供的@selector()
     NSSelectorFromString()方法
2、IMP实际上是一个函数指针,指向方法实现的首地址。其定义如下:
     id (*IMP)(id, SEL, ...)
     这个函数使用当前CPU架构实现的标准的C调用约定。第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),接下来是方法的实际参数列表。
     前面介绍过的SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的IMP,查找过程将在下面讨论。取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针了。
     通过取得IMP,我们可以跳过Runtime的消息传递机制,直接执行IMP指向的函数实现,这样省去了Runtime消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些。
3、Method用于表示类定义中的方法,则定义如下:
 typedef struct objc_method *Method;     struct objc_method {         SEL method_name                    OBJC2_UNAVAILABLE;    // 方法名         char *method_types                    OBJC2_UNAVAILABLE;         IMP method_imp                         OBJC2_UNAVAILABLE;    // 方法实现     }
     我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。

二、方法调用流程
1、隐藏参数
     objc_msgSend有两个隐藏参数:
         1    消息接收对象
         2    方法的selector
          这两个参数为方法的实现提供了调用者的信息。之所以说是隐藏的,是因为它们在定义方法的源代码中没有声明。          它们是在编译期被插入实现代码的。
     虽然这些参数没有显示声明,但在代码中仍然可以引用它们。我们可以使用self来引用接收者对象,使用_cmd来引用选择器。如下代码所示:
   - strange     {         id  target = getTheReceiver();         SEL method = getTheMethod();         if ( target == self || method == _cmd )             return nil;         return [target performSelector:method];     }
     当然,这两个参数我们用得比较多的是self,_cmd在实际中用得比较少。

三、消息转发
      1、动态方法解析
     对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个”处理方法””。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。如下代码所示:
voidfunctionForMethod1(idself, SEL _cmd) {<span style="white-space:pre"></span>NSLog(@"%@, %p",self, _cmd);}+ (BOOL)resolveInstanceMethod:(SEL)sel {<span style="white-space:pre"></span>NSString*selectorString =NSStringFromSelector(sel);<span style="white-space:pre"></span>if([selectorString isEqualToString:@"method1"]) {       <span style="white-space:pre"></span>class_addMethod(self.class,@selector(method1), (IMP)functionForMethod1,"@:");    <span style="white-space:pre"></span>}<span style="white-space:pre"></span>return[superresolveInstanceMethod:sel];}
  不过这种方案更多的是为了实现@dynamic属性。

2、备用接收者
如果在上一步无法处理消息,则Runtime会继续调以下方法:
- (id)forwardingTargetForSelector:(SEL)aSelector

如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。

使用这个方法通常是在对象内部,可能还有一系列其它对象能处理该消息,我们便可借这些对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。如下代码所示:
@interfaceSUTRuntimeMethodHelper :NSObject- (void)method2;@end@implementationSUTRuntimeMethodHelper- (void)method2 {<span style="white-space:pre"></span>NSLog(@"%@, %p",self, _cmd);}@end#pragma mark -@interfaceSUTRuntimeMethod () {    SUTRuntimeMethodHelper *_helper;}@end@implementationSUTRuntimeMethod+ (instancetype)object {<span style="white-space:pre"></span>return [[self alloc] init];}- (instancetype)init {<span style="white-space:pre"></span>self = [super init];<span style="white-space:pre"></span>if (self !=nil) {        <span style="white-space:pre"></span>_helper = [[SUTRuntimeMethodHelper alloc] init];    <span style="white-space:pre"></span>}<span style="white-space:pre"></span>returnself;}- (void)test {    [self performSelector:@selector(method2)];}- (id)forwardingTargetForSelector:(SEL)aSelector {<span style="white-space:pre"></span>NSLog(@"forwardingTargetForSelector");<span style="white-space:pre"></span>NSString *selectorString =NSStringFromSelector(aSelector);<span style="white-space:pre"></span>// 将消息转发给_helper来处理<span style="white-space:pre"></span>if ([selectorString isEqualToString:@"method2"]) {<span style="white-space:pre"></span>return _helper;  <span style="white-space:pre"></span>  }<span style="white-space:pre"></span>return [super forwardingTargetForSelector:aSelector];}@end
这一步合适于我们只想将消息转发到另一个能处理该消息的对象上。但这一步无法对消息进行处理,如操作消息的参数和返回值。

3、完整消息转发
如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会调用以下方法:
-(void)forwardInvocation:(NSInvocation *)anInvocation
     
运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation方法中选择将消息转发给其它对象。

forwardInvocation:方法的实现有两个任务:

  1. 定位可以响应封装在anInvocation中的消息的对象。这个对象不需要能处理所有未知消息。
  2. 使用anInvocation作为参数,将消息发送到选中的对象。anInvocation将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。
 不过,在这个方法中我们可以实现一些更复杂的功能,我们可以对消息的内容进行修改,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。

还有一个很重要的问题,我们必须重写以下方法:
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector

消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。
完整的示例如下所示:
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector {<span style="white-space:pre"></span>NSMethodSignature*signature = [supermethodSignatureForSelector:aSelector];<span style="white-space:pre"></span>if(!signature) {<span style="white-space:pre"></span>if([SUTRuntimeMethodHelper instancesRespondToSelector:aSelector]) {            signature = [SUTRuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];        }    }<span style="white-space:pre"></span>returnsignature;}- (void)forwardInvocation:(NSInvocation*)anInvocation {<span style="white-space:pre"></span>if([SUTRuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {        [anInvocation invokeWithTarget:_helper];    }}

NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。

从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。这取决于具体的实现。


运行时之四:Method Swizzling
一、黑魔法
 MethodfromMethod =class_getInstanceMethod([selfclass],@selector(action_1)); MethodtoMethod =class_getInstanceMethod([selfclass],@selector(action_2));    //通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了    if(!class_addMethod([selfclass],@selector(action_1),method_getImplementation(toMethod),method_getTypeEncoding(toMethod))) {        method_exchangeImplementations(fromMethod, toMethod);    }

 Swizzling应该总是在+load中执行
在Objective-C中,运行时会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证—事实上,如果在应用中没为给这个类发送消息,则它可能永远不会被调用。

Swizzling应该总是在dispatch_once中执行
与上面相同,因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保这种行为,我们应该将其作为method swizzling的最佳实践。

二、选择器、方法与实现
在Objective-C中,选择器(selector)、方法(method)和实现(implementation)是运行时中一个特殊点,虽然在一般情况下,这些术语更多的是用在消息发送的过程描述中。

以下是Objective-C Runtime Reference中的对这几个术语一些描述:

  1. Selector(typedef struct objc_selector *SEL):用于在运行时中表示一个方法的名称。一个方法选择器是一个C字符串,它是在Objective-C运行时被注册的。选择器由编译器生成,并且在类被加载时由运行时自动做映射操作。
  2. Method(typedef struct objc_method *Method):在类定义中表示方法的类型
  3. Implementation(typedef id (*IMP)(id, SEL, ...)):这是一个指针类型,指向方法实现函数的开始位置。这个函数使用为当前CPU架构实现的标准C调用规范。每一个参数是指向对象自身的指针(self),第二个参数是方法选择器。然后是方法的实际参数。
理解这几个术语之间的关系最好的方式是:一个类维护一个运行时可接收的消息分发表;分发表中的每个入口是一个方法(Method),其中key是一个特定名称,即选择器(SEL),其对应一个实现(IMP),即指向底层C函数的指针。

为了swizzle一个方法,我们可以在分发表中将一个方法的现有的选择器映射到不同的实现,而将该选择器对应的原始实现关联到一个新的选择器中

三、注意事项
 Swizzling通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。虽然它不是最安全的,但如果遵从以下几点预防措施的话,还是比较安全的:
  1. 总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分。
  2. 避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。
  3. 明白是怎么回事:简单地拷贝粘贴swizzle代码而不理解它是如何工作的,不仅危险,而且会浪费学习Objective-C运行时的机会。阅读Objective-C Runtime Reference和查看<objc/runtime.h>头文件以了解事件是如何发生的。
  4. 小心操作:无论我们对Foundation, UIKit或其它内建框架执行Swizzle操作抱有多大信心,需要知道在下一版本中许多事可能会不一样。


运行时之五:拾遗
1、super
在Objective-C中,如果我们需要在类的方法中调用父类的方法时,通常都会用到super,如下所示:
@interfaceMyViewController:UIViewController@end@implementationMyViewController- (void)viewDidLoad {     [super viewDidLoad];// do something     ...}@end

 如何使用super我们都知道。现在的问题是,它是如何工作的呢?
     首先我们需要知道的是superself不同。self是类的一个隐藏参数,每个方法的实现的第一个参数即为self。而super并不是隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用viewDidLoad方法时,去调用父类的方法,而不是本类中的方法。而它实际上与self指向的是相同的消息接收者。为了理解这一点,我们先来看看super的定义:

struct objc_super { id receiver; Class      superClass; };
这个结构体有两个成员:
  1. receiver:即消息的实际接收者
  2. superClass:指针当前类的父类
当我们使用super来接收消息时,编译器会生成一个objc_super结构体。就上面的例子而言,这个结构体的receiver就是MyViewController对象,与self相同;superClass指向MyViewController的父类UIViewController。
接下来,发送消息时,不是调用objc_msgSend函数,而是调用objc_msgSendSuper函数,其声明如下:
id objc_msgSendSuper ( struct objc_super *super, SEL op, ... );

该函数第一个参数即为前面生成的objc_super结构体,第二个参数是方法的selector。该函数实际的操作是:从objc_super结构体指向的superClass的方法列表开始查找viewDidLoad的selector,找到后以objc->receiver去调用这个selector,而此时的操作流程就是如下方式了
     objc_msgSend(objc_super->receiver, @selector(viewDidLoad))

由于objc_super->receiver就是self本身,所以该方法实际与下面这个调用是相同的:
     objc_msgSend(self, @selector(viewDidLoad))
 2、块操作
     我们都知道block给我们带到极大的方便,苹果也不断提供一些使用block的新的API。同时,苹果在runtime中也提供了一些函数来支持针对block的操作,这些函数包括:
// 创建一个指针函数的指针,该函数调用时会调用特定的blockIMP imp_implementationWithBlock ( id block );// 返回与IMP(使用imp_implementationWithBlock创建的)相关的blockid imp_getBlock ( IMP anImp );// 解除block与IMP(使用imp_implementationWithBlock创建的)的关联关系,并释放block的拷贝BOOL imp_removeBlock ( IMP anImp );

imp_implementationWithBlock函数:参数block的签名必须是method_return_type ^(id self, method_args …)形式的。该方法能让我们使用block作为IMP。如下代码所示:
@interface MyRuntimeBlock : NSObject@end@implementation MyRuntimeBlock@end// 测试代码IMP imp = imp_implementationWithBlock(^(id obj,NSString *str) {<span style="white-space:pre"></span>NSLog(@"%@", str);});class_addMethod(MyRuntimeBlock.class, @selector(testBlock:), imp,"v@:@");MyRuntimeBlock *runtime = [[MyRuntimeBlock alloc] init];[runtime performSelector:@selector(testBlock:) withObject:@"hello world!"];

3、弱引用操作
// 加载弱引用指针引用的对象并返回id objc_loadWeak ( id *location );// 存储__weak变量的新值id objc_storeWeak ( id *location, id obj );</span>

objc_loadWeak函数:该函数加载一个弱指针引用的对象,并在对其做retainautoreleasing操作后返回它。这样,对象就可以在调用者使用它时保持足够长的生命周期。该函数典型的用法是在任何有使用__weak变量的表达式中使用。
objc_storeWeak函数:该函数的典型用法是用于__weak变量做为赋值对象时。

0 0
原创粉丝点击