Runtime 用处蛮多的但是归结起来有如下几个:
1. 使用Runtime获得对象的属性,方法,协议等信息:
* 实现NSCoding的自动归档和解档 * 实现字典的模型和自动转换
|
这个大家可以阅读YYModel,MJExtension这些开源库,从中学习这部分的用法。
下面给出简化版的代码来方便大家理解Runtime在这方面的应用:
这个比较简单,就是通过class_copyIvarList获取到类到各个实例变量,然后通过KVC方式获取或者设置当前的属性,这里需要注意的是YYModel中针对某些不支持KVC的类型通过向属性的setter/getter发送消息的方式来替换KVC.目前几乎所有的Objective-C对象都能使用KVC,但是一些纯Swift类和结构体是不支持KVC的。或许YYModel出于这方面考虑使用了通过发送消息的方式来取值和设值。
- (instancetype)initWithCoder:(NSCoder *)coder { \ if(self = [super init]) { \ unsigned int outCount = 0; \ Ivar *varList = class_copyIvarList([self class], &outCount); \ for (unsigned int index = 0; index < outCount; index++) { \ const char *var = ivar_getName(varList[index]); \ if(!var || !strlen(var)) continue; \ NSString *varName = [NSString stringWithUTF8String:var]; \ id value = [coder decodeObjectForKey:varName]; \ if(value) { \ [self setValue:value forKey:varName]; \ } \ } \ free(varList); \ } \ return self; \ } \
- (void)encodeWithCoder:(NSCoder *)coder { \ unsigned int outCount = 0; \ Ivar *varList = class_copyIvarList([self class], &outCount); \ for (unsigned int index = 0; index < outCount; index++) { \ const char *var = ivar_getName(varList[index]); \ if(!var || !strlen(var)) continue; \ NSString *varName = [NSString stringWithUTF8String:var]; \ id value = [self valueForKey:varName]; \ [coder encodeObject:value forKey:varName]; \ } \ free(varList); \ } \
|
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface IDLTopic : NSObject<NSCoding>
@property(nonatomic, strong) NSString *topicName;
@property(nonatomic, assign) NSInteger topicRankStar;
@end
@interface IDLUser : NSObject<NSCoding>
@property(nonatomic, strong) NSString *userName;
@property(nonatomic, assign) NSInteger userAge;
@property(nonatomic, strong) NSString *localAddress;
@property(nonatomic, strong) NSString *country;
@property(nonatomic, strong) NSArray<IDLTopic *> *topics;
@end
NS_ASSUME_NONNULL_END
|
#import "IDLUser.h" #import <objc/runtime.h> #import "CommonHeader.h"
@implementation IDLTopic
INIT_WITH_CODER
ENCODE_WITH_CODER
@end
@implementation IDLUser
+ (NSDictionary *)arrayElementModelTypeMap { return @{@"topics":[IDLTopic class]}; }
INIT_WITH_CODER
ENCODE_WITH_CODER
@end
|
- 字典转模型:
这部分的思路就是通过class_copyIvarList获取Model的各个属性,然后去掉属性名字下的下划线,作为查询字典的key。取出值通过KVC设置到model,
这里有两个比较关键的问题,当model属性为字典或者自定义类型的时候需要怎么处理,当model属性为数组的时候怎么处理。大家看下面代码:
NS_ASSUME_NONNULL_BEGIN
@interface IDLTopic : NSObject
@property(nonatomic, strong) NSString *topicName;
@property(nonatomic, assign) NSInteger topicRankStar;
@end
@interface IDLUser : NSObject
@property(nonatomic, strong) NSString *userName;
@property(nonatomic, assign) NSInteger userAge;
@property(nonatomic, strong) NSString *localAddress;
@property(nonatomic, strong) NSString *country;
@property(nonatomic, strong) NSArray<IDLTopic *> *topics;
@end
NS_ASSUME_NONNULL_END
|
#import "IDLUser.h"
@implementation IDLTopic
@end
@implementation IDLUser
+ (NSDictionary *)arrayElementModelTypeMap { return @{@"topics":[IDLTopic class]}; }
@end
|
+ (instancetype)idl_modelWithDic:(NSDictionary *)dictionary {
if(!dictionary || ![dictionary count]) return nil;
id objc = [[self alloc] init];
unsigned int outCount = 0; Ivar *varList = class_copyIvarList([self class], &outCount); if(!outCount) { free(varList); return objc; }
for (unsigned int index = 0; index < outCount; index++) {
NSString *varName = [NSString stringWithUTF8String:ivar_getName(varList[index])]; if(!varName || ![varName length]) continue; varName = [varName substringFromIndex:1]; NSString *varType = [NSString stringWithUTF8String:ivar_getTypeEncoding(varList[index])]; varType = [varType stringByReplacingOccurrencesOfString:@"\"" withString:@""]; varType = [varType stringByReplacingOccurrencesOfString:@"@" withString:@""];
id varValue = dictionary[varName];
if([varValue isKindOfClass:[NSDictionary class]] || ![varType hasPrefix:@"NS"]) { Class modelClass = NSClassFromString(varType); if(modelClass) { varValue = [modelClass idl_modelWithDic:varValue]; } }
if([varValue isKindOfClass:[NSArray class]] || [varValue isKindOfClass:[NSMutableArray class]]) { if([self respondsToSelector:@selector(arrayElementModelTypeMap)]) { NSDictionary *arrayElementModelTypeMap = [self performSelector:@selector(arrayElementModelTypeMap)]; Class elementModelType = arrayElementModelTypeMap[varName];
NSMutableArray *array = [NSMutableArray new]; for (NSDictionary *dict in varValue) { id elementValue = [elementModelType idl_modelWithDic:dict]; [array addObject:elementValue]; } varValue = array; } }
if(varValue) { [objc setValue:varValue forKey:varName]; } } free(varList); return objc; }
|
2. 通过Runtime动态创建一个类,给类增加方法,协议,属性等信息,调用私有方法:
void speak() { NSLog(@"Hello World!"); }
@implementation IDLUser
+ (BOOL)resolveInstanceMethod:(SEL)sel { if([NSStringFromSelector(sel) isEqualToString:@"speak"]) { class_addMethod([self class], @selector(speak), (IMP)speak, "v@:"); } return [super resolveInstanceMethod:sel]; }
@end
[user performSelector:@selector(speak)];
|
UIView *maskView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)]; maskView.backgroundColor = [UIColor redColor]; SEL selector = NSSelectorFromString(@"setMaskView:"); [self.view performSelector:selector withObject:maskView];
|
3. 使用Method Swizzling交换方法实现
Method Swizzling的主要用于改变已经存在的selector 的实现,我们知道每个方法都和一个selector以及IMP相对应。但是iOS的Runtime 提供了一种改变这种对应关系的方法,它就是Method Swizzling,直接上代码,这个例子来自 nshipster Method Swizzling.
Method Swizzling 大体都一个模子:
@implementation IDLViewController (IDLMethodSwizzling)
+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{
SEL originalSEL = @selector(viewWillAppear:); SEL swizzlingSEL = @selector(idl_viewWillAppear:);
Class class = [self class]; Method originalMethod = class_getInstanceMethod(class, originalSEL); Method swizzlingMethod = class_getInstanceMethod(class, swizzlingSEL);
BOOL didAddedSuccess = class_addMethod(class, originalSEL, method_getImplementation(swizzlingMethod), method_getTypeEncoding(swizzlingMethod)); if(didAddedSuccess) { class_replaceMethod(class, swizzlingSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzlingMethod); } }); }
- (void)idl_viewWillAppear:(BOOL)animated { [self idl_viewWillAppear:animated]; NSLog(@"========>"); }
|
之所以放在load方法,主要有两点原因:
- load方法的时机在镜像加载的时候,这时候可以保证在任何逻辑开始之前就交换好了。
- load方法可以避免分类中的Method Swizzling 覆盖主类的Method Swizzling。
这里在最开始的时候会先试探得添加originalSEL对应的方法,如果我们类中还没有实现这个方法那么就会通过class_addMethod进行添加,这时候didAddedSuccess返回YES, 由于已经添加了originalSEL 指向swizzlingMethod的IMP,接下来就是通过class_replaceMethod,将swizzlingSEL指向originalMethod的IMP,从而完成方法的交换。
如果类中实现了originalSEL,那么didAddedSuccess返回NO。由于swizzlingSEL和swizzlingMethod的IMP也已经存在了,所以只要交换下这两个SEL的实现就可以了。
- (void)idl_viewWillAppear:(BOOL)animated { [self idl_viewWillAppear:animated]; NSLog(@"========>"); }
|
看到上面代码,大家估计会觉得不对吧,实际上是这样的,经过Method Swizzling,一旦系统向UIViewController 发送 viewWillAppear:消息,就会执行****- (void)idl_viewWillAppear:(BOOL)animated****的IMP。也就是:
{ [self idl_viewWillAppear:animated]; NSLog(@"========>"); }
|
执行到****[self idl_viewWillAppear:animated]**** 就会执行原来的viewWillAppear: IMP实现。所以这个是没有问题的。
上面只是一个简单的例子,如果大家对Method Swizzling感兴趣,可以在GitHub上搜索 Method Swizzling。 其中jrswizzle,RSSwizzle大家可以了解下。实际项目中没有用过。有机会看下源码的实现再给大家分析下整个实现。无非就是兼容性好一点,但是实际项目中上面应该够用了。
4. 使用消息转发机制实现多继承,热修复,消息分发器,路由:
这部分大家可以看JSPatch,ASpect 这两个开源库。多继承见下一篇博客
5. 使用关联对象给Catogry添加属性
- (NSString *)catogiesName { return objc_getAssociatedObject(self, _cmd); }
- (void)setCatogiesName:(NSString *)catogiesName { objc_setAssociatedObject(self, @selector(catogiesName), catogiesName, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }
|
关于Runtime的API最主要的都集中在****<objc/runtime.h>,<objc/message.h>****这两个文件中大家可以在使用的时候进行查看。
如果大家想要更进一步学习Runtime 可以在Github搜索 Runtime