一、引言
runtime是iOS开发者进阶的必备装逼特技之一,一出手就是华丽丽的特效,也曾经看过很多资料,但一直没有足够系统并且容易理解的。所以今天为广大对runtime理解不够透彻的小伙伴整理一下。
二、runtime是什么?
简单点讲,runtime就是一个系统库。强调一遍,它是一个系统库文件。我们要使用#import <objc/runtime.h>
进行导入。
它有什么存在的意义呢?
我们知道Objective-C是一门动态语言。那什么是动态语言呢? 就是让程序在运行时做更多的事情。比如OC的程序可以在运行时判断对象类型、判断对象能否响应某个方法以及同名方法应该按照哪个类的实现来执行。所以把这些工作推迟到运行时进行,光靠编译器是不够的,这就需要一个动态的系统来执行编译后的代码,这个系统就是runtime。所以,可以说runtime就是Objective-C的基石。
虽然,我们在实际开发中并不是很常用runtime机制,但理解它对我们理解OC这门语言会有很大帮助,而且也可以用来装逼。
三、runtime在哪里出没?(官方给的概念,了解)
Objective-C Source Code (OC源代码)
实际上我们写的所有OC代码,大部分都离不开runtime的幕后支持,因为所有的OC代码都会在运行时转换成runtime的代码实现。比如我们最常见的[reciver message];
消息发送机制,其实在运行时它就会被编译成objc_msgSend(reciver,message);
这种C风格的函数调用语句。而objc_msgSend()
就是声明在runtime.h
这个库文件里的函数。NSObject Methods (NSObject方法)
有的NSObject中的方法起到了抽象接口的作用,比如description
方法需要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时获得类的信息,并检查一些特性。
比如:
class
返回对象的类;isKindOfClass:
和isMemberOfClass:
则检查对象是否在指定的类继承体系中;respondsToSelector:
检查对象能否响应指定的消息conformsToProtocol:
检查对象是否实现了指定协议类的方法;methodForSelector:
则返回指定方法实现的地址。这些方法会用到runtime里的Class类型、SEL类型等等。Runtime Functions (Runtime的函数)
Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。在Objective-C Runtime Reference中有对 Runtime 函数的详细文档。
四、Objective-C对象的本质(理解)
我们打开NSObject的头文件可以看到对NSObject类的定义如下
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
那这个Class是个什么类型呢?
typedef struct objc_class *Class;
可以看出Class实际上是一个结构体指针,而这个结构体struct 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;
我们发现一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。
在objc_class
结构体中,有如下成员:
-
ivars
是objc_ivar_list
指针;它里面存放了当前类的所有成员变量。 -
methodLists
是方法列表,它指向objc_method_list
。它里面记录了当前类的所有方法。它是一个指针的指针,也就是说可以动态修改*methodLists
的值来添加成员方法,这也是Category
实现的原理,同样解释了Category
不能添加属性的原因。 -
objc_cache
,顾名思义它是缓存,它里面记录最近使用的方法,这样下次再调用时可以直接在cache
里查找,提高调用效率。
而这个结构体中,也存在一个Class
类型的成员变量isa
,这是因为OC的类本身也是一个对象,为了处理类和对象的关系,runtime库创建了一种叫做元类(Meta Class)的东西。如果把类看成一个实例,那么这个元类就是描述这个类实例的,而类是用来描述实例的。这句话优点拗口,但你一定要仔细回味。而类型方法就定义在元类里。因为这些方法可以理解为类实例可以直接响应的方法。当我们想一个类发送一个类方法,比如[NSObject alloc]
,实际上是响应alloc
这个方法的target就是这个类实例(由元类描述)。NSObject
的元类同时也是一个根元类(root Meta Class),所有的元类最终都指向根元类。如下图:
上图实线是
super_class
指针,虚线是isa
指针。 根元类的超类是NSObject
,而isa
指向了自己,而NSObject
的超类为nil
,也就是它没有超类.
五、runtime术语
id类型
objc_msgSend(id self,SEL method,arg1,arg2...)
上面的函数就是我们说的消息发送机制的实现函数。第一个参数记录当前消息接受者地址,第二个参数记录方法名,后面是方法的参数,可以有多个。
这里的id类型,实际上是指向类实例的指针
typedef struct objc_object *id;
展开objc_object
又是啥呢?
struct objc_object { Class isa; };
我们发现objc_object
实际上也是一个结构体,它里面有一个成员,是一个Class类型的isa指针,通过上面所讲,我们知道这个结构体描述了类的基本信息。根据这个isa指针,我们就可以找到该对象所属的类。
注意:isa指针并不一定总是指向该对象所属的类,所以不能通过isa来判断该对象的类型,而应该使用class方法。比如KVO的实现原理,就是让isa指向一个中间类,而不是真实的类。这种技术叫做isa-swizzling。详情见官方文档
SEL
SEL是一个类型,它是selector在OC中的表示类型(Swift是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:
typedef struct objc_selector *SEL;
其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。
不同类中的相同名字的方法对应的方法选择器是相同的,即使方法名字相同而参数类型不同,他们的方法选择器也是相同的。
Method
Method是一种代表类中的某个方法的类型。
typedef struct objc_method *Method;
而objc_method
在上面的方法列表中提到过,它存储了方法名,方法类型和方法实现:
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
- 方法名类型为
SEL
,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。 - 方法类型
method_types
是个char指针,其实存储着方法的参数类型和返回值类型。 -
method_imp
指向了方法的实现,本质上是一个函数指针。
Ivar
Ivar是一种代表类中实例的类型。它的定义如下
typedef struct objc_ivar *Ivar;
而objc_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
} OBJC2_UNAVAILABLE;
不难看出,这个结构体里记录了该成员变量的名字ivar_name、类型ivar_type、距离首地址的偏移量ivar_offset。我们可以根据实例查找其在类中的名字,也就是反射。
-(NSString *)nameWithInstance:(id)instance {
unsigned int numIvars = 0;
NSString *key=nil;
Ivar * ivars = class_copyIvarList([self class], &numIvars);
for(int i = 0; i < numIvars; i++) {
Ivar thisIvar = ivars[i];
const char *type = ivar_getTypeEncoding(thisIvar);
NSString *stringType = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
if (![stringType hasPrefix:@"@"]) {
continue;
}
if ((object_getIvar(self, thisIvar) == instance)) {
//此处若 crash 不要慌!
key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
break;
}
}
free(ivars); return key;
}
class_copyIvarList
函数获取的不仅有实例变量,还有属性。但会在原本的属性名前加上一个下划线。
IMP
IMP的定义如下,它定义在objc.h文件内
typedef id (*IMP)(id, SEL, ...);
可以看出它其实是一个类型为id (*)(id, SEL,...)
类型的函数指针。上面我们提到过一个Method
,它其实是Method结构体内的一个成员变量,它记录了当前方法的实现过程。也就是说,当一个对象接收到一个消息之后,最终执行的那段代码就是由这个函数指针指定的。如果我们能找到函数的首地址,就可以绕开消息的传递阶段,直接执行方法。
我们发现IMP指针指向的函数类型跟objc_msgSend()的类型是相同的。这也间接说明了,IMP记录的是消息的最终实现函数的地址。这个函数类型里包含两个参数id和SEL,id记录了当前响应方法的对象,每个方法都对应一个SEL类型的方法选择器,而每个实例对象中的SEL对应的方法有事唯一的(因为在一个类中,不能出现重名方法),所以通过一组id和SEL,就能确定唯一的方法实现的地址。
Cache
在runtime中,Cache的定义如下
typedef struct objc_cache *Cache
在Class类型中,有一个成员变量struct objc_cache *cache
,它是一个缓存,那么它缓存的是什么呢?
我们先看看它的结构
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
Cache
为方法调用的性能进行优化,通俗地讲,每当实例对象接收到一个消息时,它不会直接在isa
指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了,而是优先在Cache
中查找。Runtime
系统会把被调用的方法存到Cache中,下次查找的时候效率更高。
Property
@property
标记了类中的属性,它是一个指向objc_property
结构体的指针:
typedef struct objc_property *Property;typedef struct objc_property *objc_property_t;//这个更常用
可以通过class_copyPropertyList
和protocol_copyPropertyList
方法来获取类和协议中的属性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
返回类型为指向指针的指针,因为属性列表是个数组,每个元素内容都是一个objc_property_t
指针,而这两个函数返回的值是指向这个数组的指针。
举例说明,先声明一个类:
@interface Lender : NSObject {
float alone;
}
@property float alone;
@end
你可以用下面的代码获取属性列表:
id LenderClass = objc_getClass("Lender");unsigned int outCount;objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
你可以用property_getName
函数来查找属性名称:
const char *property_getName(objc_property_t property)
你可以用class_getProperty
和protocol_getProperty
通过给出的名称来在类和协议中获取属性的引用:
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
你可以用property_getAttributes
函数来发掘属性的名称和@encode
类型字符串:
const char *property_getAttributes(objc_property_t property)
把上面的代码放一起,你就能从一个类中获取它的属性了:
id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property),property_getAttributes(property));
}
对比下class_copyIvarList
函数,使用class_copyPropertyList
函数只能获取类的属性,而不包含成员变量。但此时获取的属性名是不带下划线的。
六、实际应用
1.动态创建一个类
#import <objc/runtime.h>
// 自定义一个方法
void reportFunction (id self, SEL _cmd) {
NSLog(@"This object is %p", self);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 1.动态创建对象 创建一个Person 继承自 NSObject类
Class newClass = objc_allocateClassPair([NSObject class], “Person”, 0);
// 为该类增加名为Report的方法
class_addMethod(newClass, @selector(report), (IMP)reportFunction, @"v@:");
// 注册该类
objc_registerClassPair(newClass);
// 创建一个 Student 类的实例
instantOfNewClass = [[newClass alloc] init];
// 调用方法
[instantOfNewClass report];
}
return 0;
}
这里用到的Selector事实上是一个C的结构体,表示一个消息,类似于C的方法调用:
typedef struct objc_selector? *SEL;
IMP (Method Implementations)
,就是一个函数指针,当你发起一个ObjC消息之后,最终它会执行的那个代码,就是由这个函数指针指定的。
typedef id (*IMP)(id self,SEL _cmd,...);
2.关联对象
对象在内存中的排布可以看成是一个结构体,该结构体的大小并不能动态的变化,所以无法在运行时动态的给对象增加成员变量,但是我们可以通过关联对象的方法变相的给对象增加一个成员变量。
比如,我们想给NSObject新增一个关联对象:
创建一个NSObject的类目AssociatedObject
,在.h文件里面声明一个属性
@interface NSObject (AssociatedObject)
@property (nonatomic, strong) id associatedObject;?
@end
在NSObject+AssociatedObject.m文件里面进行关联
#import "NSObject+AssociatedObject.h"
#import <objc/runtime.h>
@implementation NSObject (AssociatedObject)
@dynamic associatedObject;
- (void)setAssociatedObject:(id)object {
// 设置关联对象
objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)associatedObject {
// 得到关联对象
return objc_getAssociatedObject(self, @selector(associatedObject));
}
@end
这样就给NSObject新增了一个属性,可是,这有什么用呢?通常,我们会利用关联对象给UIAlertView
新增一个block
回调,方便使用。
3.利用RunTime进行模型归档
对于一个有很多属性的Person类,遵守了NSCoding协议之后,我们可以利用RunTime遍历模型对象的所有属性进行归档,关键代码如下:
// 利用runtime机制进行属性的归档接档
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([Person class], &count);
for (int i = 0; i<count; i++) {
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
// 查看成员变量
const char *name = ivar_getName(ivar);
// 归档
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
free(ivars);
}
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([Person class], &count);
for (int i = 0; i<count; i++) {
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
// 查看成员变量
const char *name = ivar_getName(ivar);
// 归档
NSString *key = [NSString stringWithUTF8String:name];
id value = [aDecoder decodeObjectForKey:key];
// 设置到成员变量身上
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}