Runtime是什么
顾名思义,Runtime是运行时, 是 Objective-C 区别于 C 语言这样的静态语言的一个非常重要的特性。
对于静态语言,函数的调用会在编译期就已经决定好,在编译完成后直接执行。OC 是一门动态语言,函数调用变成了消息发送,在编译期不知道要调用哪个函数。 Runtime 就是去解决如何在运行时期找到具体调用的方法。
#if !OBJC_TYPES_DEFINED
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
struct objc_protocol_list {
struct objc_protocol_list *next;
long count;
Protocol *list[1];
};
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;
} OBJC2_UNAVAILABLE;
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;
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;
} OBJC2_UNAVAILABLE;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
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;
} OBJC2_UNAVAILABLE;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
在OC中怎么调用方法
实例对象中存放 isa 指针,有 isa 指针可以找到实例对象所属的类对象 ,类中存放着实例方法列表。 在编译时期,根据方法名字会生成方法的唯一的标识 SEL。IMP 是函数指针, 指向函数实现。 简化过程即:instance -> class -> methodList -> SEL -> IMP -> 实现函数
具体细化方法的调用分三种情况: 1,当调用实例方法时,通过 isa 指针找到实例对应的 class对象并且在其中的缓存方法列表以及方法列表中进行查询,如果找不到则根据 super_class 指针在父类中查询,直至根类(NSObject).
2,当调用类方法时,通过 isa 指针找到实例对应的 metaclass 并且在其中的缓存方法列表以及方法列表中进行查询,如果找不到则根据 super_class 指针在父类的metaclass中查询,直至根类的metaClass.
3,如果还没找到则进入消息转发过程。
整个 Runtime 的核心就是 objc_msgSend 函数,通过给类发送 SEL 以传递消息,找到匹配的 IMP。如下的这张图描述了对象的内存布局。
消息转发机制
简化图如下:
消息转发共分为3个步骤:
-
消息动态处理阶段
-
消息快速转发阶段
-
消息常规转发阶段
消息动态处理阶段
检查该类是否实现了resolveInstanceMethod: 方法,然后检查是否动态向该类添加了方法。
- (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(run)) {
SEL readSEL = @selector(readBook);
Method readM = class_getInstanceMethod(self, readSEL);
IMP readImp = method_getImplementation(readM);
const char *type = method_getTypeEncoding(readM);
return class_addMethod(self, sel, readImp, type);
}
return [super resolveInstanceMethod:sel];
}
+ (BOOL)resolveClassMethod:(SEL)name
{
return [super resolveClassMethod:name];
}
动态方法解析的实质: 在该方法内动态向该类添加方法。添加后类对象里面的实例方法列表和缓存方法列表中就有此方法了,返回true后,系统会自动再次遍历继承链去查找imp。
消息快速转发阶段
对象内部可能还有其他可以响应此方法的对象,所以这个方法是转发SEL去对象内部的其他可以响应该方法的对象。
- (id)forwardingTargetForSelector:(SEL)aSelector {
return [[Car alloc] init];
}
消息常规转发阶段
常规转发本质上跟快速转发是一样的,都是切换接受消息的对象,但是常规转发切换接受消息的对象更复杂一些,快速转发只需返回一个可以响应的对象就可以了,常规转发还需要手动将响应方法切换给响应对象。
第三步有2个步骤:
1.创建签名
2.转发方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
NSString *sel = NSStringFromSelector(selector);
if([sel isEqualString:@"run"]) {
return [NSMethodSignature signatureWithObjcTypes:"v@:"];
}
return [sunper methodSignatureForSelector:selector];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
SEL selector = [invocation selector];
//新建需要转发消息的对象
Car *car = [[Car alloc] init];
if([car respondsToSelector:selector]){
[invocation invokeWithTarget:car];
}
}
总结:在三个步骤的每一步,消息接受者都还有机会去处理消息。同时,越往后面处理代价越高,最好的情况是在第一步就处理消息,这样runtime会在处理完后缓存结果,下回再发送同样消息的时候,可以提高处理效率。第二步转移消息的接受者也比进入常规转发流程的代价要小,如果到最后一步forwardInvocation的话,就需要处理完整的NSInvocation对象了。
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;
} OBJC2_UNAVAILABLE;
从category的定义也可以看出category可为类添加实例方法、类方法、协议、属性)和不可为类添加实例变量。
category和关联对象
在category里面是无法为category添加实例变量的。但是我们很多时候需要在category中添加和对象关联的值:
#import "MyClass.h"
@interface MyClass (Category1)
@property(nonatomic,copy) NSString *name;
@end
----------
#import "MyClass+Category1.h"
#import <objc/runtime.h>
@implementation MyClass (Category1)
+ (void)load
{
NSLog(@"%@",@"load in Category1");
}
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self,
"name",
name,
OBJC_ASSOCIATION_COPY);
}
- (NSString*)name
{
NSString *nameObject = objc_getAssociatedObject(self, "name");
return nameObject;
}
@end
category中的方法
当category中的方法methodA和原类中的方法同名时,Category中的方法会替换原来中的方法,但是没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA,这事category的方法被放到了方法列表的前面,而原来类的方法被放到了方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会返回,殊不知后面可能还有一样名字的方法。
load和initialize
- load
load方法在程序启动时加载类或者分类的时候调用;
------分类中load方法不会覆盖本类的load方法。
load函数的加载顺序为:superClass -> class -> category 注意:Runtime调用+(void)load时没有autorelease pool: 原因是runtime调用+(void)load的时候,程序还没有建立其autorelease pool,所以那些需要使用到autorelease pool的代码,都会出现异常。这一点是非常需要注意的,也就是说放在+(void)load中的对象都应该是alloc出来并且不能使用autorelease来释放。
2,initialize
initialize是在类或者其子类的第一个方法被调用前调用。 ------分类中initialize方法会覆盖本类的initialize方法。
initialize函数的调用顺序为:superClass -> class 或者 superClass-> category
如果该类是子类:
a,子类中没有实现 + (void)initialize 消息,那么则会调用其父类的实现。父类的 + (void)initialize 可能会被调用多次。
b,子类中实现 + (void)initialize 消息,会先调用其父类的实现,再调用子类的实现。
c,如果类包含分类(扩展 catagory),且分类重写了initialize方法,那么则会调用分类的 initialize 实现,而原类的该方法实现不会被调用。子父类调用顺序不变。
这个机制同 NSObject 的其他方法(除 + (void)load 方法) 一样,即如果原类同该类的分类包含有相同的方法实现,那么原类的该方法被隐藏而无法被调用。
load和initialize的使用场景
4,使用场景
-
load load方法是线程安全的,而且一定会调用且只会调用一次,一般可以用来交换方法Method Swizzle。
-
initialize initialize方法主要用来对一些不方便在编译期初始化的对象进行赋值,或者说对一些静态常量进行初始化操作
KVO的原理
KVO的全称 Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。
- (void)viewDidLoad {
[super viewDidLoad];
Person *p1 = [[Person alloc] init];
Person *p2 = [[Person alloc] init];
p1.age = 1;
p1.age = 2;
p2.age = 2;
// self 监听 p1的 age属性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
p1.age = 10;
[p1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@改变了%@", object, keyPath,change);
}
// 打印内容
监听到<Person: 0x604000205460>的age改变了{
kind = 1;
new = 10;
old = 2;
}
在添加监听之后,age属性的值在发生改变时,就会通知到监听者,执行监听者的observeValueForKeyPath方法。
KVO底层实现原理
1,person对象内存结构图
在调用setage方法的时候,首先会通过p2对象中的isa指针找到Person类对象,然后在类对象中找到setage方法。然后找到方法对应的实现。
2,person加了KVO后的内存结构图
p1对象的isa指针在经过KVO监听之后已经指向了NSKVONotifyin_Person类对象,NSKVONotifyin_Person其实是Person的子类,那么也就是说其superclass指针是指向Person类对象的,NSKVONotifyin_Person是runtime在运行时生成的。那么p1对象在调用setage方法的时候,肯定会根据p1的isa找到NSKVONotifyin_Person,在NSKVONotifyin_Person中找setage的方法及实现。
NSKVONotifyin_Person中的setage方法中其实调用了 Fundation框架中C语言函数 _NSsetIntValueAndNotify,_NSsetIntValueAndNotify内部做的操作是先调用willChangeValueForKey 将要改变方法,之后调用父类的setage方法对成员变量赋值,最后调用didChangeValueForKey已经改变方法。didChangeValueForKey中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath方法中。
注意:NSKVONotifyin_Person内重写的class内部实现大致为
- (Class) class {
// 得到类对象,在找到类对象父类
return class_getSuperclass(object_getClass(self));
}
apple不希望将NSKVONotifyin_Person类暴露出来,并且不希望我们知道NSKVONotifyin_Person内部实现,所以在内部重写了class类,直接返回Person类,所以外界在调用p1的class对象方法时,是Person类。
常见KVO相关问题
1,iOS用什么方式实现一个对象的KVO?(KVO的本质是什么?)
---------当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,改为指向一个全新的通过Runtime动态创建的子类,子类拥有自己的set方法实现,set方法实现内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。
2,如何手动触发KVO 答. 被监听的属性的值被修改时,就会自动触发KVO。如果想要手动触发KVO,则需要我们自己调用willChangeValueForKey和didChangeValueForKey方法即可在不改变属性值的情况下手动触发KVO,并且这两个方法缺一不可。
Person *p1 = [[Person alloc] init];
p1.age = 1.0;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
[p1 willChangeValueForKey:@"age"];
[p1 didChangeValueForKey:@"age"];