Runtime
Runtime的特性主要是消息(方法)的传递,如果消息(方法)在对象中找不到,就会进行转发。
Runtime的介绍:
Objective-C 是一个基于C语言的扩展,并且在C语言的基础上增加了面向对象特性和Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。
高级编程语言想要成为可执行文件能被计算机识别,首先要转化成汇编语言,然后在汇编成机器语言。因为机器语言是计算机能够识别的唯一语言,所以任何语言最终都要转化成机器语言才能被计算机所识别。所以OC语言必须要经过转化成C语言然后才能进行汇编成机器语言从而被识别。从OC到C就是通过runtime来实现的。
而OC是面向对象的,C是面向过程的,所以这也就需要将 面向对象的类 转化成 面向过程的结构体。
Runtime消息转发:
1.1 OC的方法调用流程
下面以实例对象调用方法[blackDog walk]为例描述方法调用的流程:
1、编译器会把`[blackDog walk]`转化为`objc_msgSend(blackDog,SEL)`,SEL为@selector(walk)。
2、Runtime会在blackDog对象所对应的Dog类的方法缓存列表里查找方法的SEL(先查找缓存表里面的方法,有则直接取,没有则需要遍历整个methodList表)
3、如果没有找到,则在Dog类的方法分发表查找方法的SEL。(类由对象isa指针指向,方法分发表即methodList)
4、如果没有找到,则在其父类(设Dog类的父类为Animal类)的方法分发表里查找方法的SEL(父类由类的superClass指向)
5、如果没有找到,则沿继承体系继续下去,最终到达NSObject类。
6、如果在234的其中一步中找到,则定位了方法实现的入口,执行具体实现
7、如果最后还是没有找到,会面临两种情况:``(1) 如果是使用`[blackDog walk]`的方式调用方法````(2) 使用`[blackDog performSelector:@selector(walk)]`的方式调用方法
1.2 消息转发流程
1、动态方法解析接收到未知消息时(假设blackDog的walk方法尚未实现),runtime会调用+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)(这一步以方法是否被实现为准,如果方法没有被实现,不管返回值是YES or NO ,都会继续传递给下一级)
2、备用接收者如果以上方法没有做处理,runtime会调用- (id)forwardingTargetForSelector:(SEL)aSelector方法。如果该方法返回了一个非nil(也不能是self)的对象,而且该对象实现了这个方法,那么这个对象就成了消息的接收者,消息就被分发到该对象。适用情况:通常在对象内部使用,让内部的另外一个对象处理消息,在外面看起来就像是该对象处理了消息。比如:blackDog让女朋友whiteDog来接收这个消息
3、完整消息转发在- (void)forwardInvocation:(NSInvocation*)anInvocation方法中选择转发消息的对象,其中anInvocation对象封装了未知消息的所有细节,并保留调用结果发送到原始调用者。比如:blackDog将消息完整转发給主人dogOwner来处理
实现一个完整转发的例子如下:
#import "ViewController.h"
#import "objc/runtime.h"
@interface Person: NSObject
@end
@implementation Person
- (void)foo {
NSLog(@"Doing foo");//Person的foo函数
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. //执行foo函数
[self performSelector:@selector(foo)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES;//返回YES,进入下一步转发
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;//返回nil,进入下一步转发
}
- (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];
} }
@end
Runtime的使用:
1.1 json->model
原理描述:用runtime提供的函数遍历Model自身所有属性,如果属性在json中有对应的值,则将其赋值。
核心方法:在NSObject的分类中添加方法
- (instancetype)initWithDict:(NSDictionary*)dict {
if(self= [selfinit]) {
//(1)获取类的属性及属性对应的类型
NSMutableArray * keys = [NSMutableArray array];
NSMutableArray * attributes = [NSMutableArray array];
/*
* 例子
* name = value3 attribute = T@"NSString",C,N,V_value3
* name = value4 attribute = T^i,N,V_value4
*/
unsignedintoutCount;
objc_property_t * properties = class_copyPropertyList([self class], &outCount);
for(inti = 0; i < outCount; i ++) {
objc_property_tproperty = properties[i];
//通过property_getName函数获得属性的名字
NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
[keysaddObject:propertyName];
//通过property_getAttributes函数可以获得属性的名字和@encode编码
NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
[attributesaddObject:propertyAttribute];
}
//立即释放properties指向的内存
free(properties);
//(2)根据类型给属性赋值
for(NSString* keyinkeys) {
if([dictvalueForKey:key] ==nil)continue;
[selfsetValue:[dictvalueForKey:key]forKey:key];
}
}
return self;
}
//注释:::::现在直接使用一下方法也可
[self setValuesForKeysWithDictionary:dic];
1.2 一键序列化
原理描述:用runtime提供的函数遍历Model自身所有属性,并对属性进行encode和decode操作。
核心方法:在Model的基类中重写方法:
- (id)initWithCoder:(NSCoder*)aDecoder {
if(self = [super init]) {
unsigned int outCount;
Ivar* ivars =class_copyIvarList( [self class] , &outCount);
for(inti = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String: ivar_getName(ivar)];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
return self;
}
- (void)encodeWithCoder:(NSCoder*)aCoder {
unsigned int outCount;
Ivar* ivars =class_copyIvarList ( [self class], &outCount);
for(inti = 0; i < outCount; i ++) {
Ivarivar = ivars[i];
NSString * key = [NSString stringWithUTF8String: ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}
打印结果:
2018-04-01 13:00:45.423385+0800 ocram[88353:23279961] Doing foo
1.3 访问私有变量
我们知道,OC中没有真正意义上的私有变量和方法,要让成员变量私有,要放在m文件中声明,不对外暴露。如果我们知道这个成员变量的名称,可以通过runtime获取成员变量,再通过getIvar来获取它的值。方法:
Ivar ivar = class_getInstanceVariable([Model class],"_str1");
NSString* str1 = object_getIvar(model, ivar);
1.4. 关联对象(Objective-C Associated Objects)给分类增加属性
我们都是知道分类是不能自定义属性和变量的。下面通过关联对象实现给分类添加属性。
关联对象Runtime提供了下面几个接口:
//关联对象
void objc_setAssociatedObject(id object,const void*key,id value, objc_AssociationPolicy policy)
//获取关联的对象
id objc_getAssociatedObject(id object,const void*key)
//移除关联的对象
void objc_removeAssociatedObjects(id object)
参数解释
id object:被关联的对象
const void*key:关联的key,要求唯一
id value:关联的对象
objc_AssociationPolicy policy:内存管理的策略
下面实现一个UIView的Category添加自定义属性defaultColor。
#import "ViewController.h"
#import "objc/runtime.h"
@interfaceUIView (DefaultColor)
@property (nonatomic, strong) UIColor *defaultColor;
@end
@implementation UIView (DefaultColor)
@dynamic defaultColor;
//也可以使用方法的指针地址作为唯一的key 例如:@selector(defaultColor)
static char kDefaultColorKey;
- (void)setDefaultColor:(UIColor*)defaultColor {
objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)defaultColor {
return objc_getAssociatedObject(self, &kDefaultColorKey);
}
@end
@interfaceViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[superviewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
UIView*test = [UIView new];
test.defaultColor = [UIColor blackColor];
NSLog(@"%@", test.defaultColor);
}
@end
打印结果:
2018-04-01 15:41:44.977732+0800 ocram[2053:63739]
UIExtendedGrayColorSpace 0 1
1.5 Method Swizzling(俗称黑魔法)
简单说就是进行方法交换
method Swizzling原理
每个类都维护一个方法(Method)列表,Method则包含SEL和其对应IMP的信息,方法交换做的事情就是把SEL和IMP的对应关系断开,并和新的IMP生成对应关系
*****在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。
****每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP。
交换方法的几种实现方式
利用 method_exchangeImplementations 交换两个方法的实现
利用 class_replaceMethod 替换方法的实现
利用 method_setImplementation 来直接设置某个方法的IMP
使用实例:
1.5.1方法添加
//class_addMethod(Class _Nullable __unsafe_unretained cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
class_addMethod([self class], sel, (IMP)fooMethod,"v@:");
@cls 被添加方法的类
@name 添加的方法的名称的SEL
@imp 方法的实现。该函数必须至少要有两个参数,self,_cmd
@类型编码
1.5.2方法替换
下面实现一个替换ViewController的viewDidLoad方法的例子
@implementation ViewController
+ (void)load {
static dispatch_once_tonceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector =@selector(viewDidLoad);
SEL swizzledSelector =@selector(jkviewDidLoad);
Method originalMethod = class_getInstanceMethod(class,originalSelector);
Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);
//judge the method named swizzledMethod is already existed.
BOOL didAddMethod =class_addMethod(class, originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));
// if swizzledMethod is already existed.
if(didAddMethod) {
class_replaceMethod(class, swizzledSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));
} else{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)jkviewDidLoad {
NSLog(@"替换的方法");
[self jkviewDidLoad];
}
- (void)viewDidLoad {
NSLog(@"自带的方法");
[superviewDidLoad];
}
@end
swizzling应该只在+load中完成。 在Objective-C的运行时中,每个类有两个方法都会自动调用。+load是在一个类被初始装载时调用,+initialize是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。
swizzling应该只在dispatch_once中完成,由于swizzling改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatch 的 dispatch_once满足了所需要的需求,并且应该被当做使用swizzling的初始化单例方法的标准。
1.6 KVO实现
全称是Key-value observing,翻译成键值观察。提供了一种当其它对象属性被修改的时候能通知当前对象的机制。再MVC大行其道的Cocoa中,KVO机制很适合实现model和controller类之间的通讯。
KVO的实现依赖于Objective-C强大的Runtime,当观察某对象A时,KVO机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性keyPath的setter方法。setter方法随后负责通知观察对象属性的改变状况。
Apple使用了isa-swizzling来实现KVO。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A的新类,该类继承自对象A的本类,且KVO为NSKVONotifying_A重写观察属性的setter方法,setter方法会负责在调用原setter方法之前和之后,通知所有观察对象属性值的更改情况。
KVO的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和didChangeValueForKey:,在存取数值的前后分别调用 2 个方法:
被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该keyPath的属性值即将变更;
当改变发生后,didChangeValueForKey:被调用,通知系统该keyPath的属性值已经变更;之后,observeValueForKey:ofObject:change:context:也会被调用。且重写观察属性的setter方法这种继承方式的注入是在运行时而不是编译时实现的
- (void)setName:(NSString*)newName {
[selfwillChangeValueForKey:@"name"];//KVO 在调用存取方法之前总调用
[supersetValue:newName forKey:@"name"];//调用父类的存取方法 [selfdidChangeValueForKey:@"name"];//KVO 在调用存取方法之后总调用
}
参考文章:
https://www.jianshu.com/p/07b6c4a40a90