runtime介绍:
runtime 叫运行时, 是一套底层C语言的API。我们平时编写的OC代码都是基于runtime实现的。
因为我们程序在编译时期无法完成全部操作(方法的调用,类的创建),需要一个运行时的库,所以出现了runtime。
OC在编译时期不能决定真正调用那个函数,只有在运行的时候才会根据函数名找到对应的函数来调用
所以在编译阶段:OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。
Runtime的作用:
作用一:runtime消息传递
可以先看一下 实例对象、类对象、方法在runtime底层的表达形式:
//实例对象
struct objc_object {
Class isa;
};
//类对象
struct objc_class {
Class isa;
if !OBJC2
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
endif
} ;
//方法列表
struct objc_method_list {
struct objc_method_list *obsolete;
int method_count;
ifdef LP64
int space;
endif
/* variable length structure */
struct objc_method method_list[1];
};
//方法
struct objc_method {
SEL method_name;
char *method_types;
IMP method_imp;
}
实例对象有isa指针 类对象有isa指针和superClass指针
对象调用方法 [obj foo] 的流程是:
底层运行时会被编译器转化为:objc_msgSend(obj, foo)
如果obj是实例对象.
第一步:obj通过isa指针找到Class
第二步:在Class的实例方法列表objc_method_list中查找有没有foo
第三步:如果没有,就在Class的父类superClass中找。
第四步:一直找到根类RootClass。
如果obj是类对象
第一步: obj通过isa指针找到自己的元类meta-class
第二步:在meta-class的类方法类别中查找有没有foo
第三步:如果没有,就在meta-superClass中找
第四步:一直找到meta-RootClass。
如果每一次调用方法 都这么查找的话,涉及到效率问题。
所以出现了 objc_cache。 记录缓存。这样可以先在缓存中找。
Category 在 Runtime 中是用结构体 category_t 来表示的,
typedef struct category_t {
const char *name;//主类名字
classref_t cls;//类
struct method_list_t *instanceMethods;//实例方法的列表
struct method_list_t *classMethods;//类方法的列表
struct protocol_list_t *protocols;//所有协议的列表
struct property_list_t *instanceProperties;//添加的所有属性
} category_t;
也有很多人是这样说的
typedef struct objc_category *Category;
struct objc_category {
char * _Nonnull category_name;
char * _Nonnull class_name;
struct objc_method_list * _Nullable instance_methods;
struct objc_method_list * _Nullable class_methods;
struct objc_protocol_list * _Nullable;
} ;
对比objc_class 可以发现objc_category中少了
struct objc_ivar_list * _Nullable ivars
也就是说没有ivars数组.
变相的解释了,分类为什么不能添加成员。
可以添加属性,但是这个属性需要动态绑定,并且没有生成对应成员变量。
关于分类的问题:
为什么分类的方法优先级高于主类的方法?
因为category_t中的方法列表是插入到主类方法列表前面。所以先执行分类中的方法, 找到了对应的方法,就会停止查找,这里就造成了一种覆盖主类方法的假象。
category实现原理:
我们都知道OC所有的对象在运行时都是用结构体表示的。 category也是,用category_t表示。
1、在编译时期,将分类中实现的方法生成一个结构体 method_list_t 、将声明的属性生成一个结构体 property_list_t 。然后通过这些结构体生成一个结构体 category_t 。
2、然后将结构体 category_t 保存下来。
3、在运行时期,Runtime 会拿到编译时期我们保存下来的结构体 category_t。
4、然后将结构体 category_t 中的实例方法列表、协议列表、属性列表添加到主类中。
5、将结构体 category_t 中的类方法列表 添加到主类的 metaClass 中
引申:可以给协议protocol添加属性
需要在遵守协议的类中,实现属性的setter,getter方法。
作用二:Runtime消息转发
https://www.jianshu.com/p/6ebda3cd8052
如果消息传递,在方法列表中找不到对应的方法。那么就会进入消息转发。
从消息转发到程序报错前有三个机会:
1、动态方法解析
2、备用接收者(快速转发)
3、完整消息转发
动态方法解析
实现这两个方法+ (BOOL)resolveInstanceMethod:(SEL)sel;和 + (BOOL)resolveClassMethod:(SEL)sel;
- (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(foo:)) {
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
return YES;
}
if (sel == @selector(fuu)) {
// 获取 class
Class predicateMetaClass = objc_getClass([NSStringFromClass(self) UTF8String]);
// 根据 class 获取方法的实现
IMP impletor = class_getMethodImplementation(predicateMetaClass, @selector(hahaha));
// 获取实例方法
Method predicateMethod = class_getInstanceMethod(predicateMetaClass, @selector(hahaha));
const char *encoding = method_getTypeEncoding(predicateMethod);
class_addMethod(predicateMetaClass, sel, impletor, encoding);
}
return [super resolveInstanceMethod:sel];
}
这里第一字符v代表函数返回类型void,第二个字符@代表self的类型id,第三个字符:代表_cmd的类型SEL。
void fooMethod(id obj, SEL _cmd) {
NSLog(@"Doing foo");//新的foo函数
}
- (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(methodResolve)) {
// 获取 MetaClass
Class predicateMetaClass = objc_getMetaClass([NSStringFromClass(self) UTF8String]);
// 根据 metaClass 获取方法的实现
IMP impletor = class_getMethodImplementation(predicateMetaClass, @selector(proxyMethod));
// 获取类方法
Method predicateMethod = class_getClassMethod(predicateMetaClass, @selector(proxyMethod));
const char *encoding = method_getTypeEncoding(predicateMethod);
// 动态添加类方法
class_addMethod(predicateMetaClass, sel, impletor, encoding);
return YES;
}
return [super resolveClassMethod:sel];
}
- (void)proxyMethod {
NSLog(@"---veryitman--- +proxyMethod of class's method for OC.");
}
备用接收者(快速转发)
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(foo)) {
return [Person new];//返回Person对象,让Person对象接收这个消息
}
return [super forwardingTargetForSelector:aSelector];
}
通过创建一个新的对象,让这个新的对象去实现方法。
完整消息转发
首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。
如果-methodSignatureForSelector:返回nil ,
Runtime则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。
如果返回了一个函数签名,Runtime就会创建一个NSInvocation 对象
并发送-forwardInvocation:消息给目标对象。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
Person *p = [Person new];
if([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];
}
else {
[self doesNotRecognizeSelector:sel];
}
}
第二步跟第三步的区别:
1、需要重载的API方法的用法不同
前者只需要重载一个API即可,后者需要重载两个API。
前者只需在API方法里面返回一个新对象即可,
后者需要对被转发的消息进行重签并手动转发给新对象(利用 invokeWithTarget:)
2、转发给新对象的个数不同
前者只能转发一个对象,后者可以连续转发给多个对象。
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector==@selector(run)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector: aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL selector =[anInvocation selector];
RunPerson *RP1=[RunPerson new];
RunPerson *RP2=[RunPerson new];
if ([RP1 respondsToSelector:selector]) {
[anInvocation invokeWithTarget:RP1];
}
if ([RP2 respondsToSelector:selector]) {
[anInvocation invokeWithTarget:RP2];
}
}
Runtime在实际项目中的应用
1、关联对象 : 给分类添加属性
2、添加方法,交换方法 :KVO实现
3、消息转发:热更新JSPatch
4、实现NSCoding的自动归档和自动解档
5、实现字典和模型的自动转换
KVO的”isa-swizzling”技术,就是将指针原来指向本类,改成了指向中间类。
就是通过这个方法。
object_setClass(self, [SimpleKVO_Dog class]);
将self 改成 SimpleKVO_Dog类。这样isa指针指向SimpleKVO_Dog。