1.什么是Runtime
我们写的代码在程序运行过程中都会被转化成runtime的C代码执行,例如[target doSomething];会被转化成objc_msgSend(target,@selector(doSomething));。
OC中一切都被设计成了对象,我们都知道一个类被初始化成一个实例,这个实例是一个对象。实际上一个类本质上也是一个对象,在runtime中用结构体表示。
相关的定义:
/// 描述类中的一个方法 typedef struct objc_method *Method; /// 实例变量 typedef struct objc_ivar *Ivar; /// 类别Category typedef struct objc_category *Category; /// 类中声明的属性 typedef struct objc_property *objc_property_t;/// 描述类中的一个方法 typedef struct objc_method *Method; /// 实例变量 typedef struct objc_ivar *Ivar; /// 类别Category typedef struct objc_category *Category; /// 类中声明的属性 typedef struct objc_property *objc_property_t;
类在runtime中的表示
//类在runtime中的表示 struct objc_class { Class isa;//指针,顾名思义,表示是一个什么, //实例的isa指向类对象,类对象的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 } OBJC2_UNAVAILABLE; /* Use `Class` instead of `struct objc_class * /
2.消息机制
objc_msgSend,只有对象才能发送消息,因此以objc开头.
使用消息机制的前提:导入#improt<objc/message.h>
消息机制原理:对象根据方法编号(SEL)去映射表查找对应的方法实现
Xcode参数配置
消息机制的简单使用
#import <Foundation/Foundation.h> @interface Person : NSObject +(void)eat; -(void)eat; @end #import "Person.h" @implementation Person +(void)eat { NSLog(@"class"); } -(void)eat { NSLog(@"instance"); } @end // 创建person对象 Person *p = [[Person alloc] init]; // 调用对象方法 [p eat]; // 本质:让对象发送消息 objc_msgSend(p, @selector(eat)); // 调用类方法的方式:两种 // 第一种通过类名调用 [Person eat]; // 第二种通过类对象调用 [[Person class] eat]; // 用类名调用类方法,底层会自动把类名转换成类对象调用 // 本质:让类对象发送消息 objc_msgSend([Person class], @selector(eat));
输出结果:
2016-11-22 19:39:23.516 Test[18510:2960488] instance 2016-11-22 19:39:25.040 Test[18510:2960488] instance 2016-11-22 19:39:26.081 Test[18510:2960488] class 2016-11-22 19:39:26.724 Test[18510:2960488] class 2016-11-22 19:39:27.534 Test[18510:2960488] class
3.获取列表
有时候会有这样的需求,我们需要知道当前类中每个属性的名字(比如字典转模型,字典的Key和模型对象的属性名字不匹配)。
我们可以通过runtime的一系列方法获取类的一些信息(包括属性列表,方法列表,成员变量列表,和遵循的协议列表)。
unsigned int count; //获取属性列表 objc_property_t *propertyList = class_copyPropertyList([self class], &count); for (unsigned int i=0; i<count; i++) { const char *propertyName = property_getName(propertyList[i]); NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]); } //获取方法列表 Method *methodList = class_copyMethodList([self class], &count); for (unsigned int i; i<count; i++) { Method method = methodList[i]; NSLog(@"method---->%@", NSStringFromSelector(method_getName(method))); } //获取成员变量列表 Ivar *ivarList = class_copyIvarList([self class], &count); for (unsigned int i; i<count; i++) { Ivar myIvar = ivarList[i]; const char *ivarName = ivar_getName(myIvar); NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]); } //获取协议列表 __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count); for (unsigned int i; i<count; i++) { Protocol *myProtocal = protocolList[i]; const char *protocolName = protocol_getName(myProtocal); NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]); }
在Xcode上跑一下看看输出吧,需要给你当前的类写几个属性,成员变量,方法和协议,不然获取的列表是没有东西的。
注意,调用这些获取列表的方法别忘记导入头文件#import <objc/runtime.h>。
4.方法调用
如果用实例对象调用实例方法,会到实例的isa指针指向的对象(也就是类对象)操作。
如果调用的是类方法,就会到类对象的isa指针指向的对象(也就是元类对象)中操作。
首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行。
如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现执行
如果没找到,去父类指针所指向的对象中执行1,2.
以此类推,如果一直到根类还没找到,转向拦截调用。
如果没有重写拦截调用的方法,程序报错。
以上的过程给我带来的启发:
重写父类的方法,并没有覆盖掉父类的方法,只是在当前类对象中找到了这个方法后就不会再去父类中找了。
如果想调用已经重写过的方法的父类的实现,只需使用super这个编译器标识,它会在运行时跳过在当前的类对象中寻找方法的过程。
5.拦截调用
在方法调用中说到了,如果没有找到方法就会转向拦截调用。
那么什么是拦截调用呢。
拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理。
+ (BOOL)resolveClassMethod:(SEL)sel; + (BOOL)resolveInstanceMethod:(SEL)sel; //后两个方法需要转发到其他的类处理 - (id)forwardingTargetForSelector:(SEL)aSelector; - (void)forwardInvocation:(NSInvocation *)anInvocation;
第一个方法是当你调用一个不存在的类方法的时候,会调用这个方法,默认返回NO,你可以加上自己的处理然后返回YES。
第二个方法和第一个方法相似,只不过处理的是实例方法。
第三个方法是将你调用的不存在的方法重定向到一个其他声明了这个方法的类,只需要你返回一个有这个方法的target。
第四个方法是将你调用的不存在的方法打包成NSInvocation传给你。做完你自己的处理后,调用invokeWithTarget:方法让某个target触发这个方法。
6.动态添加方法
开发场景:如果一个类方法非常多,加载了到内存的时候也比较耗费资源,需给每个方法生成映射表,可以使用动态给某个类,添加方法解决.
经典面试题:有没有使用preformSelector,其实主要想问有没有添加过方法;
重写了拦截调用的方法并且返回了YES,根据传进来的SEL类型的selector动态添加一个方法。
首先从外部隐式调用一个不存在的方法:
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. Person *p = [[Person alloc] init]; // 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。 // 动态添加方法就不会报错 [p performSelector:@selector(eat)]; } @end @implementation Person // void(*)() // 默认方法都有两个隐式参数, void eat(id self,SEL sel) { NSLog(@"%@ %@",self,NSStringFromSelector(sel)); } // 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来. // 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法 + (BOOL)resolveInstanceMethod:(SEL)sel { //if ([NSStringFromSelector(sel) isEqualToString:@"eat"]) { if (sel == @selector(eat)) { // 动态添加eat方法 // 第一个参数:Class cls 给哪个类添加方法,本例中是self // 第二个参数:SEL name 添加的方法,本例中是重写的拦截调用传进来的selector // 第三个参数:添加方法的函数实现(函数地址),IMP imp 方法的实现,C方法的方法实现可以直接获得。如果是OC方法,可以用+ (IMP)instanceMethodForSelector:(SEL)aSelector;获得方法的实现。 // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd *参数 class_addMethod(self, @selector(eat), (IMP)eat, "v@:"); } return [super resolveInstanceMethod:sel]; } @end
7.关联对象
原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类上
场景:现在你准备用一个系统的类,但是系统的类并不能满足你的需求,你需要额外添加一个属性。
这种情况的一般解决办法就是继承。但是,只增加一个属性,就去继承一个类,总是觉得太麻烦类。
这个时候,runtime的关联属性就发挥它的作用了。
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. // 给系统NSObject类动态添加属性name NSObject *objc = [[NSObject alloc] init]; objc.name = @"噢mygade"; NSLog(@"%@",objc.name); } @end //首先定义一个全局变量,用它的地址作为关联对象的key static const char *key = "name"; @implementation NSObject (Property) - (NSString *)name { /* id object获取谁的关联对象。 const void *key根据这个唯一的key获取关联对象。 */ return objc_getAssociatedObject(self, key); } - (void)setName:(NSString *)name { // 第一个参数:id object给谁设置关联对象。 // 第二个参数:const void *key关联对象唯一的key,获取时会用到 // 第三个参数:id value关联对象 /* 第四个参数:objc_AssociationPolicy关联策略,有以下几种策略: enum { OBJC_ASSOCIATION_ASSIGN = 0, OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, OBJC_ASSOCIATION_COPY_NONATOMIC = 3, OBJC_ASSOCIATION_RETAIN = 01401, OBJC_ASSOCIATION_COPY = 01403 }; */ objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @end
其实,你还可以把添加和获取关联对象的方法写在你需要用到这个功能的类的类别中,方便使用。
//添加关联对象 - (void)addAssociatedObject:(id)object{ objc_setAssociatedObject(self, @selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } //获取关联对象 - (id)getAssociatedObject{ return objc_getAssociatedObject(self, _cmd); }
注意:这里面我们把getAssociatedObject方法的地址作为唯一的key,_cmd代表当前调用方法的地址。
8.方法交换
开发场景:系统自带的方法功能不够,给系统自带的方法扩展一些功能.并且保留原有的功能.
方式1:继承系统的类,重写方法.
方式2:使用runtime,交换方法.
方法交换,顾名思义,就是将两个方法的实现交换。例如,将A方法和B方法交换,调用A方法的时候,就会执行B方法中的代码,反之亦然。
话不多说,这是参考Mattt大神在NSHipster上的文章自己写的代码。
#import "UIViewController+swizzling.h" #import <objc/runtime.h> @implementation UIViewController (swizzling) //load方法会在类第一次加载的时候被调用 //调用的时间比较靠前,适合在这个方法里做方法交换 + (void)load{ //方法交换应该被保证,在程序中只会执行一次 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ //获得viewController的生命周期方法的selector SEL systemSel = @selector(viewWillAppear:); //自己实现的将要被交换的方法的selector SEL swizzSel = @selector(swiz_viewWillAppear:); //两个方法的Method Method systemMethod = class_getInstanceMethod([self class], systemSel); Method swizzMethod = class_getInstanceMethod([self class], swizzSel); //首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败 BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod)); if (isAdd) { //如果成功,说明类中不存在这个方法的实现 //将被交换方法的实现替换到这个并不存在的实现 class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod)); }else{ //否则,交换两个方法的实现 method_exchangeImplementations(systemMethod, swizzMethod); } }); } - (void)swiz_viewWillAppear:(BOOL)animated{ //这时候调用自己,看起来像是死循环 //但是其实自己的实现已经被替换了 [self swiz_viewWillAppear:animated]; NSLog(@"swizzle"); } @end
在一个自己定义的viewController中重写viewWillAppear
- (void)viewWillAppear:(BOOL)animated{ //调用swiz_viewWillAppear:(BOOL)animated 实现方法 [super viewWillAppear:animated]; NSLog(@"viewWillAppear"); }
实例二
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. // 需求:给imageNamed方法提供功能,每次加载图片就判断下图片是否加载成功。 // 步骤一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name; // 步骤二:交换imageNamed和imageWithName的实现,就能调用imageWithName,间接调用imageWithName的实现。 UIImage *image = [UIImage imageNamed:@"123"]; } @end @implementation UIImage (Image) // 加载分类到内存的时候调用 + (void)load { // 交换方法 // 获取imageWithName方法地址 Method imageWithName = class_getClassMethod(self, @selector(imageWithName:)); // 获取imageWithName方法地址 Method imageName = class_getClassMethod(self, @selector(imageNamed:)); // 交换方法地址,相当于交换实现方式 method_exchangeImplementations(imageWithName, imageName); } // 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super. // 既能加载图片又能打印 + (instancetype)imageWithName:(NSString *)name { // 这里调用imageWithName,相当于调用imageName UIImage *image = [self imageWithName:name]; if (image == nil) { NSLog(@"加载空的图片"); } return image; } @end
我的理解:
方法交换对于我来说更像是实现一种思想的最佳技术:AOP面向切面编程。
既然是切面,就一定不要忘记,交换完再调回自己。
一定要保证只交换一次,否则就会很乱。
最后,据说这个技术很危险,谨慎使用。
9.字典转模型
设计模型:字典转模型的第一步
模型属性,通常需要跟字典中的key一一对应
问题:一个一个的生成模型属性,很慢?
需求:能不能自动根据一个字典,生成对应的属性。
解决:提供一个分类,专门根据字典生成对应的属性字符串。
@implementation NSObject (Log) // 自动打印属性字符串 + (void)resolveDict:(NSDictionary *)dict{ // 拼接属性字符串代码 NSMutableString *strM = [NSMutableString string]; // 1.遍历字典,把字典中的所有key取出来,生成对应的属性代码 [dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { // 类型经常变,抽出来 NSString *type; if ([obj isKindOfClass:NSClassFromString(@"__NSCFString")]) { type = @"NSString"; }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFArray")]){ type = @"NSArray"; }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFNumber")]){ type = @"int"; }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFDictionary")]){ type = @"NSDictionary"; } // 属性字符串 NSString *str; if ([type containsString:@"NS"]) { str = [NSString stringWithFormat:@"@property (nonatomic, strong) %@ *%@;",type,key]; }else{ str = [NSString stringWithFormat:@"@property (nonatomic, assign) %@ %@;",type,key]; } // 每生成属性字符串,就自动换行。 [strM appendFormat:@"\n%@\n",str]; }]; // 把拼接好的字符串打印出来,就好了。 NSLog(@"%@",strM); } @end
字典转模型的方式一:KVC
@implementation Status + (instancetype)statusWithDict:(NSDictionary *)dict { Status *status = [[self alloc] init]; [status setValuesForKeysWithDictionary:dict]; return status; } @end
KVC字典转模型弊端:必须保证,模型中的属性和字典中的key一一对应。
如果不一致,就会调用[<Status0x7fa74b545d60> setValue:forUndefinedKey:]报key找不到的错。
分析:模型中的属性和字典的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。
解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖,就能继续使用KVC,字典转模型了。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key { }
字典转模型的方式二:Runtime
思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。
步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类转。
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. // 解析Plist文件 NSString *filePath = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil]; NSDictionary *statusDict = [NSDictionary dictionaryWithContentsOfFile:filePath]; // 获取字典数组 NSArray *dictArr = statusDict[@"statuses"]; // 自动生成模型的属性字符串 // [NSObject resolveDict:dictArr[0][@"user"]]; _statuses = [NSMutableArray array]; // 遍历字典数组 for (NSDictionary *dict in dictArr) { Status *status = [Status modelWithDict:dict]; [_statuses addObject:status]; } // 测试数据 NSLog(@"%@ %@",_statuses,[_statuses[0] user]); } @end @implementation NSObject (Model) + (instancetype)modelWithDict:(NSDictionary *)dict { // 思路:遍历模型中所有属性-》使用运行时 // 0.创建对应的对象 id objc = [[self alloc] init]; // 1.利用runtime给对象中的成员属性赋值 // class_copyIvarList:获取类中的所有成员属性 // Ivar:成员属性的意思 // 第一个参数:表示获取哪个类中的成员属性 // 第二个参数:表示这个类有多少成员属性,传入一个Int变量地址,会自动给这个变量赋值 // 返回值Ivar *:指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到。 /* 类似下面这种写法 Ivar ivar; Ivar ivar1; Ivar ivar2; // 定义一个ivar的数组a Ivar a[] = {ivar,ivar1,ivar2}; // 用一个Ivar *指针指向数组第一个元素 Ivar *ivarList = a; // 根据指针访问数组第一个元素 ivarList[0]; */ unsigned int count; // 获取类中的所有成员属性 Ivar *ivarList = class_copyIvarList(self, &count); for (int i = 0; i < count; i++) { // 根据角标,从数组取出对应的成员属性 Ivar ivar = ivarList[i]; // 获取成员属性名 NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)]; // 处理成员属性名->字典中的key // 从第一个角标开始截取 NSString *key = [name substringFromIndex:1]; // 根据成员属性名去字典中查找对应的value id value = dict[key]; // 二级转换:如果字典中还有字典,也需要把对应的字典转换成模型 // 判断下value是否是字典 if ([value isKindOfClass:[NSDictionary class]]) { // 字典转模型 // 获取模型的类对象,调用modelWithDict // 模型的类名已知,就是成员属性的类型 // 获取成员属性类型 NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)]; // 生成的是这种@"@\"User\"" 类型 -》 @"User" 在OC字符串中 \" -> ",\是转义的意思,不占用字符 // 裁剪类型字符串 NSRange range = [type rangeOfString:@"\""]; type = [type substringFromIndex:range.location + range.length]; range = [type rangeOfString:@"\""]; // 裁剪到哪个角标,不包括当前角标 type = [type substringToIndex:range.location]; // 根据字符串类名生成类对象 Class modelClass = NSClassFromString(type); if (modelClass) { // 有对应的模型才需要转 // 把字典转模型 value = [modelClass modelWithDict:value]; } } // 三级转换:NSArray中也是字典,把数组中的字典转换成模型. // 判断值是否是数组 if ([value isKindOfClass:[NSArray class]]) { // 判断对应类有没有实现字典数组转模型数组的协议 if ([self respondsToSelector:@selector(arrayContainModelClass)]) { // 转换成id类型,就能调用任何对象的方法 id idSelf = self; // 获取数组中字典对应的模型 NSString *type = [idSelf arrayContainModelClass][key]; // 生成模型 Class classModel = NSClassFromString(type); NSMutableArray *arrM = [NSMutableArray array]; // 遍历字典数组,生成模型数组 for (NSDictionary *dict in value) { // 字典转模型 id model = [classModel modelWithDict:dict]; [arrM addObject:model]; } // 把模型数组赋值给value value = arrM; } } if (value) { // 有值,才需要给模型的属性赋值 // 利用KVC给模型中的属性赋值 [objc setValue:value forKey:key]; } } return objc; } @end