Runtime数据结构:在Objective-C中,使用[receiver message]
语法并不会马上执行receiver
对象的message
方法的代码,而是向receiver
发送一条message
消息,这条消息可能由receiver
来处理,也可能由转发给其他对象来处理,也有可能假装没有接收到这条消息而没有处理。其实[receiver message]
被编译器转化为:
id objc_msgSend ( id self, SEL op, ... );
下面从两个数据结构id
和SEL
来逐步分析和理解Runtime有哪些重要的数据结构。
SEL:
SEL
是函数objc_msgSend
第二个参数的数据类型,表示方法选择器,按照下面路径查看到SEL
数据结构如下:
typedef struct objc_selector *SEL;
其实它就是映射到方法的C字符串,你可以通过Objc编译器命令@selector()
或者Runtime系统的sel_registerName
函数来获取一个SEL
类型的方法选择器。
如果你知道selector对应的方法名是什么,可以通过NSString* NSStringFromSelector(SEL aSelector)
方法将SEL转化为字符串,再用NSLog
打印。
id:
接下来看objc_msgSend
第一个参数的数据类型id
,id
是通用类型指针,能够表示任何对象,按下面路径打开objc.h
文件
查看到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
结构体指针,它包含一个Class isa
成员,根据isa
指针就可以顺藤摸瓜找到对象所属的类。
注意:根据Apple的官方文档Key-Value Observing Implementation Details提及,key-value observing是使用isa-swizzling的技术实现的,isa指针在运行时被修改,指向一个中间类而不是真正的类。所以,你不应该使用isa指针来确定类的关系,而是使用class方法来确定实例对象的类。
Class:
isa
指针的数据类型是Class
,Class
表示对象所属的类,按下面路径打开objc.h
文件
/// An opaque type that represents an Objective-C class. typedef struct objc_class *Class;
可以查看到Class
其实就是一个objc_class
结构体指针,但这个头文件找不到它的定义,需要在runtime.h
才能找到objc_class
结构体的定义。
按下面路径打开runtime.h
文件
查看到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 *` */
注意:OBJC2_UNAVAILABLE是一个Apple对Objc系统运行版本进行约束的宏定义,主要为了兼容非Objective-C 2.0的遗留版本,但我们仍能从中获取一些有用信息。
让我们分析一些重要的成员变量表示什么意思和对应使用哪些数据结构:
-
isa
表示一个Class对象的Class,也就是Meta Class。在面向对象设计中,一切都是对象,Class在设计中本身也是一个对象。我们会在objc-runtime-new.h
文件找到证据,发现objc_class
有以下定义:
struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags ...... }
由此可见,结构体objc_class
也是继承objc_object
,说明Class在设计中本身也是一个对象。
其实Meta Class
也是一个Class,那么它也跟其他Class一样有自己的isa
和super_class
指针,关系如下:
上图实线是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)
-
super_class
表示实例对象对应的父类name
表示类名-
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
结构体存储类的单个成员变量信息。
-
methodLists
表示方法列表,它指向objc_method_list
结构体的二级指针,可以动态修改*methodLists
的值来添加成员方法,也是Category实现原理,同样也解释Category不能添加实例变量的原因。在runtime.h
可以看到它的定义:
-
-
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
作为入口,逐步深入分析Runtime的数据结构,了解每个数据结构的作用和它们之间关系后,我们正式转入消息发送这个正题。
objc_msgSend函数
在前面已经提过,当某个对象使用语法[receiver message]
来调用某个方法时,其实[receiver message]
被编译器转化为:
id objc_msgSend ( id self, SEL op, ... );
现在让我们看一下objc_msgSend
它具体是如何发送消息:
-
- 首先根据
receiver
对象的isa
指针获取它对应的class
- 优先在
class
的cache
查找message
方法,如果找不到,再到methodLists
查找 - 如果没有在
class
找到,再到super_class
查找 - 一旦找到
message
这个方法,就执行它实现的IMP
。
- 首先根据
下面通过实例学习runtime
当我们对一个实例发送消息时(-开头的方法),会在该 instance 对应的类的 methodLists 里查找。
当我们对一个类发送消息时(+开头的方法),会在该类的 MetaClass 的 methodLists 里查找。
创建person对象
Person *p = [[Person alloc] init];
调用对象方法
[p run]; // 本质:让对象发送消息:objc_msgSend(p, @selector(run));
调用类方法的方式:两种
第一种通过类名调用 :[Person run];
第二种通过类对象调用 :[[Person class] run];
用类名调用类方法,底层会自动把类名转换成类对象调用
本质:让类对象发送消息 objc_msgSend([Person class], @selector(eat));
1、交换两个方法的实现
/*
获得类方法:
Method class_getClassMethod(Class cls , SEL name)
获得某个类的实例对象方法
Method class_getInstanceMethod(Class cls , SEL name)
交换两个方法的实现
void method_exchangeImplementations(Method m1 , Method m2)
*/
//类方法
Method class1 = class_getClassMethod([Person class], @selector(run)); Method class2 = class_getClassMethod([Person class], @selector(study)); method_exchangeImplementations(class1, class2); [Person run]; //调用study方法实现 [Person study];//调用run方法实现
//对象方法 Method instance1 = class_getInstanceMethod([Person class], @selector(run)); Method instance2 = class_getInstanceMethod([Person class], @selector(stydy)); method_exchangeImplementations(instance1, instance2); Person *person = [[Person alloc]init]; [person run];//调用study实现 [person study];//调用run实现
2、用自己的方法替换系统方法
//创建UIImage的分类
//.h文件
#import <UIKit/UIKit.h> @interface UIImage (Category)
+ (UIImage *)CZC_imageNamed:(NSString *)name; @end
//.m文件 #import "UIImage+Category.h" #import <objc/runtime.h> @implementation UIImage (Category) + (void)load { // 获取两个类的类方法 Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:)); Method m2 = class_getClassMethod([UIImage class], @selector(CZC_imageNamed:)); // 开始交换方法实现 method_exchangeImplementations(m1, m2); } + (UIImage *)CZC_imageNamed:(NSString *)name{ double version = [[UIDevice currentDevice].systemVersion doubleValue]; if (version >= 7.0) {
// 如果系统版本是7.0以上,使用另外一套文件名结尾是‘_os7’的扁平化图片 name = [name stringByAppendingString:@"_7"]; } return [UIImage CZC_imageNamed:name]; } UIImageView *imageVIew = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
//最终调用的图片是ios_7 imageVIew.image = [UIImage imageNamed:@"ios"]; [self.view addSubview:imageVIew];
3、在分类中设置属性,给任何一个对象设置属性
/* set方法,将值value 跟对象object 关联起来(将值value 存储到对象object 中)
参数 object:给哪个对象设置属性
参数 key:一个属性对应一个Key,将来可以通过key取出这个存储的值,key 可以是任何类型:double、int 等,建议用char 可以节省字节
参数 value:给属性设置的值
参数policy:存储策略 (assign 、copy 、 retain就是strong)
设置值:
void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy)
获得值:
id objc_getAssociatedObject(id object , const void *key)
*/
//Person分类
//.h文件
#import "Person.h" @interface Person (Category)<NSCoding> @property (nonatomic, copy)NSString *name; @end
//.m文件 #import "Person+Category.h" #import <objc/runtime.h> char nameKey; @implementation Person (Category) - (void)setName:(NSString *)name{ objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY); } - (NSString *)name{ return objc_getAssociatedObject(self, &nameKey); } @end
4、 获得对象的所有成员变量
//创建Person类
#import <Foundation/Foundation.h> #import <UIKit/UIKit.h> @interface Person : NSObject<NSCoding> { NSString *_userName; NSUInteger _userAge; int _uerHeight; CGFloat _userMoney; CGFloat _userWeight; } @property (nonatomic, copy)NSString *name; @property (nonatomic, copy)NSString *age; //创建Person的分类
//.h文件 #import "Person.h" @interface Person (Category)<NSCoding> - (void)getAllIvar; @end //.m文件 #import "Person+Category.h" #import <objc/runtime.h> @implementation Person (Category) - (void)getAllIvar{ unsigned int outCount = 0; Ivar *ivars = class_copyIvarList([Person class], &outCount); // 遍历所有成员变量 for (int i = 0; i < outCount; i++) { // 取出i位置对应的成员变量 Ivar ivar = ivars[i]; const char *name = ivar_getName(ivar); const char *type = ivar_getTypeEncoding(ivar); NSLog(@"成员变量名:%s 成员变量类型:%s",name,type); } // 注意释放内存! free(ivars); }
//在controller中调用 [person getAllIvar];
5、利用runtime 获取所有属性来进行字典转模型
//创建Teacher类,其中包括Dog对象
//.h文件
#import <Foundation/Foundation.h> #import <UIKit/UIKit.h> #import "Dog.h" @interface Teacher : NSObject @property (nonatomic, copy)NSString *name; @property (nonatomic, copy)NSString *secondName; @property (nonatomic, assign)NSUInteger age; @property (nonatomic, assign)NSUInteger height; @property (nonatomic, assign)CGFloat money; @property (nonatomic, strong)Dog *dog; //当字典的key和模型的属性匹配不上 - (void)setDict:(NSDictionary *)dict; - (void)sendMessage:(NSString *)word; @end //.m文件 Teacher #import "Teacher.h" #import <objc/runtime.h> @implementation Teacher - (void)setDict:(NSDictionary *)dict { Class c = self.class; while (c &&c != [NSObject class]) { unsigned int outCount = 0; Ivar *ivars = class_copyIvarList(c, &outCount); for (int i = 0; i < outCount; i++) { Ivar ivar = ivars[i]; NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)]; // 成员变量名转为属性名(去掉下划线 _ ) key = [key substringFromIndex:1]; // 取出字典的值 id value = dict[key]; // 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil而报错 if (value == nil) continue; // 将字典中的值设置到模型上 [self setValue:value forKeyPath:key]; } free(ivars); c = [c superclass]; } } @end //创建Dog类
//.h文件 #import <Foundation/Foundation.h> @interface Dog : NSObject @property (nonatomic, copy)NSString *name; @property (nonatomic, copy)NSString *color; @property (nonatomic, assign)NSUInteger age; @end //在controller中调用 NSDictionary *dict = @{ @"name":@"夏明", @"age":@33, @"money":@1000 }; //字典赋值模型 Teacher *teacher = [[Teacher alloc]init]; [teacher setDict:dict]; NSLog(@"------teacher---%@", teacher);
//两层模型 NSDictionary *dict2 = @{ @"name" : @"小明", @"age" : @40, @"dog" : @{ @"name":@"黑子", @"color":@"黄" } }; Teacher *teacher2 = [[Teacher alloc]init]; [teacher2 setObjDict:dict2]; NSLog(@"----teacher2-----%@", teacher2);
6、动态添加一个方法//创建Person类
//.h文件
#import <Foundation/Foundation.h> #import <UIKit/UIKit.h> @interface Person : NSObject<NSCoding> @end
//.m文件Person #import "Person.h" #import "NSObject+Category.h" #import <objc/runtime.h> @implementation Person // void(*)() // 默认方法都有两个隐式参数, void eat(id self,SEL sel) { NSLog(@"%@ %@",self,NSStringFromSelector(sel)); } // 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来. // 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(eat)) { // 动态添加eat方法 // 第一个参数:给哪个类添加方法 // 第二个参数:添加方法的方法编号 // 第三个参数:添加方法的函数实现(函数地址) // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd class_addMethod(self, sel, eat, "v@:");//参数:eat可用(IMP)eat替换 } return [super resolveInstanceMethod:sel]; } @end //在controller中调用 //动态添加一个方法,p中没有这个方法
Person *p = [[Person alloc]init]; [p performSelector:@selector(eat)];
//第二种方式 + (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; } //动态添加方法, Person *p1 = [[Person alloc]init]; [p1 sendMessage:@"陈志超"];
7、消息的转发
//.h
#import <Foundation/Foundation.h> #import <UIKit/UIKit.h> #import "Dog.h" @interface Teacher : NSObject @end //.m #import "Teacher.h" #import <objc/runtime.h> @implementation Teacher
/* 实现方式一
//消息转发,让另一个类对象执行方法
- (id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector == @selector(sendMessage:)) { return [[Dog alloc]init]; } return nil; } */
//下面两个方式结合为方式二 #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 { Dog *messageForwarding = [[Dog alloc]init]; if ([messageForwarding respondsToSelector:anInvocation.selector]) { [anInvocation invokeWithTarget:messageForwarding]; } } @end //创建Dog类 #import <Foundation/Foundation.h> @interface Dog : NSObject - (void)sendMessage:(NSString *)word; @end #import "Dog.h" @implementation Dog - (void)sendMessage:(NSString *)word{ NSLog(@"Dog--sendMessage = %@", word); } @end
//此时在controller中,
Teacher *t = [[Teacher alloc] init];
//下面代码会调用Dog的sendMessage方法
[t sendMessage:@"你好,teacher"];
注(6、7结合看):[receiver message]
调用方法时,如果在message方法在receiver对象的类继承体系中没有找到方法,那怎么办?一般情况下,程序在运行时就会Crash掉,抛出 unrecognized selector sent to …类似这样的异常信息。但在抛出异常之前,还有三次机会按以下顺序让你拯救程序。
- Method Resolution
- Fast Forwarding
- Normal Forwarding

1、首先Objective-C在运行时调用+ resolveInstanceMethod:
或+ resolveClassMethod:
方法,让你添加方法的实现。如果你添加方法并返回YES
,那系统在运行时就会重新启动一次消息发送的过程
2、如果目标对象实现- forwardingTargetForSelector:
方法,系统就会在运行时调用这个方法,只要这个方法返回的不是nil
或self
,也会重启消息发送的过程,把这消息转发给其他对象来处理。否则,就会继续Normal Fowarding。
3、如果没有使用Fast Forwarding来消息转发,最后只有使用Normal Forwarding来进行消息转发。它首先调用methodSignatureForSelector:
方法来获取函数的参数和返回值,如果返回为nil,程序会Crash掉,并抛出unrecognized selector sent to instance异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用-forwardInvocation:
方法。
三种方法的选择:
Runtime提供三种方式来将原来的方法实现代替掉,那该怎样选择它们呢?
- Method Resolution:由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在原来的类中代替掉。
- Fast Forwarding:它可以将消息处理转发给其他对象,使用范围更广,不只是限于原来的对象。
- Normal Forwarding:它跟Fast Forwarding一样可以消息转发,但它能通过NSInvocation对象获取更多消息发送的信息,例如:target、selector、arguments和返回值等信息。