这篇文章是笔者结合一些参考文章和当初学习Runtime的心得而写的一篇总结,主要讲解Runtime在工作中的运用,没有涉及到太底层的知识,极尽详略,适合初中级学者,水平有限,有错误的地方,还请大佬在评论中指出,一起快乐学习。持续更新中。。。
1.Runtime简介
Runtime 简称运行时,是一套C语言的API(引入
或
)。OC 就是运行时机制,也就是在运行时候的一些机制,其中最主要的是 消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数。
对于OC,函数的调用称为消息发送,属于动态调用过程。在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
2.Runtime消息机制
消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。
验证:
1.在main.m中创建一个对象;
id object = [NSObject alloc];
object = [object init];
2.终端切换到该目录下,执行命令clang -rewrite-objc main.m
,编译后会生成一个main.cpp(C++文件);
3.在.cpp文件中搜索autoreleasepool
,可以找到上述对象创建的底层代码;
id object = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"));
object = ((id (*)(id, SEL))(void *)objc_msgSend)((id)object, sel_registerName("init"));
可以看出调用方法本质就是发消息。[[NSObject alloc]init]
语句发了两次消息,第一次发了alloc 消息,第二次发送init 消息。
4.我们自己来尝试实现,首先导入头文件 #import
,然后让消息机制方法有提示(【build setting
-> 搜索msg
-> objc_msgSend
(YES --> NO)】)。
id object = objc_msgSend(objc_getClass("NSObject"), sel_registerName("alloc"));
object = objc_msgSend(object, sel_registerName("init"));
/**
objc_getClass(const char *name) 获取当前类
sel_registerName(const char *str) 注册个方法编号
objc_msgSend(id self:谁发送消息, SEL op:发送什么消息, ...:参数)
*/
换个写法:
id objc = objc_msgSend([NSObject class], @selector(alloc));
objc = objc_msgSend(objc, @selector(init));
参数处理:
objc_msgSend(p, sel_registerName("height:"), 180);
注:
objc_msgSend:这是个最基本的用于发送消息的函数。
其实编译器会根据情况在objc_msgSend
,objc_msgSend_fpret
,objc_msgSend_stret
,objc_msgSendSuper
, 或 objc_msgSendSuper_stret
五个方法中选择一个来调用。
如果消息是传递给超类,那么会调用名字带有 Super
的函数;
如果消息返回值是浮点数,那么会调用名字带有fpret
的函数;
如果消息返回值是数据结构而不是简单值时,那么会调用名字带有stret
的函数。
3.Runtime方法调用流程
- 对象方法:(保存到类对象的方法列表)
- 类方法:(保存到元类(
Meta Class
)中方法列表)
1.消息传递:
一个对象的方法像这样[obj foo]
,编译器转成消息发送objc_msgSend(obj, foo)
,Runtime
时执行的流程是什么样的呐?
1.首先,通过obj
的isa
指针找到它的 class
;
2.注册方法编号SEL
,可以快速查找;
3.根据方法编号,在 class
的 method list
找 foo
;
3.如果 class
中没到 foo
,继续往它的 superclass
中找 ;
4.一旦找到 foo
这个函数,就去执行它的实现IMP
。
2.Runtime的三次转发流程:
动态方法解析:
Objective-C
运行时会调用 +resolveInstanceMethod:
或者 +resolveClassMethod:
,让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程(动态添加方法)。如果未实现方法,运行时就会移到下一步:forwardingTargetForSelector
。
备用接收者:
如果目标对象实现了-forwardingTargetForSelector:
,Runtime
这时就会调用这个方法,给你把这个消息转发给其他对象的机会。如果还不能处理未知消息,就会进入完整消息转发阶段。
完整消息转发:
Runtime系统会向对象发送methodSignatureForSelector:
消息,并取到返回的方法签名用于生成NSInvocation对象。为接下来的完整的消息转发生成一个 NSMethodSignature对象。NSMethodSignature 对象会被包装成 NSInvocation 对象,forwardInvocation: 方法里就可以对 NSInvocation 进行处理了。如果未实现,Runtime则会发出 -doesNotRecognizeSelector:
消息,程序这时也就挂掉了。
4.Runtime动态添加方法
使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。
经典面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。
方法介绍:
// 参数1:给哪个类添加方法
// 参数2:添加方法的方法编号SEL
// 参数3:添加方法的函数实现IMP(函数地址)
// 参数4:函数的类型,(返回值+参数类型)
class_addMethod(Class cls, SEL name, IMP imp, const char * types)
注:
1.class_addMethod会添加一个覆盖父类的实现,但不会取代原有类的实现。
方法示例:
假如Person
对象调用eat
方法,而该方法并没有实现,则会报错。我们可以利用Runtime
在Person
类中动态添加eat
方法,来实现该方法的调用。
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
p = objc_msgSend(p, sel_registerName("init"));
[p performSelector:@selector(eat)];
}
@end
@implementation Person
/**
void的前面没有+、-号,因为只是C的代码;
必须有两个指定参数(id self,SEL _cmd)
*/
void eat(id self, SEL sel)
{
NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}
// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat)) {
//函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
class_addMethod(self, sel, (IMP)eat, "v@:");
}
return [super resolveInstanceMethod:sel];
}
@end
5.Runtime方法交换(Method Swizzling)
使用场景:当第三方框架或者系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。
方法介绍:
// 交换方法地址,交换两个方法的实现
method_exchangeImplementations(Method m1, Method m2)
方法封装:为了后续调用方便,我们可以将Method Swizzling
功能封装为类方法,作为NSObject
的类别。
@interface NSObject (Swizzling)
+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector
bySwizzledSelector:(SEL)swizzledSelector;
@end
#import "NSObject+Swizzling.h"
#import <objc/message.h>
@implementation NSObject (Swizzling)
+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector
{
Class class = [self class];
//原有方法
Method originalMethod = class_getInstanceMethod(class, originalSelector);
//替换原有方法的新方法
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
//先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {//添加成功:说明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {//添加失败:说明源SEL已经有IMP,直接将两个SEL的IMP交换即可
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
@end
方法示例:例如我们想要替换ViewController生命周期方法,可以这样做。
#import "UIViewController+Swizzling.h"
#import "NSObject+Swizzling.h"
@implementation UIViewController (Swizzling)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self methodSwizzlingWithOriginalSelector:@selector(viewWillAppear:) bySwizzledSelector:@selector(mj_viewWillAppear:)];
});
}
- (void)mj_viewWillAppear:(BOOL)animated{
[self mj_viewWillAppear:animated];
NSLog(@"被调用了");
}
@end
注:
1.swizzling
建议在+load
中完成。+load
和 +initialize
是Objective-C runtime会自动调用两个类方法。+load
是在一个类被初始加载时调用,一定会被调用;+initialize
是在应用第一次调用该类的类方法或实例方法前调用,相当于懒加载方式,可能不被调用。此外 +load
方法还有一个非常重要的特性,那就是子类、父类和分类中的 +load
方法的实现是被区别对待的。换句话说在 Objective-C runtime 自动调用 +load
方法时,分类中的 +load
方法并不会对主类中的 +load
方法造成覆盖。
2.swizzling
应该只在dispatch_once
中完成,由于swizzling
改变了全局的状态,所以我们需要确保在任何情况下(多线程环境,或者被其他人手动再次调用+load方法)只交换一次,防止再次调用又将方法交换回来。+load方法本身即为线程安全,为什么仍需添加dispatch_once,其原因就在于+load方法本身无法保证其中代码只被执行一次。
6.Runtime动态添加属性
场景:分类是不能自定义属性和变量的,这时候可以使用runtime动态添加属性方法;
原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
方法:
/** 关联对象、set方法
id object:给哪个对象添加关联,给哪个对象设置属性
const void *key:关联的key,要求唯一,建议用char 可以节省字节
id value:关联的value,给属性设置的值
objc_AssociationPolicy policy:内存管理的策略
*/
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
// 获取关联的对象、get方法
id objc_getAssociatedObject(id object, const void *key)
// 移除关联的对象
void objc_removeAssociatedObjects(id object)
内存策略对应的属性修饰表:
内存策略 | 属性修饰 | 描述 |
---|---|---|
OBJC_ASSOCIATION_ASSIGN | @property (assign) 或 @property (unsafe_unretained) | 指定一个关联对象的弱引用。 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property (nonatomic, strong) | @property (nonatomic, strong) 指定一个关联对象的强引用,不能被原子化使用。 |
OBJC_ASSOCIATION_COPY_NONATOMIC | @property (nonatomic, copy) | 指定一个关联对象的copy引用,不能被原子化使用。 |
OBJC_ASSOCIATION_RETAIN | @property (atomic, strong) | 指定一个关联对象的强引用,能被原子化使用。 |
OBJC_ASSOCIATION_COPY | @property (atomic, copy) | 指定一个关联对象的copy引用,能被原子化使用。 |
示例:实现一个UIView
的Category
添加自定义属性defaultColor
。
@interface UIView (Color)
@property (nonatomic, strong) UIColor *defaultColor;
@end
@implementation UIView (Color)
static char kDefaultColorKey;
- (void)setDefaultColor:(UIColor *)defaultColor
{
objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)defaultColor {
return objc_getAssociatedObject(self, &kDefaultColorKey);
}
@end
7.NSCoding自动归档解档
场景:如果一个模型有许多个属性,实现自定义模型数据持久化时,需要对每个属性都实现一遍encodeObject
和 decodeObjectForKey
方法,比较麻烦。我们可以使用Runtime来解决。
原理:用runtime
提供的函数遍历Model
自身所有属性,并对属性进行encode
和decode
操作。
方法实现:
#import "MJMusicModel.h"
#import <objc/runtime.h>
@implementation MJMusicModel
// 设置不需要归解档的属性
- (NSArray *)ignoredNames {
return @[@"_musicUrl"];
}
// 归档调用方法
- (void)encodeWithCoder:(NSCoder *)encoder
{
unsigned int count = 0;
// 获得这个类的所有成员变量
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i<count; i++) {
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
// 获得成员变量的名字
const char *name = ivar_getName(ivar);
// 将每个成员变量名转换为NSString对象类型
NSString *key = [NSString stringWithUTF8String:name];
// 忽略不需要归档的属性
if ([[self ignoredNames] containsObject:key]) {
continue;
}
// 归档
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([self class], &count);
for (int i = 0; i<count; i++) {
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
// 获得成员变量的名字
const char *name = ivar_getName(ivar);
// 将每个成员变量名转换为NSString对象类型
NSString *key = [NSString stringWithUTF8String:name];
// 忽略不需要解档的属性
if ([[self ignoredNames] containsObject:key]) {
continue;
}
// 解档
id value = [decoder decodeObjectForKey:key];
// 设置到成员变量身上
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
@end
注:我们可以将归解档两个方法封装为宏,在需要的地方一句宏搞定;也可以写到NSObject一个分类中,方便使用。
8.Runtime字典转模型
原理:利用Runtime,遍历模型中所有属性,根据模型的属性名,去字典中查找key
,取出对应的值,给模型的属性赋值。
步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。
接下来分别介绍一下三种情况所实现的代码:
1.简单的字典转模型
注意:模型属性数量大于字典的键值对时,由于属性没有对应值会被赋值为nil,就会导致crash,所以我们要加一个判断,获取到Value
时,才给模型中属性赋值。
NSDictionary *dict = @{
@"name" : @"xiaoming",
@"age" : @25,
@"weight" : @"60kg",
@"height" : @1.81
};
#import "NSObject+Model.h"
#import <objc/message.h>
@implementation NSObject (Model)
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
// 创建对应的对象
id objc = [[self alloc] init];
// 成员变量个数
unsigned int count = 0;
// 获取类中的所有成员变量
Ivar *ivars = class_copyIvarList(self, &count);
// 遍历所有成员变量
for (int i = 0; i < count; i++) {
// 根据角标,从数组取出对应的成员变量(Ivar:成员变量,以下划线开头)
Ivar ivar = ivars[i];
// 获取成员变量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)
NSString *key = [ivarName substringFromIndex:1];
// 根据成员属性名去字典中查找对应的value
id value = dict[key];
if (value) {
// 给模型中属性赋值
[objc setValue:value forKey:key];
}
}
// 释放ivars
free(ivars);
return objc;
}
@end
2.模型中嵌套模型(模型属性是另外一个模型对象)
利用runtime的ivar_getTypeEncoding
方法获取模型对象类型,对该模型对象类型再进行字典转模型,也就是进行递归,需要注意的是要排除系统的对象类型,例如NSString
。
NSDictionary *dict2 = @{
@"name" : @"xiaoming",
@"age" : @25,
@"body" :@{
@"weight" : @"65kg",
@"height" : @1.82
}
};
// runtime字典转模型二级转换:字典->字典;如果字典中还有字典,也需要把对应的字典转换成模型
if ([value isKindOfClass:[NSDictionary class]]) {
// 获取成员变量类型
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 替换: @\"User\" -> User
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
if (![ivarType hasPrefix:@"NS"]) {
// 字典转换成模型,根据字符串类名生成类对象
Class modelClass = NSClassFromString(ivarType);
if (modelClass) { // 有对应的模型才需要转
// 把字典转模型
value = [modelClass modelWithDict:value];
}
}
}
3.数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)
拦截到模型的数组属性,进而对数组中每个模型遍历并字典转模型,但是我们不知道数组中的模型都是什么类型,需要声明一个方法,该方法目的不是让其调用,而是让其实现并返回模型的类型。
NSDictionary *dict3 = @{
@"name" : @"xiaoming",
@"age" : @25,
@"body" :@{
@"weight" : @"65kg",
@"height" : @1.82
},
@"children" : @[
@{
@"sex" : @"男",
@"love" : @"篮球",
},
@{
@"sex" : @"nv",
@"love" : @"钢琴",
}
],
};
// runtime字典转模型三级转换:字典->数组->字典;NSArray中也是字典,把数组中的字典转换成模型.
if ([value isKindOfClass:[NSArray class]]) {
// 判断对应类有没有实现字典数组转模型数组的协议
// arrayContainModelClass 提供一个协议,只要遵守这个协议的类,都能把数组中的字典转模型
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;
}
}
#import <Foundation/Foundation.h>
@protocol ModelDelegate <NSObject>
@optional
/**
提供一个协议,只要遵守这个协议的类,都能把数组中的字典转模型
*/
+ (NSDictionary *)arrayContainModelClass;
@end
@interface NSObject (Model)
/**
dict -> model
利用runtime 遍历模型中所有属性,根据模型中属性去字典中取出对应的value给模型属性赋值
*/
+ (instancetype)modelWithDict:(NSDictionary *)dict;
@end
实现协议类:
+ (NSDictionary *)arrayContainModelClass
{
// 数组属性 : 数组中的类名
return @{@"children" : @"MJChild"};
}
不忙的时候,就整理知识,经过几天时间的努力,终于写好了。途中参考大量资料,并通过Demo验证其正确性,也算对自己的一次全面级的学习与复习。
下一篇,会深入了解Runtime底层语言。
I’m not perfect. But I keep trying.
参考文献:
苹果官方文档
OC最实用的runtime总结
让你快速上手Runtime
runtime详解
iOS 模式详解—
iOS Runtime详解
装逼技术RunTime的总结篇