Runtime 用处蛮多的但是归结起来有如下几个:

1. 使用Runtime获得对象的属性,方法,协议等信息:
* 实现NSCoding的自动归档和解档
* 实现字典的模型和自动转换

这个大家可以阅读YYModel,MJExtension这些开源库,从中学习这部分的用法。

下面给出简化版的代码来方便大家理解Runtime在这方面的应用:

  • 实现NSCoding的自动归档和解档

这个比较简单,就是通过class_copyIvarList获取到类到各个实例变量,然后通过KVC方式获取或者设置当前的属性,这里需要注意的是YYModel中针对某些不支持KVC的类型通过向属性的setter/getter发送消息的方式来替换KVC.目前几乎所有的Objective-C对象都能使用KVC,但是一些纯Swift类和结构体是不支持KVC的。或许YYModel出于这方面考虑使用了通过发送消息的方式来取值和设值。

#ifndef CommonHeader_h
#define CommonHeader_h

#define INIT_WITH_CODER \
- (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; \
} \

#define ENCODE_WITH_CODER \
- (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); \
} \

#endif /* CommonHeader_h */

#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"这种
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);

// 如果要交换类方法可以使用下面代码替换上面的
// Class class = object_getClass((id)self);
// Method originalMethod = class_getClassMethod(class, originalSelector);
// Method swizzledMethod = class_getClassMethod(class, swizzledSelector);

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方法,主要有两点原因:

  1. load方法的时机在镜像加载的时候,这时候可以保证在任何逻辑开始之前就交换好了。
  2. 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

Contents
  1. 1. 1. 使用Runtime获得对象的属性,方法,协议等信息:
  2. 2. 2. 通过Runtime动态创建一个类,给类增加方法,协议,属性等信息,调用私有方法:
  3. 3. 3. 使用Method Swizzling交换方法实现
  4. 4. 4. 使用消息转发机制实现多继承,热修复,消息分发器,路由:
  5. 5. 5. 使用关联对象给Catogry添加属性