iOS开发之Runtime
一直用着YYModel这个第三方库,却没怎么去看里面的实现代码,今天兴致来了,去瞧瞧它的真面目,点进去一看,只有五个文件,主要就是YYClassInfo和NSObject+YYModel两个模块。YYClassInfo主要功能是将Runtime层级的结构体封装到NSObject层级以便调用。尽然这样,那咋们就先来来总结一下Runtime.
一:Runtime 基本介绍
Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。
Runtime
其实有两个版本: “modern
” 和 “legacy
”。我们现在用的 Objective-C 2.0
采用的是现行 (Modern
) 版的 Runtime
系统,只能运行在 iOS
和 macOS 10.5
之后的 64
位程序中。而 macOS
较老的32
位程序仍采用 Objective-C 1
中的(早期)Legacy
版本的 Runtime
系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。
Runtime
基本是用 C
和汇编
写的。苹果和GNU各自维护一个开源的 runtime 版本,这两个版本之间都在努力的保持一致。
高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC
并不能直接编译为汇编语言,而是要先转写为纯C
语言再进行编译和汇编的操作,从OC
到C
语言的过渡就是由runtime来实现的。然而我们使用OC
进行面向对象开发,而C
语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。
RunTime简称运行时,就是系统在运行的时候的一些机制,其中最主要的是消息机制。
对于C语言,函数的调用在编译的时候会决定调用哪个函数,编译完成之后直接顺序执行,无任何二义性。
OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编 译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。
只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。
二:Runtime消息传递
1.OC 在向一个对象发送消息时,runtime 库会根据对象的 isa指针找到该对象对应的类或其父类中查找方法。。
2.注册方法编号(这里用方法编号的好处,可以快速查找)。
3.根据方法编号去查找对应方法。
4.找到只是最终函数实现地址,根据地址去方法区调用对应函数。
每一个对象内部都有一个isa指针,这个指针是指向它的真实类型,根据这个指针就能知道将来调用哪个类的方法。
比如[tableView cellForRowAtIndexPath:indexPath];在编译时RunTime会将上述代码转化成[发送消息]
objc_msgSend(tableView, @selector(cellForRowAtIndexPath:),indexPath);
一个对象的方法像这样[obj foo]
,编译器转成消息发送objc_msgSend(obj, foo)
三:Runtime消息传递
前文介绍了进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject
),如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:
方法报unrecognized selector
错。那么消息转发到底是什么呢?接下来将会逐一介绍最后的三次机会。
1.动态方法解析
2.备用接收者
3.完整消息转发
四:Runtime常用方法
1.获取属性列表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]);
}
2.获取方法列表
unsigned int count;
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i<count; i++) {
Method method = methodList[i];
NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}
3.获取成员变量列表
unsigned int count;
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]);
}
4.获取协议列表
unsigned int count;
__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]);
}
现在有一个Person类,和person创建的xiaoming对象,有test1和test2两个方法
1.获得类方法Class PersonClass = object_getClass([Person class]);
SEL oriSEL = @selector(test1);
Method oriMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
Class PersonClass = object_getClass([xiaoming class]);
SEL oriSEL = @selector(test2);
Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
3. 添加方法
BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
4.替换原方法实现
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
method_exchangeImplementations(oriMethod, cusMethod);
五:Runtime的应用
1.动态的添加对象的成员变量和方法
2.动态交换两个方法的实现
3.拦截并替换方法
4.在方法上增加额外功能
5.实现NSCoding的自动归档和解档
6.实现字典转模型的自动转换
1)动态变量控制
在程序中,xiaoming的age是10,后来被runtime变成了20,来看看runtime是怎么做到的。
1.动态获取XiaoMing类中的所有属性[当然包括私有]
Ivar *ivar = class_copyIvarList([self.xiaoming class], &count);
2.遍历属性找到对应name字段
const char *varName = ivar_getName(var);
3.修改对应的字段值成20
object_setIvar(self.xiaoMing, var, @"20");
4.代码参考
-(void)answer{
unsigned int count = 0;
Ivar *ivar = class_copyIvarList([self.xiaoMing class], &count);
for (int i = 0; i<count; i++) {
Ivar var = ivar[i];
const char *varName = ivar_getName(var);
NSString *name = [NSString stringWithUTF8String:varName];
if ([name isEqualToString:@"_age"]) {
object_setIvar(self.xiaoMing, var, @"20");
break;
}
}
NSLog(@"XiaoMing's age is %@",self.xiaoMing.age);
}
2)动态添加方法
在程序当中,假设XiaoMing的中没有guess这个方法,后来被Runtime添加一个名字叫guess的方法,最终再调用guess方法做出相应。那么,Runtime是如何做到的呢?
1.动态给XiaoMing类中添加guess方法:
class_addMethod([self.xiaoMing class], @selector(guess), (IMP)guessAnswer, "v@:");
这里参数地方说明一下:
(IMP)guessAnswer 意思是guessAnswer的地址指针;
"v@:" 意思是,v代表无返回值void,如果是i则代表int;@代表 id sel; : 代表 SEL _cmd;
“v@:@@” 意思是,两个参数的没有返回值。
2.调用guess方法响应事件:
[self.xiaoMing performSelector:@selector(guess)];
3.编写guessAnswer的实现:
void guessAnswer(id self,SEL _cmd){
NSLog(@"i am from beijing");
}
这个有两个地方留意一下:
void的前面没有+、-号,因为只是C的代码。
必须有两个指定参数(id self,SEL _cmd)
4.代码参考
-(void)answer{
class_addMethod([self.xiaoMing class], @selector(guess), (IMP)guessAnswer, "v@:");
if ([self.xiaoMing respondsToSelector:@selector(guess)]) {
[self.xiaoMing performSelector:@selector(guess)];
} else{
NSLog(@"Sorry,I don't know");
}
}
void guessAnswer(id self,SEL _cmd){
NSLog(@"i am from beijing");
}
3):动态交换两个方法的实现
在程序当中,假设XiaoMing的中有test1和 test2这两个方法,后来被Runtime交换方法后,每次调动test1的时候就会去执行test2,调动test2的时候就会去执行test1, 。那么,Runtime是如何做到的呢?
获取这个类中的两个方法并交换
Method m1 = class_getInstanceMethod([self.xiaoMing class], @selector(test1));
Method m2 = class_getInstanceMethod([self.xiaoMing class], @selector(test2));
method_exchangeImplementations(m1, m2);
交换方法之后,以后每次调用这两个方法都会交换方法的实现
4):拦截并替换方法
在程序当中,假设XiaoMing的中有test1这个方法,但是由于某种原因,我们要改变这个方法的实现,但是又不能去动它的源代码(正如一些开源库出现问题的时候),这个时候runtime就派上用场了。
我们先增加一个tool类,然后写一个我们自己实现的方法-change,
通过runtime把test1替换成change。
Class PersionClass = object_getClass([Person class]);
Class toolClass = object_getClass([tool class]);
////源方法的SEL和Method
SEL oriSEL = @selector(test1);
Method oriMethod = class_getInstanceMethod(PersionClass, oriSEL);
////交换方法的SEL和Method
SEL cusSEL = @selector(change);
Method cusMethod = class_getInstanceMethod(toolClass, cusSEL);
////先尝试給源方法添加实现,这里是为了避免源方法没有实现的情况
BOOL addSucc = class_addMethod(PersionClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
if (addSucc) {
// 添加成功:将源方法的实现替换到交换方法的实现
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else {
//添加失败:说明源方法已经有实现,直接将两个方法的实现交换即
method_exchangeImplementations(oriMethod, cusMethod);
}
5):在方法上增加额外功能
有这样一个场景,出于某些需求,我们需要跟踪记录APP中按钮的点击次数和频率等数据,怎么解决?当然通过继承按钮类或者通过类别实现是一个办法,但是带来其他问题比如别人不一定会去实例化你写的子类,或者其他类别也实现了点击方法导致不确定会调用哪一个,runtime可以这样解决:
@implementation UIButton (Hook)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class selfClass = [self class];
SEL oriSEL = @selector(sendAction:to:forEvent:);
Method oriMethod = class_getInstanceMethod(selfClass, oriSEL);
SEL cusSEL = @selector(mySendAction:to:forEvent:);
Method cusMethod = class_getInstanceMethod(selfClass, cusSEL);
BOOL addSucc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
if (addSucc) {
class_replaceMethod(selfClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else {
method_exchangeImplementations(oriMethod, cusMethod);
}
});
}
- (void)mySendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
[CountTool addClickCount];
[self mySendAction:action to:target forEvent:event];
}
@end
load方法会在类第一次加载的时候被调用,调用的时间比较靠前,适合在这个方法里做方法交换,方法交换应该被保证,在程序中只会执行一次。
6).实现NSCoding的自动归档和解档
如果你实现过自定义模型数据持久化的过程,那么你也肯定明白,如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍encodeObject和 decodeObjectForKey方法,如果这样的模型又有很多个,这还真的是一个十分麻烦的事情。下面来看看简单的实现方式。
假设现在有一个Movie类,有3个属性,它的h文件这这样的
#import <Foundation/Foundation.h>
//1. 如果想要当前类可以实现归档与反归档,需要遵守一个协议NSCoding
@interface Movie : NSObject<NSCoding>
@property (nonatomic, copy) NSString *movieId;
@property (nonatomic, copy) NSString *movieName;
@property (nonatomic, copy) NSString *pic_url;
@end
如果是正常写法, m文件应该是这样的:
#import "Movie.h"
@implementation Movie
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:_movieId forKey:@"id"];
[aCoder encodeObject:_movieName forKey:@"name"];
[aCoder encodeObject:_pic_url forKey:@"url"];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super init]) {
self.movieId = [aDecoder decodeObjectForKey:@"id"];
self.movieName = [aDecoder decodeObjectForKey:@"name"];
self.pic_url = [aDecoder decodeObjectForKey:@"url"];
}
return self;
}
@end
如果这里有100个属性,那么我们也只能把100个属性都给写一遍。
不过你会使用runtime后,这里就有更简便的方法。
下面看看runtime的实现方式:
#import "Movie.h"
#import <objc/runtime.h>
@implementation Movie
- (void)encodeWithCoder:(NSCoder *)encoder
{
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([Movie class], &count);
for (int i = 0; i<count; i++) {
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
// 查看成员变量
const char *name = ivar_getName(ivar);
// 归档
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[encoder encodeObject:value forKey:key];
}
free(ivars);
}
- (id)initWithCoder:(NSCoder *)decoder
{
if (self = [super init]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([Movie class], &count);
for (int i = 0; i<count; i++) {
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
// 查看成员变量
const char *name = ivar_getName(ivar);
// 归档
NSString *key = [NSString stringWithUTF8String:name];
id value = [decoder decodeObjectForKey:key];
// 设置到成员变量身上
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
@end
7.实现字典转模型的自动转换
字典转模型的应用可以说是每个app必然会使用的场景,虽然实现的方式略有不同,但是原理都是一致的:遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。
像几个出名的开源库:JSONModel,MJExtension等都是通过这种方式实现的。
先实现最外层的属性转换
// 创建对应模型对象
id objc = [[self alloc] init];
unsigned int count = 0;
// 1.获取成员属性数组
Ivar *ivarList = class_copyIvarList(self, &count);
// 2.遍历所有的成员属性名,一个一个去字典中取出对应的value给模型属性赋值
for (int i = 0; i < count; i++) {
// 2.1 获取成员属性
Ivar ivar = ivarList[i];
// 2.2 获取成员属性名 C -> OC 字符串
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 2.3 _成员属性名 => 字典key
NSString *key = [ivarName substringFromIndex:1];
// 2.4 去字典中取出对应value给模型属性赋值
id value = dict[key];
// 获取成员属性类型
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
}
如果模型比较简单,只有NSString,NSNumber等,这样就可以搞定了。但是如果模型含有NSArray,或者NSDictionary等,那么我们还需要进行第二步转换。
内层数组,字典的转换
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType containsString:@"NS"]) {
// 是字典对象,并且属性名对应类型是自定义类型
// 处理类型字符串 @\"User\" -> User
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
// 自定义对象,并且值是字典
// value:user字典 -> User模型
// 获取模型(user)类对象
Class modalClass = NSClassFromString(ivarType);
// 字典转模型
if (modalClass) {
// 字典转模型 user
value = [modalClass objectWithDict:value];
}
}
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 objectWithDict:dict];
[arrM addObject:model];
}
// 把模型数组赋值给value
value = arrM;
}
}
我自己觉得系统自带的KVC模式字典转模型就挺好的,假设movie是一个模型对象,dict 是一个需要转化的 [movie setValuesForKeysWithDictionary:dict];这个是系统自带的字典转模型方法,个人感觉也还是挺好用的,不过使用这个方法的时候需要在模型里面再实现一个方法才行,
- (void)setValue:(id)value forUndefinedKey:(NSString *)key重写这个方法为了实现两个目的:1. 模型中的属性和字典中的key不一致的情况,比如字典中有个id,我们需要把它赋值给uid属性;2. 字典中属性比模型的属性还多的情况。
如果出现以上两种情况而没有实现这个方法的话,程序就会崩溃。
这个方法的实现:
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
if ([key isEqualToString:@"id"]) {
self.uid = value;
}
}