概念
Runtime就是一个C语言写的API库,通过Runtime这个库,我们可以在程序运行时对类、实例对象、变量、属性、方法进行各种操作。
- isa:Runtime中每个对象都有一个isa指针。实例对象的isa指向类,类也是一个对象,类的isa指向该类的元类,所有的元类的isa指向基类(NSObject)的元类,基类的元类的isa指针指向自己。需要注意一点,基类(NSObject)的父类为空,而基类的元类的父类就是基类。
- SEL:选择器。我的理解是这就是一个指向方法名字的指针,它的存在就是为了找到函数指针IMP。两个类都有一个相同名字的方法,那么他们的SEL就相同,所以一个SEL可能指向不同的IMP。
- IMP:就是函数指针,也就是方法实现的首地址,找到了他就能执行方法。
- Method:SEL到IMP的映射。
消息机制
- 先判断receiver为不为nil,是的话直接忽略这次消息发送
- 根据isa指针找到实例所属的类
- 在类中根据sel查找IMP,先从方法缓存Cache里找,再从方法分发列表里面找
- 还是没找到的话就根据指向父类的指针superclass找到父类,再从父类里面去找,直到找到基类NSObject里面为止
- 最后还是没找到,那么触发消息转发机制
消息转发机制(轻松学习消息转发)
void run(id self,SEL _cmd){
// NSLog(@"%@正在%@",self,NSStringFromSelector(_cmd));
NSLog(@"%@正在%s",self,sel_getName(_cmd));
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"20");
// if (sel == @selector(run)) {
// class_addMethod(self, sel, (IMP)run, "v@:");
// }
return [super resolveInstanceMethod:sel];
}
-(id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"28");
// return [[Car alloc]init];
return self;
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"34");
NSString *sel = NSStringFromSelector(aSelector);
//判断你要转发SEL
if ([sel isEqualToString:@"run"]) {
//为你的转发方法手动生成签名
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"43");
SEL aSelector = [anInvocation selector];
//新建需要转发的消息对象
Car *aCar = [Car new];
if ([aCar respondsToSelector:aSelector]) {
//唤醒这个方法
[anInvocation invokeWithTarget:aCar];
}
}
应用场景(OC最实用的runtime总结,面试、工作你看我就足够了)
应用场景1:解归档,特别是当模型的属性比较多的时候
核心API:class_copyIvarList这个方法可以得到一个类h和m文件里所有属性的个数及其名字
- (void)encode:(NSCoder *)aCoder {
// 一层层父类往上查找,对父类的属性执行归解档方法
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 如果有实现该方法再去调用
if ([self respondsToSelector:@selector(ignoredNames)]) {
if ([[self ignoredNames] containsObject:key]) continue;
}
id value = [self valueForKeyPath:key];
// 归档
[aCoder encodeObject:value forKey:key];
}
free(ivars);
c = [c superclass];
}
}
- (void)decode:(NSCoder *)aDecoder {
// 一层层父类往上查找,对父类的属性执行归解档方法
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(c, &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 如果有实现该方法再去调用
if ([self respondsToSelector:@selector(ignoredNames)]) {
if ([[self ignoredNames] containsObject:key]) continue;
}
id value = [aDecoder decodeObjectForKey:key];
//解档
[self setValue:value forKey:key];
}
free(ivars);
c = [c superclass];
}
}
应用场景2:方法替换
楼主经常的做法是用自定义方法替换系统方法防止崩溃。比如往数组、字典里写入空值会导致cash,比如数组读取越界元素,比如URL的网址带有中文时该url会被系统判定为空,此时发起request请求会cash等。这时候就可以用runtime进行方法替换,让他们什么都不变却执行我们新写的加了判断的方法。
核心API: class_getInstanceMethod/class_getClassMethod/method_exchangeImplementations这三个API可以取得两个方法的名字并对他们进行交换
//加载这个类的load方法
+(void)load{
//class_getClassMethod : 获取类方法
//class_getInstanceMethod : 获取对象方法
//1.拿到两个方法 苹果原来的URLWithString 和HK_URLWithString
//2.1 类类型
//2.2 方法编号
Method mURLWithStr = class_getClassMethod([NSURL class], @selector(URLWithString:));
Method mHK_URLWithStr = class_getClassMethod([NSURL class], @selector(HK_URLWithString:));
//2.交换这两个方法!(你调用A 个么会执行 B )
method_exchangeImplementations(mURLWithStr, mHK_URLWithStr);
}
+(instancetype)HK_URLWithString:(NSString *)URLString
{
NSURL * url = [NSURL HK_URLWithString:URLString];//会找 URLWithString这个方法
if (url == nil) {
NSLog(@"哥么是一个空的URL");
}
return url;
}
应用场景3:方法的懒加载(这个实际上消息转发机制里面的方案一)
核心API:resolveInstanceMethod/resolveClassMethod当一个类被调用了没有实现实例方法或者类方法就会触发,然后class_addMethod动态添加方法,具体的格式全是C语言的,如下
//C语言的
//所有的C语言的函数里面!都有这两个隐式参数!只要调用,系统都会传递进来!
void eat(id self, SEL _cmd){
NSLog(@"调用了%@对象的%@方法",self,NSStringFromSelector(_cmd));
}
void eat1(id self, SEL _cmd,id obj){
NSLog(@"今晚吃五个%@",obj);
}
//当这个类被调用了没有实现的方法!就会来到这里
+(BOOL)resolveInstanceMethod:(SEL)sel{
// NSLog(@"你没有实现这个方法%@",NSStringFromSelector(sel));
if (sel == @selector(eat)) {
/*
1.cls: 类类型
2.name: 方法编号
3.imp: 方法实现,函数指针!
4.types:函数类型 C字符串(代码) void === "v"
*/
//v代表返回值,后面的都代表参数,具体哪个符号什么意思参看文档,这里@代表Object,:代表方法
class_addMethod([Pseron class],sel, (IMP)eat, "v@:");
}else if(sel == @selector(eat:)){
class_addMethod([Pseron class], sel, (IMP)eat1, "v@:@");
}
return [super resolveInstanceMethod:sel];
}
应用场景4:避免动态绑定
避免动态绑定的唯一办法就是取得方法的地址,并且直接像函数调用一样调用它。当一个方法会被连续调用很多次,而且您希望节省每次调用方法都要发送消息的开销时,使用方法地址来直接调用方法就显得很有效。
应用场景5:给Category添加成员变量
//步骤一:创建一个分类,比如给任何一个对象都添加一个name属性,就是NSObject添加分类(NSObject+Category)
//步骤二:先在.h 中@property 声明出get 和 set 方法,方便点语法调用
@property(nonatomic,copy)NSString *name;
//步骤三:在.m 中重写set 和 get 方法,内部利用runtime 给属性赋值和取值
char nameKey;
- (void)setName:(NSString *)name {
// 将某个值跟某个对象关联起来,将某个值存储到某个对象中
objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, &nameKey);
应用场景6: 字典转模型(这其实就是YYModel或者MJExtension的大致原理)
以往我们都是利用KVC进行字典转模型,但是它还是有一定的局限性,例如:模型属性和键值对对应不上会crash(虽然可以重写setValue:forUndefinedKey:方法防止报错),模型属性是一个对象或者数组时不好处理等问题,所以无论是效率还是功能上,利用runtime进行字典转模型都是比较好的选择。
三种特殊情况:
1.当字典的key和模型的属性匹配不上
2.模型中嵌套模型(模型属性是另外一个模型对象)
3.数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)
-
当字典的key和模型的属性匹配不上
不对应有两种,一种是字典的键值大于模型属性数量,这时候我们不需要任何处理,因为runtime是先遍历模型所有属性,再去字典中根据属性名找对应值进行赋值,多余的键值对也当然不会去看了;另外一种是模型属性数量大于字典的键值对,这时候由于属性没有对应值会被赋值为nil,就会导致crash,我们只需加一个判断即可
- (void)setDict:(NSDictionary *)dict {
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(c, &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 成员变量名转为属性名(去掉下划线 _ )
key = [key substringFromIndex:1];
// 取出字典的值
id value = dict[key];
// 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil而报错
if (value == nil) continue;
// 将字典中的值设置到模型上
[self setValue:value forKeyPath:key];
}
free(ivars);
c = [c superclass];
}
}
-
模型的属性是另外一个模型对象
这时候我们就需要利用runtime的ivar_getTypeEncoding 方法获取模型对象类型,对该模型对象类型再进行字典转模型,也就是进行递归,需要注意的是我们要排除系统的对象类型,例如NSString,下面的方法中我添加了一个类方法方便递归。
- (void)setDict:(NSDictionary *)dict {
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(c, &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 成员变量名转为属性名(去掉下划线 _ )
key = [key substringFromIndex:1];
// 取出字典的值
id value = dict[key];
// 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil而报错
if (value == nil) continue;
// 获得成员变量的类型
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 如果属性是对象类型
NSRange range = [type rangeOfString:@"@"];
if (range.location != NSNotFound) {
// 那么截取对象的名字(比如@"Dog",截取为Dog)
type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
// 排除系统的对象类型
if (![type hasPrefix:@"NS"]) {
// 将对象名转换为对象的类型,将新的对象字典转模型(递归)
Class class = NSClassFromString(type);
value = [class objectWithDict:value];
}
}
// 将字典中的值设置到模型上
[self setValue:value forKeyPath:key];
}
free(ivars);
c = [c superclass];
}
}
+ (instancetype )objectWithDict:(NSDictionary *)dict {
NSObject *obj = [[self alloc]init];
[obj setDict:dict];
return obj;
}
-
模型的属性是一个数组,数组中是一个个模型对象
我们既然能获取到属性类型,那就可以拦截到模型的那个数组属性,进而对数组中每个模型遍历并字典转模型,但是我们不知道数组中的模型都是什么类型,我们可以声明一个方法,该方法目的不是让其调用,而是让其实现并返回模型的类型。
这块语言可能解释不太清楚,可以参考我的demo,直接运行即可。
- (void)setDict:(NSDictionary *)dict {
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(c, &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 成员变量名转为属性名(去掉下划线 _ )
key = [key substringFromIndex:1];
// 取出字典的值
id value = dict[key];
// 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil而报错
if (value == nil) continue;
// 获得成员变量的类型
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 如果属性是对象类型
NSRange range = [type rangeOfString:@"@"];
if (range.location != NSNotFound) {
// 那么截取对象的名字(比如@"Dog",截取为Dog)
type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
// 排除系统的对象类型
if (![type hasPrefix:@"NS"]) {
// 将对象名转换为对象的类型,将新的对象字典转模型(递归)
Class class = NSClassFromString(type);
value = [class objectWithDict:value];
}else if ([type isEqualToString:@"NSArray"]) {
// 如果是数组类型,将数组中的每个模型进行字典转模型,先创建一个临时数组存放模型
NSArray *array = (NSArray *)value;
NSMutableArray *mArray = [NSMutableArray array];
// 获取到每个模型的类型
id class ;
if ([self respondsToSelector:@selector(arrayObjectClass)]) {
NSString *classStr = [self arrayObjectClass];
class = NSClassFromString(classStr);
}
// 将数组中的所有模型进行字典转模型
for (int i = 0; i < array.count; i++) {
[mArray addObject:[class objectWithDict:value[i]]];
}
value = mArray;
}
}
// 将字典中的值设置到模型上
[self setValue:value forKeyPath:key];
}
free(ivars);
c = [c superclass];
}
}
+ (instancetype )objectWithDict:(NSDictionary *)dict {
NSObject *obj = [[self alloc]init];
[obj setDict:dict];
return obj;
}
网友评论