Runtime概念

runtime

运行时语言,实现Object-C的C语言库,将OC转换成C进行编译的过渡者。

作为一门动态编程语言,Objective-C 会尽可能的将编译和链接时要做的事情推迟到运行时。只要有可能,Objective-C 总是使用动态 的方式来解决问题。这意味着 Objective-C 语言不仅需要一个编译环境,同时也需要一个运行时系统来执行编译好的代码,这就是runtime

运行时系统(runtime)扮演的角色类似于 Objective-C 语言的操作系统,Objective-C 基于该系统来工作。因此,runtime好比Objective-C的灵魂,很多东西都是在这个基础上出现的。所以它是值的你花功夫去理解的。

实现

主要是用C语言实现,部分由汇编语言。实际上正是runtime将OC中面向对象的类转换成C语言中面向过程的结构体。无论是实例对象还是类对象,实际上对应的都是C中的结构体。

使用

iskindofclass和isMemberOfClass就是两个典型的运行时方法。
同时注意使用class_addMethod等方法时要引入#import <objc/runtime.h>头文件。

关于32位与64位

runtime分为早期版本和现行版本,32位使用早期,64位使用现行。32位已被淘汰。
64位与32位的区别在于,操作系统上64位用于3D动画等先进功能32位用于日常简单使用;处理器上64位比32位更宽处理能力更强;软件上基于32位和64位开发的不同;内存容量不同。现在必须支持arm64,代码中主要注意类型长度上的区别。ios推荐使用的NSInteger区别于int的地方也就是在于前者可以根据系统位数使用较大的长度。

与传统静态语言C语言的区别

很常见的一个消息发送语句:

[receiver message]

会被编译器转化成

objc_msgSend(receiver, selector)

如果有参数则为

objc_msgSend(receiver, selector, arg1, arg2, …) 

receiver:消息接受者(receiver),self,某个类
selector:方法名(message)
arg:参数
传统的静态语言,编译器会自上而下,在存在调用函数时会进入调用函数,逐句实现编译。
而OC在编译过程中并不会这样做,只是编辑成明确方法名调用者参数的形式,该函数被如何实现以及是否实现并不关心。只有在程序运行时才会根据方法名进入具体函数中,这也就是会在运行中找不到对应函数时会造成崩溃的原因。
OC实际上是使用消息传递机制代替传统C语言的函数调用。
_CMD,SEL类型,可以获取当前方法名的关键字。

- (void)message  
{  
    self.name = @"James";//通过self关键字给当前对象的属性赋值  
    SEL currentSel = _cmd;//通过_cmd关键字取到当前函数对应的SEL  
    NSLog(@"currentSel is :%s",(char *)currentSel);  
}

打印结果

ObjcRunTime[693:403] currentSel is :message

初步理解,其实objc_msgSend 所做的事情,就是通过我们传入的self 指针,找到class 的method_list 然后根据SEL 做比较,没有的话,就在super class 找,如此往复。直到找到匹配的SEL,然后,call imp。当然,runtime在其中又做了很多优化。

OC中的消息传递

OC中的消息传递与C中函数调用的最大区别在于OC中的消息传递在时运行时中进行的,而函数调用是在编译时就可以进行的。

id num = @123;
//输出123
NSLog(@"%@", num);
//程序崩溃,报错[__NSCFNumber appendString:]: unrecognized selector sent to instance 0x7b27
[num appendString:@"Hello World"];

上述代码没有任何问题,id可以使任何类型包括NSString,但是在运行过程中在NSNumber中找不到appendString这个方法,所以会报错。所以消息传递的强大之处在于可以在运行时添加新方法,缺点在于无法再编译时发现错误。

OC中的消息传递转化为C的函数调用

简单的创建Person对象

//为了方便查看转写后的C语言代码,将alloc和init分两步完成
Person *p = [Person alloc];
p = [p init];
p.name = @"Jiaming Chen";
[p showMyself];

使用clang -rewrite-objc main.m可以转化成C

Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("init"));
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_f5b408_mi_1);
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("showMyself"));

第一行代码简要表示为

Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));

第一步,获取Person类,第二步注册alloc方法,然后通过objc_msgSend将alloc消息发送给消息接受者Person。
第二行代码简要表示为

p = objc_msgSend(p, sel_registerName("init"));

由此可见,对于OC而言都是以消息传递实现,而正是runtime通过objc_msgSend将一个面向对象的消息传递转换成了一个面向过程的的函数调用。

消息转发

我们知道消息传递实际上做的事就是利用objc_msgSend,将消息传递给接受者,在接受者的结构体中的objc_method_list中查找方法。如果找到就可以直接调用,如果找不到就会通过super_class指针去它的父类中查找,直至基类NSObject。如果还是没有找到就会调用NSObject的doesNotRecognizeSelector报出unrecognized的错误。也就是我们常出现的unrecognized selector sent to instance错误(对象提前被释放或者为空导致无法识别其方法)。
但是在此之前,还有三次机会来处理这个消息来避免出现崩溃,这就是所谓的消息转发。详细见后文。

runtime源码学习
  • selector
    selector可以叫做选择器,其实指的就是对象的方法,也可以理解为C语言里面的函数指针,面向对象里面的对应概念。
    @selector(xxxx)的作用是找到名字为xxxx的方法。一般用于[a performSelector:@selector(b)];就是说去调用a对象的b方法,和[a b];的意思一样,但是这样更加动态一些。@selector(xxxx)返回的类型是SEL,看方法说明的时候如果参数类型是SEL,那么就是要接受@selector(xxxx)返回的值。
    在Objc中 SEL的定义是:
typedef struct objc_selector *SEL;  

我们注意到对于SEL的定义,实际上就是使用typedef将结构体objc_selector重新命名为*SEL。而SEL则是指向这个结构体的指针,所以SEL的对象不需要再加*。
在Mac OS X中SEL其实被映射为一个C字符串,可以看作是方法的名字(编号),它并不一个指向具体方法实现(IMP类型才是)。对于所有的类,只要方法名是相同的,产生的selector都是一样的。
简而言之,你可以理解 @selector()就是取类方法的编号,他的行为基本可以等同C语言的中函数指针,只不过C语言中,可以把函数名直接赋给一个函数指针,而Object-C的类不能直接应用函数指针,这样只能做一个@selector语法来取。
它的结果是一个SEL类型。这个类型本质是类方法的名字(编号)。

注意1. @selector是查找当前类的方法,而[object @selector(方法名:方法参数..) ] ;是取object对应类的相应方法.
注意2.查找类方法时,除了方法名,方法参数也查询条件之一.
注意3. 可以用字符串来找方法 SEL 变量名 = NSSelectorFromString(方法名字的字符串);
注意4. 可以运行中用SEL变量反向查出方法名字字符串
NSString *变量名 = NSStringFromSelector(SEL参数);

runtime 在实现selector时,实现了一个很大的Set,简单的说就是一个经过了优化过的hash表。而Set的特点就是唯一,也就是SEL是唯一的。那么对于字符串的比较仅仅需要比较他们的地址就可以了。 所以OC中不允许使用方法名一样参数不一样的函数,编译器会根据每个方法的方法名生成唯一的SEL。所以,在速度上是无与伦比的。
selector主要用于两个对象之间进行松耦合的通讯.这种方法很多开发环境用到。比如GTK,Delphi.基本上整个Cocoa库之间对象,控制之间通讯都是在这个基础构建的。
用户行为统计中的经典运用:

+ (void)setUpAnalytics
{
   __weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
     //读取配置文件,获取需要统计的事件列表
     for (NSString *classNameString in analyticsData.allKeys) {
         //使用运行时创建类对象
         const char *className = [classNameString UTF8String];
         //从一个字串返回一个类
         Class newClass = objc_getClass(className);
         NSArray *pageEventList = [[analyticsData objectForKey:classNameString] objectForKey:Event];
         for (NSDictionary *eventDict in pageEventList) {
             //事件方法名称
             NSString *eventMethodName = eventDict[MethodName];
             SEL seletor = NSSelectorFromString(eventMethodName);
             NSString *eventId = eventDict[EventId];
             [weakSelf trackEventWithClass:newClass selector:seletor event:eventId];
             [weakSelf uAnalyticstrackEventWithEventdata:eventDict];
         }
     }
   });
}
  • id
    id是通用类型指针,能够表示任何对象,换句话说,id 类型的变量可以存放任何数据类型的对象。在内部处理上,这种类型被定义为指向对象的指针,实际上是一个指向这种对象的实例变量的指针。
    查看到id数据结构如下:
// Represents an instance of a class.  
struct objc_object {  
    Class isa  OBJC_ISA_AVAILABILITY;  
};  
// A pointer to an instance of a class.  
typedef struct objc_object *id; 

实际上*id就是结构体objc_object的别名。而id则是一个指向objc_object结构体指针,它包含一个Class isa成员,根据isa指针就可以顺藤摸瓜找到对象所属的类。需要注意的是id 是一个指针,所以在使用id的时候不需要加星号。

NSString *str = [[NSString alloc] init];

这里我们定义的对象str,实际上同样是一个objc_object结构体指针。

  • Class
    isa指针的数据类型是Class,Class表示对象所属的类。
// An opaque type that represents an Objective-C class.  
typedef struct objc_class *Class;  

可以查看到Class其实就是一个objc_class结构体指针。class类型和id类型一样,本身继承于指针类型,用于储存类对象的指针,故在声明class类型变量时和id一样不需要*号。

NSString *str = [[NSString alloc] init];
Class c = [str Class];

这里我们定义的c,实际上同样是一个objc_class结构体指针。
查看到objc_class结构体定义如下:

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;  
/* Use `Class` instead of `struct objc_class *` */  

OC的类其实也是一个对象,即类对象,意思就是你可以向一个类发送消息。为了可以调用类方法,这个类的isa指针必须指向一个包含这些类方法的类结构体,也就是元类(meta-class)的概念。meta-class之所以重要,是因为它保存着创建类对象以及类方法所需的所有信息。每个类都会有一个单独的meta-class,因为每个类的类方法基本不可能完全相同。
任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己所属的类。所有的meta-class使用基类的meta-class作为它们的父类,而基类的meta-class也是属于它自己,也就是说基类的meta-class的isa指针指向它自己。


meta图解说明.png

上图实线是super_class指针,虚线是isa指针。有几个关键点需要解释以下:
1.Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。
2.每个Class都有一个isa指针指向唯一的Meta class
3.Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。
4.每个Meta class的isa指针都指向Root class (meta)。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        Class c1 = [p class];
        Class c2 = [Person class];
        //输出 1
        NSLog(@"%d", c1 == c2);
    }
    return 0;
}

由此可见,类对象是单例。对于Person这个类对象来说,只有一个,所有类对象均是如此。
介绍两个函数:

OBJC_EXPORT BOOL class_isMetaClass(Class cls) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
    
OBJC_EXPORT Class object_getClass(id obj) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

class_isMetaClass用于判断Class对象是否为元类,object_getClass用于获取对象的isa指针指向的对象。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        //输出1
        NSLog(@"%d", [p class] == object_getClass(p));
        //输出0
        NSLog(@"%d", class_isMetaClass(object_getClass(p)));
        //输出1
        NSLog(@"%d", class_isMetaClass(object_getClass([Person class])));
        //输出0
        NSLog(@"%d", object_getClass(p) == object_getClass([Person class]));
    }
    return 0;
}

由上看出,实例的isa指针指向类,类的isa指针指向元类。

super_class表示实例对象对应的父类;
name表示类名,我们可以在运行期,通过这个名称查找到该类(通过:id objc_getClass(const char *aClassName))或该类的 metaclass(id objc_getMetaClass(const char *aClassName));
ivars表示多个成员变量,它指向objc_ivar_list结构体。在runtime.h可以看到它的定义:

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;  
}  

objc_ivar_list其实就是一个链表,存储多个objc_ivar,而objc_ivar结构体存储类的单个成员变量信息。
(* struct objc_ivar ivar_list[1] 这个称为结构体数组,顾名思义,结构数组是指能够存放多个结构体类型(objc_ivar)的一种数组(ivar_list)形式。
methodLists表示方法列表,它指向objc_method_list结构体的二级指针,可以动态修改
methodLists的值来添加成员方法,也是Category实现原理,同样也解释Category不能添加属性的原因。
Category只能在运行时中添加属性。代码如下:

///例如可能是这样的使用
  static const void *propertyKey = &propertyKey;
  /// 将value通过运行时绑定到self
  objc_setAssociatedObject(self, propertyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  /// 将value在运行时中通过propertyKey取出绑定的值
  id value = objc_getAssociatedObject(self, propertyKey);

在runtime.h可以看到它的定义

struct objc_method_list {  
  struct objc_method_list *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;  
}  

同理,objc_method_list也是一个链表,存储多个objc_method,而objc_method结构体存储类的某个方法的信息。
cache用来缓存经常访问的方法,它指向objc_cache结构体,后面会重点讲到。
protocols表示类遵循哪些协议。

  • Method
    Method表示类中的某个方法,在runtime.h文件中找到它的定义:
/// An opaque type that represents a method in a class definition.  
typedef struct objc_method *Method;  
struct objc_method {  
    SEL method_name                                          OBJC2_UNAVAILABLE;  
    char *method_types                                       OBJC2_UNAVAILABLE;  
    IMP method_imp                                           OBJC2_UNAVAILABLE;  
}  

其实Method就是一个指向objc_method结构体指针,它存储了方法名(method_name)、方法类型(method_types)和方法实现(method_imp)等信息。而method_imp的数据类型是IMP,它是一个函数指针,后面会重点提及。

  • Ivar
    Ivar表示类中的实例变量,在runtime.h文件中找到它的定义:
/// An opaque type that represents an instance variable.  
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  
}  

Ivar其实就是一个指向objc_ivar结构体指针,它包含了变量名(ivar_name)、变量类型(ivar_type)等信息。

  • IMP
    在上面讲Method时就说过,IMP本质上就是一个函数指针,指向方法的实现,在objc.h找到它的定义:
/// A pointer to the function of a method implementation.   
#if !OBJC_OLD_DISPATCH_PROTOTYPES  
typedef void (*IMP)(void /* id, SEL, ... */ );   
#else  
typedef id (*IMP)(id, SEL, ...);   
#endif

当你向某个对象发送一条信息,可以由这个函数指针来指定方法的实现,它最终就会执行那段代码,这样可以绕开消息传递阶段而去执行另一个方法实现。

  • cache
    顾名思义,Cache主要用来缓存,那它缓存什么呢?我们先在runtime.h文件看看它的定义
typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE;  
struct objc_cache {  
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;  
    unsigned int occupied                                    OBJC2_UNAVAILABLE;  
    Method buckets[1]                                        OBJC2_UNAVAILABLE;  
};  

Cache其实就是一个存储Method的链表,主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。

消息发送
  • objc_msgSend函数
    在前面已经提过,当某个对象使用语法[receiver message]来调用某个方法时,其实[receiver message]被编译器转化为:
id objc_msgSend ( id self, SEL op, ... );  

现在让我们看一下objc_msgSend它具体是如何发送消息:
1.首先根据receiver对象的isa指针获取它对应的class;
2.优先在class的cache查找message方法,如果找不到,再到methodLists查找;
3.如果没有在class找到,再到super_class查找;
4.一旦找到message这个方法,就执行它实现的IMP。

  • self与super
    为了让大家更好地理解self和super,借用sunnyxx博客的iOS程序员6级考试一道题目:下面的代码分别输出什么?
@implementation Son : Father  
-(id)init  
{  
    self = [super init];  
    if (self)  
    {  
        NSLog(@"%@", NSStringFromClass([self class]));  
        NSLog(@"%@", NSStringFromClass([super class]));  
    }  
    return self;  
}  
@end  

self表示当前这个类的对象,而super是一个编译器标示符,和self指向同一个消息接受者(son)。在本例中,无论是[self class]还是[super class],接受消息者都是Son对象,但super与self不同的是,self调用class方法时,是在子类Son中查找方法,而super调用class方法时,是在父类Father中查找方法。
当调用[self class]方法时,会转化为objc_msgSend函数,这个函数定义如下:

id objc_msgSend(id self, SEL op, ...)  

这时会从当前Son类的方法列表中查找,如果没有,就到Father类查找,还是没有,最后在NSObject类查找到。我们可以从NSObject.mm文件中看到- (Class)class的实现:

-(Class)class {  
    return object_getClass(self);  
}  

所以NSLog(@"%@", NSStringFromClass([self class]));会输出Son。
当调用[super class]方法时,会转化为objc_msgSendSuper,这个函数定义如下:

id objc_msgSendSuper(struct objc_super *super, SEL op, ...) 

objc_msgSendSuper函数第一个参数super的数据类型是一个指向objc_super的结构体,从message.h文件中查看它的定义:

/// Specifies the superclass of an instance.   
struct objc_super {  
    /// Specifies an instance of a class.  
    __unsafe_unretained id receiver;  
  
    /// Specifies the particular superclass of the instance to message.   
#if !defined(__cplusplus)  &&  !__OBJC2__  
    /* For compatibility with old objc-runtime.h header */  
    __unsafe_unretained Class class;  
#else  
    __unsafe_unretained Class super_class;  
#endif  
    /* super_class is the first class to search */  
};  
#endif 

结构体包含两个成员,第一个是receiver,表示某个类的实例。第二个是super_class表示当前类的父类。
这时首先会构造出objc_super结构体,这个结构体第一个成员是self,第二个成员是(id)class_getSuperclass(objc_getClass("Son")),实际上该函数会输出Father。然后在Father类查找class方法,查找不到,最后在NSObject查到。此时,内部使用objc_msgSend(objc_super->receiver, @selector(class))去调用,与[self class]调用相同,所以结果还是Son。
Method Resolution
Fast Forwarding
Normal Forwarding
我的理解:objc_super结构体中receiver是son,superclass是father,而objc_msgSend(objc_super->receiver, @selector(class))内部是使用objc_super中的receiver作为消息发送者,也就是son调用NSObject中的class方法,所以结果是son。

方法解析与消息转发

[receiver message]调用方法时,如果在message方法在receiver对象的类继承体系中没有找到方法,那怎么办?一般情况下,程序在运行时就会Crash掉,抛出unrecognized selector sent to…类似这样的异常信息。但在抛出异常之前,还有三次机会按以下顺序让你拯救程序。
1.Method Resolution 即所属类动态方法解析
2.Fast Forwarding 即备援接受者
3.Normal Forwarding

  • Method Resolution首先Objective-C在运行时调用+ resolveInstanceMethod:或+ resolveClassMethod:方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。
    举一个简单例子,定义一个类Message,它主要定义一个方法sendMessage,下面就是它的设计与实现:
@interface Message : NSObject  
-(void)sendMessage:(NSString *)word;  
@end  
@implementation Message  
-(void)sendMessage:(NSString *)word  
{  
    NSLog(@"normal way : send message = %@", word);  
}  
@end  

如果我在viewDidLoad方法中创建Message对象并调用sendMessage方法:

-(void)viewDidLoad {  
    [super viewDidLoad];  
    Message *message = [Message new];  
    [message sendMessage:@"Sam Lau"];  
}  

控制台会打印以下信息:

normal way : send message = Sam Lau

但现在我将原来sendMessage方法实现给注释掉,覆盖resolveInstanceMethod方法:

#pragma mark - Method Resolution  
/// override resolveInstanceMethod or resolveClassMethod for changing sendMessage method implementation  
+(BOOL)resolveInstanceMethod:(SEL)sel  
{  
    if (sel == @selector(sendMessage:)) {  
        class_addMethod([self class], sel, imp_implementationWithBlock(^(id self, NSString *word) {  
            NSLog(@"method resolution way : send message = %@", word);  
        }), "v@*");  
    }  
    return YES;  
} 

控制台就会打印以下信息:

method resolution way : send message = Sam Lau

该方法主要原型为
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
方法名为给class添加方法,返回一个Bool类型返回值。第一个参数为需要添加方法的类,第二个参数为实例方法的名字也就是+(BOOL)resolveInstanceMethod:(SEL)sel方法中的参数sel,第三个参数为IMP类型的变量也就是函数的实现。需要传一个C函数,该函数至少要有两个变量,一个是id self,一个是SEL _cmd,也可以传字符串。
另一种写法。

void dynamicAdditionMethodIMP(id self, SEL _cmd) {
    //实现
    NSLog(@"dynamicAdditionMethodIMP");
}
class_addMethod([self class], name, (IMP)dynamicAdditionMethodIMP, "v@:");

第四个参数传入一个字符串"v@*,它表示方法的参数和返回值,详情请参考Type Encodings。
如果resolveInstanceMethod方法返回NO,运行时就跳转到下一步:消息转发(Message Forwarding)。

  • Fast Forwarding
    当对象所属类不能动态添加方法后,runtime就会询问当前的接受者是否有其他对象可以处理这个未知的selector。方法就是:
- (id)forwardingTargetForSelector:(SEL)aSelector;

如果目标对象实现- forwardingTargetForSelector:方法,系统就会在运行时调用这个方法,只要这个方法返回的不是nil或self,也会重启消息发送的过程,把这消息转发给其他对象来处理。否则,就会继续Normal Fowarding。
继续上面Message类的例子,将sendMessage和resolveInstanceMethod方法注释掉,然后添加forwardingTargetForSelector方法的实现:

#pragma mark - Fast Forwarding  
-(id)forwardingTargetForSelector:(SEL)aSelector  
{  
    if (aSelector == @selector(sendMessage:)) {  
        return [MessageForwarding new];  
    }  
    return nil;  
}  

此时还缺一个转发消息的类MessageForwarding,这个类的设计与实现如下:

@interface MessageForwarding : NSObject  
-(void)sendMessage:(NSString *)word;  
@end  
@implementation MessageForwarding  
-(void)sendMessage:(NSString *)word  
{  
    NSLog(@"fast forwarding way : send message = %@", word);  
}  
@end  

此时,控制台会打印以下信息:

fast forwarding way : send message = Sam Lau

这里叫Fast,是因为这一步不会创建NSInvocation对象,但Normal Forwarding会创建它,所以相对于更快点。

  • Normal Forwarding
    如果没有使用Fast Forwarding来消息转发,最后只有使用Normal Forwarding来进行消息转发。它首先调用methodSignatureForSelector:方法来获取函数的参数和返回值,如果返回为nil,程序会Crash掉,并抛出unrecognized selector sent to instance异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用-forwardInvocation:方法。
    继续前面的例子,将forwardingTargetForSelector方法注释掉,添加methodSignatureForSelector和forwardInvocation方法的实现:
#pragma mark - Normal Forwarding  
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector  
{  
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];  
    if (!methodSignature) {  
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];  
    }  
    return methodSignature;  
}  
-(void)forwardInvocation:(NSInvocation *)anInvocation  
{  
    MessageForwarding *messageForwarding = [MessageForwarding new];  
    if ([messageForwarding respondsToSelector:anInvocation.selector]) {  
        [anInvocation invokeWithTarget:messageForwarding];  
    }  
}  
三种方法的选择

Runtime提供三种方式来将原来的方法实现代替掉,那该怎样选择它们呢?

  • Method Resolution
    由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在原来的类中代替掉。
  • Fast Forwarding
    它可以将消息处理转发给其他对象,使用范围更广,不只是限于原来的对象。
  • Normal Forwarding
    它跟Fast Forwarding一样可以消息转发,但它能通过NSInvocation对象获取更多消息发送的信息,例如:target、selector、arguments和返回值等信息。
Associated Objects

如果我们想对系统的类添加方法时,可以使用扩展,但是添加属性时,只能使用继承。如果不想使用继承,则可以用runtime来关联对象,这与我们自定义类的添加属性不同。本质上是使用类别进行扩展,通过添加get方法和set方法从而在使用时可以使用点方法使用。与普通使用方法一致。
同时当使用Category对某个类进行扩展时,有时需要存储属性,Category是不支持的,这时需要使用Associated Objects来给已存在的类Category添加自定义的属性。Associated Objects提供三个C语言API来向对象添加、获取和删除关联值:

  • 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 )

其中objc_AssociationPolicy是个枚举类型,它可以指定Objc内存管理的引用计数机制。

  typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {  
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */  
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.  
                                            *   The association is not made atomically. */  
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied.  
                                            *   The association is not made atomically. */  
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object. 
                                            *   The association is made atomically. */  
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied. 
                                            *   The association is made atomically. */  
};  

下面有个关于NSObject+AssociatedObject Category添加属性associatedObject的示例代码:
NSObject+AssociatedObject.h

@interface NSObject (AssociatedObject)  
@property (strong, nonatomic) id associatedObject;  
@end 

NSObject+AssociatedObject.m

@implementation NSObject (AssociatedObject)  
- (void)setAssociatedObject:(id)associatedObject  
{  
    objc_setAssociatedObject(self, @selector(associatedObject), associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);  
}  
- (id)associatedObject  
{  
    return objc_getAssociatedObject(self, _cmd);  
}  
@end 

Associated Objects的key要求是唯一并且是常量,而SEL是满足这个要求的,所以上面的采用隐藏参数_cmd作为key。
一个给scrollview添加refreshView的实际用例:

@interface UIScrollView (Refresh)  
@property (nonatomic) RefreshView * refreshView;  
@end  
#import <objc/runtime.h>  
static char kRefreshView;  
@implementation UIScrollView (Refresh)  
@dynamic refreshView;  
  
- (void)setRefreshView:(RefreshView *)aView {  
      
    objc_setAssociatedObject(self, &kRefreshView, aView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);  
}  
  
  
- (RefreshView *)refreshView {  
      
    return objc_getAssociatedObject(self, &kRefreshView);  
}  
  
@end  
GetClass

得到一个实例的类。代码如下:

#import "ViewController.h"  
#import <objc/runtime.h>  
#import "Person.h"  
  
@interface ViewController ()  
  
@end  
  
@implementation ViewController  
  
- (void)viewDidLoad {  
    [super viewDidLoad];  
    // Do any additional setup after loading the view, typically from a nib.  
      
      
    Person * p1 = [[Person alloc] init];  
      
    Class c1 = object_getClass(p1);  
    NSLog(@"%@", c1);  
    Person * p2 = [[[c1 class] alloc] init];  
    NSLog(@"%@", p2.name);  
      
} 
isKindOfClass和isMemberOfClass

先看看isKindOfClass和isMemberOfClass在Object.mm中的实现:

- (BOOL)isKindOf:aClass
{
     Class cls;
     for (cls = isa; cls; cls = cls->superclass)
          if (cls == (Class)aClass)
               return YES;
     return NO;
}

- (BOOL)isMemberOf:aClass
{
     return isa == (Class)aClass;
}

可以看到isKindOfClass是取自身指针isa与参数指针做对比,如果相等返回YES,如果不等则取其superClass指针,直到superClass为空为止,循环结束返回空。

int main(int argc, const char * argv[]) {
     @autoreleasepool {
          BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
          BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
          BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
          BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
          NSLog(@"%d %d %d %d", res1, res2, res3, res4);
     }
     return 0;
}

//输出
2014-11-05 14:45:08.474 Test[9412:721945] 1 0 0 0

res1中NSObject的isa指针指向NSOject的meta-class,继续循环,meta-class的superClass是NSObject所以为ture;
res3中Sark的isa指针指向sark的meta-class,接下来指向NSObject的meta-class,再接下来指向NSObject;
res2中NSObject的isa指针指向NSOject的meta-class,与NSObject不一致;
res4中Sark的isa指针指向sark的meta-class,与Sark不一致。
注意上面是类与类相比。下面是类的实例与类相比:

Person *person = [[Person alloc] init];  
Teacher *teacher = [[Teacher alloc] init];  
  
//YES   
if ([teacher isKindOfClass:[Teacher class]]) {  
    NSLog(@"teacher 是 Teacher类或Teacher的子类");  
}  
//YES   
if ([teacher isKindOfClass:[Person class]]) {  
    NSLog(@"teacher 是 Person类或Person的子类");  
}  
//YES   
if ([teacher isKindOfClass:[NSObject class]]) {  
    NSLog(@"teacher 是 NSObject类或NSObject的子类");  
} 

// YES   
if ( [teacher respondsToSelector: @selector( setName: )] == YES ) {  
    NSLog(@"teacher responds to setSize: method" );  
}  
  
// NO   
if ( [teacher respondsToSelector: @selector( abcde )] == YES ) {  
    NSLog(@"teacher responds to nonExistant method" );  
}  
  
// YES   
if ( [Teacher respondsToSelector: @selector( alloc )] == YES ) {  
    NSLog(@"teacher class responds to alloc method\n" );  
}  
Method Swizzling

Method Swizzling就是在运行时将一个方法的实现代替为另一个方法的实现。由于Foundation等框架都是闭源的,我们没有办法直接修改代码,通常情况下只能通过继承,类别,关联属性等手段添加属性和实例方法,较为繁琐。而Method Swizzling可以通过将一个方法的实现代替另一个方法的实现,从而达到修改闭源代码的目的。
例如一个想要统计每个页面出现的次数,如果在所有页面的viewWillAppear中都添加统计代码,则太过繁琐。那我们使用一个类别,并自定义一个方法替代系统的viewWillAppear,在其中做统计,并在统计结束后调用系统的viewWillAppear即可。

@interface UIViewController (MyUIViewController)

@end

@implementation UIViewController(MyUIViewController)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        SEL originalSelector = @selector(viewWillAppear:);
        Method originalMethod = class_getInstanceMethod([self class], originalSelector);
        
        SEL exchangeSelector = @selector(myViewWillAppear:);
        Method exchangeMethod = class_getInstanceMethod([self class], exchangeSelector);
        
        method_exchangeImplementations(originalMethod, exchangeMethod);
    });
}

- (void)myViewWillAppear:(BOOL)animated {
    //这里实际上是调用系统的viewWillAppear,不会递归调用。
    [self myViewWillAppear:animated];
    NSLog(@"MyViewWillAppear %@", [self class]);
}

使用load实现预加载。使用GCD的dispatch_once_t保证只会交换一次不会重复交换。使用method_exchangeImplementations来实现交换。
将上述代码放入工程中就可以打印所有viewcontroller的加载情况。

Aspect-Oriented Programming(AOP)

类似记录日志、身份验证、缓存等事务非常琐碎,与业务逻辑无关,很多地方都有,又很难抽象出一个模块,这种程序设计问题,业界给它们起了一个名字叫横向关注点(Cross-cutting concern),AOP作用就是分离横向关注点(Cross-cutting concern)来提高模块复用性,它可以在既有的代码添加一些额外的行为(记录日志、身份验证、缓存)而无需修改代码。

runtime如何实现weak置为nil

runtime对注册的类会进行布局,对于weak修饰的对象会放入一个hash表中,用weak指向的对象内存地址作为key。当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil。

NSString *name = [[NSString alloc] initWithString: @"Jiaming Chen"];
__weak NSString *weakStr = name;

当为weakStr这一weak类型的对象赋值时,编译器会根据name的地址为key去查找weak哈希表,该表项的值为一个数组,将weakStr对象的地址加入到数组中,当name变量超出变量作用域或引用计数为0时,会执行dealloc函数,在执行该函数时,编译器会以name变量的地址去查找weak哈希表的值,并将数组里所有 weak对象全部赋值为nil。

总结

虽然在平时项目不是经常用到Objective-C的Runtime特性,但当你阅读一些iOS开源项目时,你就会发现很多时候都会用到。所以深入理解Objective-C的Runtime数据结构、消息转发机制有助于你更容易地阅读和学习开源项目。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,690评论 0 9
  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 729评论 0 2
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,182评论 0 7
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 751评论 0 1
  • runtime 和 runloop 作为一个程序员进阶是必须的,也是非常重要的, 在面试过程中是经常会被问到的, ...
    made_China阅读 1,204评论 0 7