学习资料
南峰子博客
Objective-C中的Runtime
runtime源码
onecat
详解Runtime运行时机制
补充
神经病院Objective-C Runtime入院第一天—isa和Class
说明
这篇文章只是留做自己以后回顾用,内容是参考上面几位大大的文章,然后根据自己的理解做了些整合以及思考.由于是初学,理解不到位,如有疑问可以评论,大家一起探讨.也可以通过上面链接进入大大的文章寻找答案.
初学
概念
- runtime 即
运行时
,是一个将 C 语言转化为面向对象语言的扩展,是一套 底层的 C语言 API,基本用 C 和 汇编语言编写. - 与 C 语言不同的是, Objective-C 中,函数调用在编译的时候不会真正决定调用哪个函数,只要有函数声明,即使没有函数实现也可以编译成功.这里的函数调用属于动态调用过程.
- Objective-C 的函数调用就是消息发送,属于动态调用过程.只有在真正运行的时候才会根据函数的名称找到对应的函数来调用.
- Objective-C 的动态特性(动态类型
dynamic typing
,动态绑定dynamic binding
,动态加载dynamic loading
)都是基于 runtime - runtime源码
源码
打开objc.xcodeproj
项目,我们主要了解/Public Headers
目录下的objc.h
和runtime.h
两个文件
objc.h
文件中:
typedef struct objc_class *Class;
Class
是一个指向结构体objc_class
的指针,这就是我们所说的类
typedef struct objc_object {
Class isa;
} *id;
id
是一个指向结构体objc_object
的指针,这就是我们所说的对象
isa
:是一个指向当前对象所属的类的指针,也就是上段代码的Class
.它是每个对象结构体的首个成员,是个 Class
类型的变量
runtime.h
文件中:
我们首先看到objc_class
结构体的实现:
struct objc_class {
Class isa;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类,,它定义了本类的超类.类对象所属类型(isa 指针指向的类型)是另一个类,叫做"元类"(metaClass)
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; // 该类方法定义的列表 类的实例方法都在methodLists里,类方法在元类的methodLists里
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议列表
#endif
} OBJC2_UNAVAILABLE;
-
isa
:上面我们说了它是一个指向当前对象所属的类的指针
,不难猜测Class(即类)
本身也是Objective-C中的对象,称之为类对象(class object)
,而objc_object
称为实例对象(instance object)
这里我们就会问了,objc_object
中的 isa 指向它所属的类,那么objc_class
中的 isa 指向什么?
我们需要知道类也是继承的,Objective-C
中有两个根类,分别是 NSObject 和 NSProxy ,其中 NSObject 大家经常用,而 NSProxy 则不常见,如果有兴趣可以自己去了解.同时,每个类都有它的元类(metaClass),类是元类的对象,objc_class
中的 isa 就是指向objc_class
的metaClass.
当然objc_object
和objc_class
中的成员是不一样的:
objc_object(实例对象)中isa指针指向的类结构称为class(也就是该对象所属的类)其中存放着普通成员变量与动态方法(“-”开头的方法,实例方法);此处isa指针指向的类结构称为metaclass,其中存放着static类型的成员变量与static类型的方法(“+”开头的方法,类方法) -
super_class
: 父类,,它定义了本类的超类.类对象所属类型(isa 指针指向的类型)是另一个类,叫做"元类"(metaClass) -
ivars
: 该类的而成员变量列表 -
methodLists
: 该类方法定义的列表 类的实例方法都在methodLists里,类方法在元类的methodLists里 -
cache
: 方法缓存列表,objc_msgSend(下文详解)每调用一次方法后,就会把该方法缓存到cache列表中,下次调用的时候,会优先从cache列表中寻找,如果cache没有,才从methodLists中查找方法。提高效率。 -
protocols
: 协议列表
看图说话:
上图中的层次关系主要是这样四条:
Instance of SubClass
-> SubClass
-> SubClass(meta)
Instance of SuperClass
-> SuperClass
-> SuperClass(meta)
Instance of RootClass
-> RootClass
-> RootClass(meta)
SubClass
-> SuperClass
-> RootClass
类也是一个对象,它是另外一个类的实例,这个就是“元类”,元类里面保存了类方法的列表,类里面保存了实例方法的列表。实例对象的isa指向类,类对象的isa指向元类,元类对象的isa指针指向一个“根元类”(root metaclass)。所有子类的元类都继承父类的元类,换而言之,类对象和元类对象有着同样的继承关系。
1.Class是一个指向objc_class结构体的指针,而id是一个指向objc_object结构体的指针,其中的isa是一个指向objc_class结构体的指针。其中的id就是我们所说的对象,Class就是我们所说的类。
2.isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用isKindOfClass:方法来确定实例对象的类。因为KVO的实现机制就是将被观察对象的isa指针指向一个中间类而不是真实的类。
SEL 是selector在Objective-C中的表示类型。selector可以理解为区别方法的唯一标识.
typedef struct objc_selector *SEL;
它是映射到方法的字符串, SEL 类型代表着方法的签名.在类对象的 methodLists 中存储着该签名与方法代码的对应关系,每个方法都有一个与之对应的 SEL 类型的对象,根据一个 SEL 对象就可以找到方法的地址,进而调用
Method 代表类中的某个方法的类型,在Runtime的头文件中的定义如下:
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE; // 方法类型,主要存储方法的参数类型和返回值类型
IMP method_imp OBJC2_UNAVAILABLE; // 方法的实现,函数指针.
} OBJC2_UNAVAILABLE;
获取某个类的成员方法列表
class_copyMethodList(Class cls, unsigned int *outCount)
Ivar 代表类中实例变量的类型,在Runtime的头文件中的定义如下:
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
}
获取某个类的成员变量列表
class_copyIvarList(Class cls, unsigned int *outCount)
objc_property_t 是属性,在Runtime的头文件中的的定义如下:
获取某个类的属性列表
class_copyPropertyList(Class cls, unsigned int *outCount)
IMP 是方法的实现,在Runtime的头文件中的的定义如下:
typedef id (*IMP)(id, SEL, ...);
IMP(implementation)是一个函数指针,它是由编译器生成的。当你发起一个消息后,这个函数指针决定了最终执行哪段代码。
Cache 在Runtime的头文件中的的定义如下:
typedef struct objc_cache *Cache
struct objc_cache {
unsigned int mask OBJC2_UNAVAILABLE; // 指定分配cache buckets的总数。在方法查找中,Runtime使用这个字段确定数组的索引位置
unsigned int occupied OBJC2_UNAVAILABLE; // 实际占用cache buckets的总数
Method buckets[1] OBJC2_UNAVAILABLE; //指定Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。
};
每调用一次方法后,不会直接在isa指向的类的方法列表(methodLists)中遍历查找能够响应消息的方法,因为这样效率太低。它会把该方法缓存到cache列表中,下次的时候,就直接优先从cache列表中寻找,如果cache没有,才从isa指向的类的方法列表(methodLists)中查找方法。提高效率。
** Catagory** 在Runtime的头文件中的的定义如下:
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; // 协议列表
}
这就是我们平时使用的类别
** Objective-C的消息传递机制**
在面向对象的编程中,对象调用方法的过程就叫做发送消息.在编译时,程序的源代码就会从对象发送消息转换成 runtime 的 objc_msgSend 函数调用.
举个栗子:
某实例对象 person 调用唱歌方法 sing
[person sing];
runtime会将消息转成这样的代码
objc_msgSend(person,@selector(sing))
传递消息的几种函数:
objc_msgSend
:普通的消息都会通过该函数发送。
objc_msgSend_stret
:消息中有结构体作为返回值时,通过此函数发送和接收返回值。
objc_msgSend_fpret
:消息中返回的是浮点数,可交由此函数处理。
objc_msgSendSuper
:和objc_msgSend类似,这里把消息发送给超类。
objc_msgSendSuper_stret
:和objc_msgSend_stret类似,这里把消息发送给超类。
objc_msgSendSuper_fpret
:和objc_msgSend_fpret类似,这里把消息发送给超类。
编译器会根据情况选择一个函数来执行。
objc_msgSend
发送消息的原理:
-
第一步: 检测这个 selector 是不是要忽略
-
第二步: 检测这个 target 是不是 nil 对象. nil 对象发送任何一个消息都会被忽略掉
-
第三步:
- 当是实例对象调用实例方法时: 它会首先在自身 isa 指针指向的class的 methodLists 中查找该方法,如果找不到则会通过 class 的 super_class 指针找到父类的类对象结构体,然后从其 methodLists 中查找该方法,如果仍然找不到,则继续 super_class 向上一级父类对象结构体中查找,直至 rootClass;
-
当时类对象调用类方法时: 它会首先在自身 isa 指针指向的metaclass的 methodLists 中查找该方法,如果找不到则会通过 class 的 super_class 指针找到父类的metaclass 对象结构体,然后从其 methodLists 中查找该方法,如果仍然找不到,则继续 super_class 向上一级父类metaclass结构体中查找,直至 root MetaClass;
-
第四步: 当 selector 不可忽略 + target 不为 nil + 第三步中找不到方法,则会进入消息转发(动态方法解析)
消息转发(message forwarding)
在上面第四步中,对象发出的消息无法解读,它就会将消息实施转发.转发的主要步骤函数如下:
// 第一步: 我们不动态添加方法,返回 NO, 进入第二步.....我们一般在声明了方法但没有实现的情况下,在这步解析中给对象添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return NO ;
}
// 第二步: 指定备选对象来响应 aSelector
// 当有备选对象响应,消息处理;没有则进入第三步
- (id)forwardingTargetForSelector:(SEL)aSelector
{
// // 情况一: 有备选备选响应
// People *p = [[People alloc] init] ;
//
// return p ;
// 情况二: 无备选对象响应
return nil ;
}
// 第三步: 返回方法签名,进入第四步,,,如果返回 nil, 则消息无法处理
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
// 情况一 : 返回签名
if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"] ;
}
return [super methodSignatureForSelector:aSelector] ;
//
// // 情况二 : 返回 nil
// return nil ;
}
// 第四步: 通过 aInvacation 对象做各种不同的处理,例如修改方法实现,修改响应对象等等
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
// // 1: we change the object which is called
// People *p = [[People alloc] init] ;
// p.name = @"hahhahahah" ;
// [anInvocation invokeWithTarget:p] ;
// 2: we change the impelement of the method
[anInvocation setSelector:@selector(dance)] ;
[anInvocation invokeWithTarget:self] ; // 这里我们还要指定哪个对象来实现....如果指定的方法是别的类的,对该类进行声明...我们这里用的是自己类的方法,所以用 self
// [anInvocation invoke] ; // invoke 方法默认调用的对象是 self,与 [anInvocation invokeWithTarget:self] ; 作用相同
}
// 消息无法处理时进入这里,,如果没有实现这个方法则程序 crash
- (void) doesNotRecognizeSelector:(SEL)aSelector
{
NSLog(@"没有找到该方法!!!!") ;
}
消息转发流程(图片来自网络).jpg
** 看图说消息转发步骤:**
-
第一步:对象在收到无法解读的消息后,首先调用
resolveInstanceMethod:
方法决定是否动态添加方法。如果返回YES,则调用class_addMethod
动态添加方法,消息得到处理,结束;如果返回NO,则进入下一步; -
第二步: 会进入
forwardingTargetForSelector:
方法,用于指定备选对象响应这个selector,不能指定为self。如果返回某个对象则会调用对象的方法,结束。如果返回nil,则进入下一步; -
第三步:这步我们要通过
methodSignatureForSelector:
方法签名,如果返回nil,则消息无法处理。如果返回methodSignature,则进入下一步; -
第四步:这步调用
forwardInvocation:
方法,我们可以通过anInvocation对象做很多处理,比如修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果失败,则进入doesNotRecognizeSelector
方法,抛出异常,此异常表示选择子最终未能得到处理。
结合上面的步骤函数看,相信你能有点初步的了解.
实战
说来说去,我们还是要运用到项目中才有用.我们学习前面的理论知识,我们自然会思考怎么运用到项目中?在哪里用?什么情况下用?
由于我也是初学,怎么运用都是从上面的几篇文章了解到的.现在写这个仍然是参考大大的文章,记录下来,只为以后用来回顾.自己写的符合自己的思路,以后看起来会好理解一点.同时,这也是看了好几篇文章总结的自己需要的东西.
下面是我回顾上面的理论,根据自己的理解,大致学到的怎么利用 runtime 知识进行实战编程.
-
首先,在消息发送时我们经历了四步.根据每一步所做的事情,我们可以做的事情就是: 在调用方法的具体的实现之前把原来的方法给替换成自己想做的事情.这就是 方法交换(method swizzing),我们通常在 .m 文件的
load
函数中做. -
对于一些我们无法看到的
具体实现
的类,我们希望它有一些我们需要的属性和方法时,我们可以 给这个类 关联属性和动态添加方法 -
在消息转发的过程中,我们应该能做一些事情.当我们调用了一个只声明没有写实现的方法时,编译时不会出错,但运行便会崩溃,所以我们可以在消息转发的过程中做一些判断、添加新的方法、修改方法、修改对象。
-
最后,再运行时我们可以通过runtime获取某个类的变量列表、方法列表,对于一个拥有很多属性的类,对其做一些面向很多属性的操作时,我们显然可以利用runtime进行批操作,而不用一个一个属性进行操作。例如我们从后台获取到的数据model化以后对对象进行归档。字典转模型 和 归档 就是runtime两个普遍的应用。
1.动态创建类 + 动态添加方法
#pragma mark - 动态创建一个类
- (void) createPersonClass
{
// 动态创建对象: 创建一个名为 Person 的类,它是 NSObject 的子类
Class Person = objc_allocateClassPair([NSObject class], "Person", 0) ;
/**
* 为该类添加一个 eat 的方法 class_addMethod(Class cls, SEL name, IMP imp, const char *types)
*
* @param cls 被添加方法的类
* @param name 可以理解为方法名,貌似可以随便起名
* @param imp 实现这个方法的函数
* @param types 一个定义该函数的返回值类型和参数类型的字符串
*
*/
// 关于参数二与参数三
// 参数二是方法的声明,参数三是方法的实现,,实例对象调用方法(参数二)执行参数三里面的内容
// 关于最后一个参数的解释
// "v@:@"
// v -> 表示 void, 如果是 i 则表示 int
// @ -> 表示参数 id (self)
// : -> 表示 SEL(_cmd)
// @ -> 表示 id(str) ,当没有参数时不写,当多个参数是就写多个
SEL eat = sel_registerName("eat:") ; // 注册 eat 方法
class_addMethod(Person, eat, (IMP) eatFun, "v@:@") ;
// 为该类添加成员变量
class_addIvar(Person, "_name", sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*)) ; // NSString
class_addIvar(Person, "_age", sizeof(int), sizeof(int), @encode(int)) ; // int
// 注册该类
objc_registerClassPair(Person);
// 创建一个实例对象
id p = [[Person alloc] init] ;
// KVC 动态改变对象的实例变量
[p setValue:@"许嵩" forKey:@"name"] ;
[p setValue:@29 forKey:@"age"] ;
// 从类中获取成员变量并赋值
Ivar ageIvar = class_getInstanceVariable(Person, "_age") ;
object_setIvar(p, ageIvar, @30) ;
// performSelector 是运行时负责去找方法的,在编译时不做任何校验
//[p performSelector:@selector(eat)] ;
// 调用 eat 方法,这么写好点(需要引入 <objc/message.h> )
// 强制转换objc_msgSend函数类型为带三个参数且返回值为void函数,然后才能传三个参数
((void (*)(id, SEL, id))objc_msgSend)(p,eat, @"香蕉") ;
p = nil ; // 当 Person 类或者它的子类的实例还存在,则不能调用 objc_disposeClassPair 这个方法.因此必须先销毁实例才能销毁类
objc_disposeClassPair(Person) ; // 销毁类
}
2.消息转发时添加方法+改变对象+改变实现
#import "Bird.h"
#import "People.h"
@implementation Bird
// 第一步: 我们不动态添加方法,返回 NO, 进入第二步.....我们一般在声明了方法但没有实现的情况下,在这步解析中给对象添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return NO ;
}
// 第二步: 指定备选对象来响应 aSelector
// 当有备选对象响应,消息处理;没有则进入第三步
- (id)forwardingTargetForSelector:(SEL)aSelector
{
// // 情况一: 有备选备选响应
// People *p = [[People alloc] init] ;
//
// return p ;
// 情况二: 无备选对象响应
return nil ;
}
// 第三步: 返回方法签名,进入第四步,,,如果返回 nil, 则消息无法处理
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
// 情况一 : 返回签名
if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"] ;
}
return [super methodSignatureForSelector:aSelector] ;
//
// // 情况二 : 返回 nil
// return nil ;
}
// 第四步: 通过 aInvacation 对象做各种不同的处理,例如修改方法实现,修改响应对象等等
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
// // 1: we change the object which is called
// People *p = [[People alloc] init] ;
// p.name = @"hahhahahah" ;
// [anInvocation invokeWithTarget:p] ;
// 2: we change the impelement of the method
[anInvocation setSelector:@selector(dance)] ;
[anInvocation invokeWithTarget:self] ; // 这里我们还要指定哪个对象来实现....如果指定的方法是别的类的,对该类进行声明...我们这里用的是自己类的方法,所以用 self
// [anInvocation invoke] ; // invoke 方法默认调用的对象是 self,与 [anInvocation invokeWithTarget:self] ; 作用相同
}
// 消息无法处理时进入这里,,如果没有实现这个方法则程序 crash
- (void) doesNotRecognizeSelector:(SEL)aSelector
{
NSLog(@"没有找到该方法!!!!") ;
}
- (void)dance
{
NSLog(@"你竟然跳舞了!!!") ;
}
@end
3. 关联对象
.h中:
#import "People.h"
typedef void (^CallBack)(); // 回调
@interface People (Associated)
@property (nonatomic,strong) NSString *newAblum ; // 新专辑
@property (nonatomic,copy) CallBack associatedCallBack ; // 回调
@end
.m中:
#import "People+Associated.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation People (Associated)
// 添加属性
- (void) setNewAblum:(NSString *)newAblum
{
// 设置关联属性
// 第一个入参: 关联对象
// 第二个入参: key 值,唯一并且是常量(static char),,我们这里选择子作为 key
// 第三个入参: 关联类型 OBJC_ASSOCIATION_RETAIN_NONATOMIC 与 (nonatomic,strong) 对应
/*
* OBJC_ASSOCIATION_ASSIGN = (assign) or (unsafe_unretained)
* OBJC_ASSOCIATION_RETAIN_NONATOMIC = (nonatomic,strong)
* OBJC_ASSOCIATION_COPY_NONATOMIC = (nonatomic,copy)
* OBJC_ASSOCIATION_RETAIN = (atomic,strong)
* OBJC_ASSOCIATION_COPY = (atomic,copy)
*/
objc_setAssociatedObject(self, @selector(newAblum), newAblum, OBJC_ASSOCIATION_RETAIN_NONATOMIC) ;
}
- (NSString *)newAblum
{
// get 关联对象
return objc_getAssociatedObject(self, @selector(newAblum)) ;
}
// 添加回调 -- 实际开发过程中使用的更多
- (void) setAssociatedCallBack:(CallBack)associatedCallBack
{
objc_setAssociatedObject(self, @selector(associatedCallBack), associatedCallBack, OBJC_ASSOCIATION_COPY_NONATOMIC) ;
}
- (CallBack)associatedCallBack
{
return objc_getAssociatedObject(self, @selector(associatedCallBack)) ;
}
@end
添加属性没有什么意义,我们平时在开发过成功中用的比较多的就是添加回调了。
4. 归档
- (void)setIgnoredIvarNames:(NSArray *)ignoredIvarNames
{
objc_setAssociatedObject(self, @selector(ignoredIvarNames),ignoredIvarNames, OBJC_ASSOCIATION_RETAIN_NONATOMIC) ;
}
- (NSArray *)ignoredIvarNames
{
return objc_getAssociatedObject(self, @selector(ignoredIvarNames)) ;
}
- (void)encode:(NSCoder *)aCoder
{
unsigned int outCount = 0 ;
Ivar *ivars = class_copyIvarList([self class], &outCount) ;
for (unsigned int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i] ;
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)] ;
if ([self.ignoredIvarNames containsObject:key]) {
continue ;
}
id value = [self valueForKey:key] ;
[aCoder encodeObject:value forKey:key] ;
}
free(ivars) ;
}
- (void)decode:(NSCoder *)aDecoder
{
unsigned int outCount = 0 ;
Ivar *ivars = class_copyIvarList([self class], &outCount) ;
for (unsigned int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i] ;
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)] ;
if ([self.ignoredIvarNames containsObject:key]) {
continue ;
}
id value = [aDecoder decodeObjectForKey:key] ;
[self setValue:value forKey:key] ;
}
free(ivars) ;
}
5. 方法交换
// 全局替换 UIViewController 的 dealloc 函数
// 在 load 函数中利用 runtime 交换两个方法的实现
+ (void)load
{
static dispatch_once_t onceToken ;
dispatch_once(&onceToken, ^{
Method nativeDealloc = class_getInstanceMethod(self, NSSelectorFromString(@"dealloc")) ;
Method myDealloc = class_getInstanceMethod(self, @selector(my_dealloc)) ;
method_exchangeImplementations(nativeDealloc, myDealloc) ;
}) ;
}
- (void)my_dealloc
{
NSLog(@"%@销毁了",self) ;
[self my_dealloc] ;
}
补充:
刚刚在这里看到一篇利用runtime
进行万能界面跳转
万能界面跳转
最后
由于我个人水平有限,文中如果有错误的地方,希望指正,三颗柚~
demo下载链接
网友评论