iOS进阶-详细介绍runtime
参考:
目录
- runtime 概念
- runtime的成员组成以及结构
- runtime的消息转发机制流程
- runtime 常用API 总结
- runtime使用场景
一、runtime 概念
一套C语言标准库,oc 的运行时环境,它将程序类的类型确定、和函数调用逻辑从编译时移动到了运行,是oc成为一种动态语言。
二、runtime的成员组成以及结构
描述Objective-C对象所有的数据结构定义都在Runtime的头文件里,下面我们逐一分析。
1.id
运行期系统如何知道某个对象的类型呢?对象类型并不是在编译期就知道了,而是要在运行期查找。Objective-C有个特殊的类型id,它可以表示Objective-C的任意对象类型,id类型定义在Runtime的头文件中:
struct objc_object {
Class isa;
} *id;
由此可见,每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类,通常称为isa指针。
objc_object
objc_object是表示一个类的实例的结构体
它的定义如下(objc/objc.h):
struct objc_object{
Class isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;
可以看到,这个结构体只有一个字体,即指向其类的isa指针。这样,当我们向一个Objective-C对象发送消息时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类的方法列表及父类的方法列表中去寻找与消息对应的selector指向的方法,找到后即运行这个方法。
2.Class
Class对象也定义在Runtime的头文件中,查看objc/runtime.h中的objc_class结构体:
Objective-C中,类是由Class类型来表示的,它实际上是一个指
向objc_class结构体的指针。
typedef struct objc_class *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; // 类的版本信息,默认为0
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
}
下面说下Class的结构体中的几个主要变量:
- 1.isa:
结构体的首个变量也是isa指针,这说明Class本身也是Objective-C中的对象。isa指针非常重要, 对象需要通过isa指针找到它的类, 类需要通过isa找到它的元类. 这在调用实例方法和类方法的时候起到重要的作用. - 2.super_class:
结构体里还有个变量是super_class,它定义了本类的超类。类对象所属类型(isa指针所指向的类型)是另外一个类,叫做“元类”。 - 3.ivars:
成员变量列表,类的成员变量都在ivars里面。 - 4.methodLists:
方法列表,类的实例方法都在methodLists里,类方法在元类的methodLists里面。methodLists是一个指针的指针,通过修改该指针指向指针的值,就可以动态的为某一个类添加成员方法。这也就是Category实现的原理,同时也说明了Category只可以为对象添加成员方法,不能添加成员变量。 - 5.cache:
方法缓存列表,objc_msgSend(下文详解)每调用一次方法后,就会把该方法缓存到cache列表中,下次调用的时候,会优先从cache列表中寻找,如果cache没有,才从methodLists中查找方法。提高效率。
元类(Meta Class)
meta-class是一个类对象的类。
在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。既然是对象,那么它也是一个objc_object指针,它包含一个指向其类的一个isa指针。那么,这个isa指针指向什么呢?
为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,meta-class中存储着一个类的所有类方法。
所以,调用类方法的这个类对象的isa指针指向的就是meta-class
当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。
再深入一下,meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。
即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。
通过上面的描述,再加上对objc_class结构体中super_class指针的分析,我们就可以描绘出类及相应meta-class类的一个继承体系了,如下代码
看图说话:
上图中:superclass指针代表继承关系,isa指针代表实例所属的类。
类也是一个对象,它是另外一个类的实例,这个就是“元类”,元类里面保存了类方法的列表,类里面保存了实例方法的列表。实例对象的isa指向类,类对象的isa指向元类,元类对象的isa指针指向一个“根元类”(root metaclass)。所有子类的元类都继承父类的元类,换而言之,类对象和元类对象有着同样的继承关系。
1.Class是一个指向objc_class结构体的指针,而id是一个指向objc_object结构体的指针,其中的isa是一个指向objc_class结构体的指针。其中的id就是我们所说的对象,Class就是我们所说的类。
2.isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用isKindOfClass:方法来确定实例对象的类。因为KVO的实现机制就是将被观察对象的isa指针指向一个中间类而不是真实的类。
Category
Category是表示一个指向分类的结构体的指针,其定义如下:
typedef struct objc_category *Category
struct objc_category{
char *category_name OBJC2_UNAVAILABLE; // 分类名
char *class_name OBJC2_UNAVAILABLE; // 分类所属的类名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 实例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 类方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}
这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods列表是objc_class中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。
可发现,类别中没有ivar成员变量指针,也就意味着:类别中不能够添加实例变量和属性
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
3.SEL
//// http://www.jianshu.com/p/3e050ec3b759
SEL是选择子的类型,选择子指的就是方法的名字。在Runtime的头文件中的定义如下:
typedef struct objc_selector *SEL;
它就是个映射到方法的C字符串,SEL类型代表着方法的签名,在类对象的方法列表中存储着该签名与方法代码的对应关系,每个方法都有一个与之对应的SEL类型的对象,根据一个SEL对象就可以找到方法的地址,进而调用方法。
////http://www.jianshu.com/p/adf0d566c887
SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:
方法的selector用于表示运行时方法的名字。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。
两个类之间,只要方法名相同,那么方法的SEL就是一样的,每一个方法都对应着一个SEL。所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行
如在某一个类中定义以下两个方法: 错误
- (void)setWidth:(int)width;
- (void)setWidth:(double)width;
当然,不同的类可以拥有相同的selector,这个没有问题。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。
工程中的所有的SEL组成一个Set集合,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度上无语伦比!
本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。
@selector()就是取类方法的编号
通过下面三种方法可以获取SEL:
a、sel_registerName函数
b、Objective-C编译器提供的@selector()
c、NSSelectorFromString()方法
4.Method
Method代表类中的某个方法的类型,在Runtime的头文件中的定义如下:
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; // 方法实现
}
- 1.method_name:方法名。
- 2.method_types:方法类型,主要存储着方法的参数类型和返回值类型。
- 3.IMP:方法的实现,函数指针。(下文详解)
class_copyMethodList(Class cls, unsigned int *outCount)
可以使用这个方法获取某个类的成员方法列表。
////
Method用于表示类定义中的方法
我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。
5.Ivar
Ivar代表类中实例变量的类型,在Runtime的头文件中的定义如下:
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
}
class_copyIvarList(Class cls, unsigned int *outCount)
可以使用这个方法获取某个类的成员变量列表。
6.objc_property_t
objc_property_t是属性,在Runtime的头文件中的的定义如下:
typedef struct objc_property *objc_property_t;
class_copyPropertyList(Class cls, unsigned int *outCount)
可以使用这个方法获取某个类的属性列表。
7.IMP
IMP在Runtime的头文件中的的定义如下:
typedef id (*IMP)(id, SEL, ...);
IMP是一个函数指针,它是由编译器生成的。当你发起一个消息后,这个函数指针决定了最终执行哪段代码。
////
IMP实际上是一个函数指针,指向方法实现的地址。
其定义如下:
id (*IMP)(id, SEL,...)
第一个参数:是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)
第二个参数:是方法选择器(selector)
接下来的参数:方法的参数列表。
前面介绍过的SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的IMP,查找过程将在下面讨论。取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针了。
8.Cache
Cache在Runtime的头文件中的的定义如下:
typedef struct objc_cache *Cache
objc_cache的定义如下:
struct objc_cache {
unsigned int mask OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
每调用一次方法后,不会直接在isa指向的类的方法列表(methodLists)中遍历查找能够响应消息的方法,因为这样效率太低。它会把该方法缓存到cache列表中,下次的时候,就直接优先从cache列表中寻找,如果cache没有,才从isa指向的类的方法列表(methodLists)中查找方法。提高效率。
三、runtime的消息转发机制流程
- [someObject msg_send([someObject class],SEL)]步骤:
第一步:检测selector是否可以被忽略,Mac OS X 系统有垃圾回收机制,不会理会retain ,release;
第二步: 检测selctor 对应的Target是否为nill,Rumtime 允许我们对一个nill对象执行任何方法,不会crash;
第三步:当前someObject对象通过isa指针找到对应的objc_Class(调用实例方法)或者objc_metaClass(调用类方法时);
第四步:在objc_Class内部的cache里通过SEL选择子进行匹配,如果找到对应的objc_Mehod,就用objc_Mehod内部的method_IMP找到对应的c函数执行,没有进入下一步;
第五步:在objc_Class内部的method_list里通过SEL选择子进行匹配,如果找到对应的objc_Mehod,就用objc_Mehod内部的method_IMP找到对应的c函数执行,没有进入下一步;
第六步: 通过objc_Class内部的super_class只找到父类,分类去对应的cache、method list寻找,找到就直接执行;没有继续下一步;
第七步:通过父类的root类去找,分类去对应的cache、method list寻找,找到就直接执行;没有继续下一步;
第八步: 如果没有找到,就会执行消息转发(message forwarding);
-
详细介绍消息转发步骤:
rumtime 在发送消息 没有找对用对应目标对象需要执行的任务时,允许我们进行3次修正:
- 方法动态解析: 目标通过自己是想新的IMP函数和resolveInstenceMethod或者resolveClassMethod ; 如果这两个方法参数都是没有找到对应地址的SEL变量,如果实现类存在对应的方法,首先runtime 为当前的SEL变量重新设置IMP指针,并且返回Yes,rumtime会重新执行消息发送;
void fooMethod(id obj, SEL _cmd)
{
NSLog(@"Doing foo");
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if(aSEL == @selector(foo:)){
class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod];
}
- 快速转发 : 找到实现 forwaringTargetingSelector ,参数是没有执行的SEL ,forwaringTargetingSelector内部如果如果自己存在SEL一样的函数,就会将当前的对象返回出去,runtime会重新想当前新的目标对象发送消息;
- (id)forwardingTargetForSelector:(SEL)aSelector {
if(aSelector == @selector(foo:)){
return [[BackupClass alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
- 正常转发: 同快速转发都是想新的目标对象发送消息,但是可以代替快速转发做更多的事。
forwardInvocation: 方法就是一个不能识别消息的分发中心,将这些不能识别的消息转发给不同的消息对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的“吃掉”某些消息,因此没有响应也不会报错。例如:我们可以为了避免直接闪退,可以当消息没法处理时在这个方法中给用户一个提示,也不失为一种友好的用户体验。
其中,参数invocation是从哪来的?在forwardInvocation:消息发送前,runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。所以重写forwardInvocation:的同时也要重写methodSignatureForSelector:方法,否则会抛出异常。当一个对象由于没有相应的方法实现而无法响应某个消息时,运行时系统将通过forwardInvocation:消息通知该对象。每个对象都继承了forwardInvocation:方法,我们可以将消息转发给其它的对象。
- (void)forwardInvocation:(NSInvocation *)invocation {
SEL sel = invocation.selector;
if([alternateObject respondsToSelector:sel]) {
[invocation invokeWithTarget:alternateObject];
} else {
[self doesNotRecognizeSelector:sel];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
if (!methodSignature) {
methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
}
return methodSignature;
}
这里附加NSObject+CrashHandle代码
#import "NSObject+CrashLogHandle.h"
@implementation NSObject (CrashLogHandle)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
//方法签名
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"NSObject+CrashLogHandle---在类:%@中 未实现该方法:%@",NSStringFromClass([anInvocation.target class]),NSStringFromSelector(anInvocation.selector));
}
@end
四、runtime 常用API 总结
第一类 : 为对象工作的API
object_getIvar : 获取对象的某个实例变量的值;
object_setIvar : 设置对象的某个实例变量的值;
object_getClassName : 获取当前对象的isa所指的类的名字;NSStringFromClass(Class )
object_getClass : 获取当前对象的isa所指的类;
object_setClass : 设置对象的isa所指的类;
第二类: 为Class工作的API
class_getName: 获取Class变量的名字; 相当于NSStringFromClass(Class )
class_getSupperClass : 获取父类;
class_isMetaClass : 判断是否是元类;
class_addIvar: 为class添加成员变量; 这个步骤要在 alloc之后register之前才有效果;
class_add_Method : 为当前的类添加实例方法
class_getInstanceMethod 获取当前类的实例方法
class_getClassMethod 获取当前类的实例方法
class——copyMethodList 获取当前类的所有的方法
class_replaceMethod 替换某个类的方法签名对应的实现
class_respondsToSelector 判断某个类是否存在SEL对应的函数
第三类 : 添加类相关
objc_allocateClassPair 创建一个类 里面要穿入你要继承的类Class,新的类名c字符串,大小
objc_disposeClassPair 销毁一个类和它对应的是元类
objc_registerClassPair 将创建的类假如内存,一般在添加完方法、成员变量、属性、协议、分类、扩展后使用
实例化一个类相关
class_createInstance 默认在malloc memory zone.
objc_destructInstance 销毁一个实例对象
成员变量相关
ivar_getName: 获取 Ivar的变量名字
ivar_getTypeEncoding: 获取 Ivar的变量类型
观象对象相关
objc_setAssociatedObject : 设置一个对象的关联对象,让源对象持有关联对象 参数 1 : 源对象 2、唯一的关键一是一个void的指针变量 3、关联对象 4关联规则
objc_getAssociatedObject : 通过关键字获取一个对象它所有持有对象 参数: 1、目标对象 2、关联对象的关键字
objc_removeAssociatedObjects 移除目标对象所有关联的对象 参数: 目标对象
发送消息相关 ****比较关键的
objc_msgSend : 向目标对象发送函数调用消息 参数:1、目标对象 2、SEL函数唯一签名 有关调用过程上面介绍过
runtime 内部相关的结构对象
An opaque type that represents an Objective-C class.
An opaque type that represents a method in a class definition.
An opaque type that represents an instance variable.
An opaque type that represents a category.
An opaque type that represents an Objective-C declared property.
A pointer to the start of a method implementation.
Defines an opaque type that represents a method selector.
Defines an Objective-C method.
Contains an array of method definitions.
Deprecated
Performance optimization for method calls. Contains pointers to recently used methods.
Represents a list of formal protocols.
Defines a property attribute.
五、runtime使用场景
5.1. kvo
介于传统的kvo实现方式过于的死板,只能通过代理方法实现监听回调,因此,我自己用 runtime 写了一个NSOBject+ggzKVO的分类,主要实现 : 一层属性监听和多层属性监听 2、监听回调方法定义函数回调、block实现回调
实现原理:
1、创建目标类的派生类,将派生类的isa指针指向目标类;
2、将观察关系的信息保存在一个字典里,字典放在集成在NSObject的对象里;
3、重写父类的setter方法,在改变父类的值之前获取旧值,之后获取新的值,然后获取观察信息,获取当前key对应的对调函数或者block进行回调
说着简单,不如自己手动写一下
github-custom-runtime-kvo 连接 : https://github.com/ge123/test-runtime-kvo
5.2. kvc :
setValue forKey:(NSString*) key 查找顺序: 去查找对应的set<key>方法,如果没有找到如果本类的accessInstanceVariablesDirectly属性返回YES,则按 _<key>、_is<key> 、key、is<key>的顺序去查找,如果最后还是没找到,就回调setValue:forUndefinedKey方法;我们可以重写setValue:forUndefinedKey方法进行防止报错;
github-custom-runtime-kvo 连接 : https://github.com/ge123/runtime-kvc
5.3. JSON转Model
github-custom-runtime-kvo 连接 : https://github.com/ge123/runtime--
5.4. categary 实现添加属性成员变量
github-custom-runtime-kvo 连接 : https://github.com/ge123/runtime-categary--