写在前面,对runtime
的学习,已经悄悄地进行了很长时间,知识结构,也由之前的模模糊糊,变得越来越清晰,网上已经有很多关于runtime
的学习研究篇章了,但是,别人的东西,不是我的东西(包括我的东西,也不是你的东西,如果你看了我的文章,对你有一点启发就好,别的不多求),每个人都会自己的学习记忆体系,把知识转变为自己熟悉的套路,那,在日后回看的时候,也会觉得,还是熟悉的味道,上手,也会极快滴.
1.runtime是什么?(俗一点的说法:说说你对runtime的理解.)
runtime,国内叫运行时,是一套底层的C语言API,objective-c代码,底层都是基于它来实现的.
感受一下发送消息的代码:
#import "ViewController.h"
#import "Person.h"//项目中创建好的一个Person类,它有一个working方法
#import <objc/message.h>
- (void)viewDidLoad {
[super viewDidLoad];
//Person *p = [Person alloc];
//查找build setting -> 搜索msg->设置为NO,才会有msg代码提醒
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
//p = [p init];
p = objc_msgSend(p, sel_registerName("init"));
//[p working];
objc_msgSend(p, @selector(working));
//只有对象才能发消息,所以发消息的前缀用obj
}
2.运行时和编译时特性的对比.
(知识储备)源码的解释流程:预编译->编译->链接->运行
编译:C语言在编译阶段就要切确知道被调用函数的真实类型了,如果在编译时不能确定真实类型,则会报错.
运行时:OC在调用方法/定义类/定义成员变量时,在编译时还是不能知道他们的真实类型.OC把这一切行为推迟到运行时.也就是意味着,有类型不匹配的情况,在运行时才会抛出异常.OC中调用方法,也叫消息发送.注意区别C语言中的函数调用.
注意:编码时,报错信息尽量在编译阶段就调试出来.
3.runtime的相关术语
SEL :方法选择器,他对应方法的名字,OC中方法的名字包括冒号.
id :指向某个类的实例的指针.
Class :指向 objc_class 结构体的指针.
Method :代表类中的某个方法,它存储了方法名(SEL),方法类型(参数类型和返回值类型),方法实现(IMP).
IMP :指向函数的指针,代表了方法的实现.
Cache : 专门用来缓存方法的实现的(IMP).
Property :属性
4.OC中的隐形参数
开发中,我们经常会用到一个全局self,但是你没有想过他是怎么来的呢?其实,这个self,是每个方法中都带有的隐形参数,跟他一起的,还有一个_cmd参数,他是SEL类型的变量,这两个参数,都是苹果在运行时,悄悄咪咪地添加进去的.
有关这两个隐形参数:
①观察这两个表达式typedef id (*IPM) (id, SEL,...)
和objc_msgSend(id, SEL,...)
,我们可以确定,一组id
和SEL
参数,就能确定唯一的方法实现地址,相反,一个确定的方法,也只有唯一的一组id
和SEL
参数.读到这里,你心中的这个谜团,有没有解开了呢?那就是:OC的一个类中,不能有同名的方法.
②开发中,除了上面点出的两个变量之外,你还用过哪些变量感觉"情不知何起,而一往情深的"? 看代码,说出你心中的执行结果.
@implementation Dog : Animal
- (instancetype)init{
if (self = [super init]) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
//打印结果
NSLog(@"%@", NSStringFromClass([self class])); =>Dog
NSLog(@"%@", NSStringFromClass([super class])); =>Dog
很多人会想当然的认为“ super
和self
类似,应该是指向父类的指针吧!”。这是很普遍的一个误区。其实super
是一个 Magic Key Word
, 它本质是一个编译器标示符,和 self
是指向的同一个消息接受者!他们两个的不同点在于:super
会告诉编译器,调用class
这个方法时,要去父类的方法,而不是本类里的。
super
使用的理性解释:当super
接到消息时,编译器会添加一个objc_super
结构体:
struct objc_super {
id receiver; //receiver仍然是self本身
Class class;//Class是指向objc_class结构体的指针,结构体内有指向父类的指针的成员变量,所以规定了`super`直接找父类方法
}
编译器将指向self
的id
指针和class
的SEL
传递给objc_msgSendSuper
函数(参数又进行了一次传递,内容没有变哦),而class
方法只有在NSObject
才能找到,OC底层将class
方法转为object_getClass()
,紧接着又会被编译器将代码转为objc_msgSend(objc_super->receiver,@selector(class))
,因为,一开始就是self
去调用class
方法,打印出来的,也就是Dog
,而不是Animal
.读到这,如果不是很理解,那你多读几遍,毕竟文字嘛,理解起来是没那么形象.实在理解不了,就记住那段"感性的super"吧,开发中,够用的了.
5.消息发送
本来想偷偷懒,用语言描述消息发送的步骤的,但是,写着写着,自己都绕晕了,so,a picture speaks all.
6.动态添加方法
当一个消息被发送出来后,会经过一系列的查找,其中有一个步骤就是判断有没有动态添加方法.那么runtime
提供什么样的接口给外界进行动态添加方法呢?
+ (BOOL)resolveInstanceMethod:(SEL)se
//动态对象方法
+ (BOOL)resolveClassMethod:(SEL)sel;
//动态类方法
通过重写上面方法,调用class_addMethod();
函数来动态添加方法,同时返回YES
即可,如果返回NO
,则会进入消息转发机制.
eg:
#import "ViewController.h"
#import <objc/runtime.h>
@implementation ViewController
//没有返回值没参数类型的函数
void dynamicMethIMP (id self, SEL _cmd){
NSLog(@"我是动态添加的方法实现");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self performSelector:@selector(resolveThisMethodDynamically)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(resolveThisMethodDynamically)) {
/**
@param sel __unsafe_unretained Class cls 类的类型(需要添加方法的类)
@param SEL name 方法选择器(方法的名称)
@param IMP 方法的实现
@param const char *types 函数类型
*/
class_addMethod([self class], sel, (IMP)dynamicMethIMP, "v@");
return YES;
}
return [super resolveClassMethod:sel];
}
@end
7.消息转发
消息转发,前提是没人要的消息,才会被转发.我个人认为是runtime
比较精彩的部分.
1)重定向
消息转发之前,runtime
系统允许外界替换消息的接受者为其他对象,通过-(id)forwardingTargetForSelector:(SEL)aSelector;
该方法不能指定对象为self了.如果没有指定其他对象来发送这个信息,则进入消息转发机制.
2)转发
在转发机制里面,执行的方法是- (void)forwardInvocation:(NSInvocation *)aInvocation;
这个方法需要传入一个NSInvocation
对象,这个对象系统会自动调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
返回一个方法签名对象,用来生成NSInvocation
对象当做参数传进去使用,所以在重写forwardInvocation:
方法来做各种处理(eg:修改方法实现,修改响应对象)的同时也要重写methodSignatureForSelector:
,否则会抛异常.
如果没有实现forwardInvocation:
方法,系统会调-(void)doesNotRecognizeSelector:(SEL)aSelector
方法,如果外界也没有实现这个方法,那么程序就会crash.到此,消息转发就告一段落了.
假如发送了消息之后,没有方法实现,那么,有多少次机会补救呢?我的理解是3次.
8.方法交换
#import "NSObject+CHLog.h"
#import <objc/runtime.h>
#import "CHTools.h"
@implementation NSObject (CHLog)
+(void)load{
//1.获取系统的description对象方法类型
Method instanceDescription = class_getInstanceMethod(self, @selector(description));
//2.获取myLog对象的方法类型
Method ch_instanceDescription = class_getInstanceMethod(self, @selector(myLog));
//3.交换方法
method_exchangeImplementations(instanceDescription, ch_instanceDescription);
//1.获取系统的description类对象方法类型
Method classDescription = class_getClassMethod(self, @selector(description));
//2.获取系统的myLog类对象方法类型
Method ch_classDescription = class_getClassMethod(self, @selector(myLog));
//3.交换方法
method_exchangeImplementations(classDescription, ch_classDescription);
}
- (NSString *)myLog{
NSString *str = [NSString stringWithFormat:@"[文件名:%s], " "[函数名:%s], " "[行号:%d], [时间:%@]\n打印内容:", [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __FUNCTION__, __LINE__, [LXHTools getTodayDetailDateString]] ;
return str;
}
+ (NSString *)myLog{
NSString *str = [NSString stringWithFormat:@"[文件名:%s], " "[函数名:%s], " "[行号:%d], [时间:%@]\n打印内容:", [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __FUNCTION__, __LINE__, [CHTools getTodayDetailDateString]] ;
return str;
}
@end
介绍方法交换,主要是想大家有一个思想在,就是在没有.m文件的情况下,想修改一个类的方法,除了使用继承和分类暴力抢先之外,还可以利用runtime
来实现.并且runtime
有一个好处就是只需要修改一次就能一劳永逸.你设想,你接触一个很老的项目,而项目的需求是要你在系统的方法上添加新的功能,你难道要为系统的类写一个分类,再去每个使用了该方法的类中去导入头文件,再手动把方法替换?如果你不会使用方法交换,那写分类,确实是一个解决的方法.
交换方法的实现,其实就是OC中Method Swizzle
的实践,除了method_exchangeImplementations
,我们还可以利用class_replaceMethod
来修改类,利用method_setImplementation
来直接设置某个方法的IMP,归根到底,都是偷换了selector
的IMP.so far,有没有觉得runtime
非常牛逼,但是,runtime
虽好,使用需谨慎啊.
如下面的使用的时候,调用description方法已经被替换成了调用我的myLog方法。
//项目中用到description方法的地方都会偷偷变成我的myLog方法的实现
- (void)viewDidLoad {
[super viewDidLoad];
// description => myLog 交互这两个方法实现
NSLog(@"%@", [Person description]);
Person *p = [[Person alloc] init];
NSLog(@"%@", [p description]);
}
2016-02-23 16:42:11.599 runtime[56314:6093330] [文件名:NSObject+CHLog.m], [函数名:+[NSObject(CHLog) myLog]], [行号:38], [时间:2016-02-23 16:42:11]
打印内容:
2016-02-23 16:42:11.600 runtime[56314:6093330] [文件名:NSObject+CHLog.m], [函数名:-[NSObject(CHLog) myLog]], [行号:33], [时间:2016-02-23 16:42:11]
打印内容:
9.动态添加属性
在这之前,你是不是也认为category
中只能添加方法,不能添加属性?
但是,利用runtime
,添加属性,也变成了可能!下面是我在UIView
的分类中添加的一个字符串nameTag
属性,以后直接通过点语法,可以设置/获取控件的字符串tag
,设置控件的tag
就不再拘泥于NSInteger
了.
#import "UIView+CHFrame.h"
#import <objc/runtime.h>
@implementation UIView (CHFrame)
static char nametag_key;
- (void)setNameTag:(NSString *)NameTag {
objc_setAssociatedObject(self, &nametag_key, NameTag, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)nameTag {
return objc_getAssociatedObject(self, &nametag_key);
}
- (UIView *)viewWithNameTag:(NSString *)aName {
if (!aName) return nil;
// Is this the right view? 查找view
if ([[self nameTag] isEqualToString:aName])
return self;
// Recurse depth first on subviews;
for (UIView *subview in self.subviews) {
UIView *resultView = [subview viewNamed:aName];
if (resultView) return resultView;
}
// Not found
return nil;
}
- (UIView *)viewNamed:(NSString *)aName {
if (!aName) return nil;
return [self viewWithNameTag:aName];
}
@end
10.字典转模型的安全实现原理
字典转模型,字典,才需要转成模型(将字典中的key,跟模型的属性名一一对应起来,在开发中直接通过点语法来取值,会更方便开发和利于纠错).
1) 自动生成模型属性
通常,服务器返回的字段会比较多,有些字段我们用不上,开发中,多数时候我们都是去字典中一个一个找,然后再去模型中定义对应的属性,下面介绍一个方便设计模型的方法--遍历字典中所有的key,并以@property(nonatomic,strong) NSString *name
的形式拼接打印出来,拷贝去使用就可以了.
//给NSDictionary写一个分类
#import "NSDictionary+Property.h"
@implementation NSDictionary (Property)
// isKindOfClass:判断是否是当前类或者子类
- (void)createModelPropertyFormatter
{
NSMutableString *formatter = [NSMutableString string];
// 遍历字典
[self enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull value, BOOL * _Nonnull stop) {
NSString *formatterKind;
if ([value isKindOfClass:[NSString class]]) {
formatterKind = [NSString stringWithFormat:@"@property (nonatomic, copy) NSString *%@;",key];
} else if ([value isKindOfClass:NSClassFromString(@"__NSCFBoolean")]) {
formatterKind = [NSString stringWithFormat:@"@property (nonatomic, assign) BOOL %@;",key];
} else if ([value isKindOfClass:[NSNumber class]]) {
formatterKind = [NSString stringWithFormat:@"@property (nonatomic, assign) NSInteger %@;",key];
} else if ([value isKindOfClass:[NSArray class]]) {
formatterKind = [NSString stringWithFormat:@"@property (nonatomic, strong) NSArray *%@;",key];
} else if ([value isKindOfClass:[NSDictionary class]]) {
formatterKind = [NSString stringWithFormat:@"@property (nonatomic, strong) NSDictionary *%@;",key];
}
[formatter appendFormat:@"\n%@\n",formatterKind]; //换行
}];
NSLog(@"%@",formatter);
}
@end
//我的经验:将服务器获取到的字段转成plist文件,层级更加清晰,获取字段中的字典数组,遍历数组,得到单个的字典,通过调createModelPropertyFormatter方法,把字典的key统统打印出来,拷贝到模型中,删除多余的字段,此时,模型已经创建好了.
//如果不懂使用这个快捷方法,可以留言,代码这里就不贴出来了.
2) KVC--Key Value Coding
苹果已经提供了一个方法,可以快速进行字典转模型了
#import "CHTagItem.h"
@implementation CHTagItem
+ (instancetype)tagWithDict:(NSDictionary *)dict{
CHTagItem *tagItem = [[self alloc] init];
[tagItem setValuesForKeysWithDictionary:dict];
return tagItem;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{}
@end
setValuesForKeysWithDictionary:dict
快速赋值
setValue:forUndefinedKey:
重写系统方法(空实现),以防找不到一一对应的字段时抛异常.
上面那种方法,只适用比较简单的模型设计.如果服务器返回的字段中字典有嵌套关系,只是简单地重写setValue:forUndefinedKey
方法使程序是不抛异常,你会发现,模型设计得并不是你想要的结果.下面举个:
#import <Foundation/Foundation.h>
#import "CHUserItem.h"
@interface CHCategoryItem : NSObject
@property (nonatomic, strong)NSString *name;
@property(nonatomic, strong)NSString *id;
/** 用户信息模型的集合*/
@property (nonatomic, strong)NSArray<CHUserItem *> *users;
/**用户信息的模型*/
@property (nonatomic, strong)CHUserItem *user;
@end
#import <Foundation/Foundation.h>
@interface CHUserItem : NSObject
@property (nonatomic, strong)NSString *screen_name;
@property (nonatomic, strong)NSString *header;
@property (nonatomic, strong)NSString *fans_count;
@end
在调用setValuesForKeysWithDictionary:
方法时,系统流程如下:
1.遍历模型的属性,调属性的setter方法赋值--setName:
2.如果该属性没有setter方法,系统会去找不带下划线的属性赋值--name.
3.如果该属性没有不带下划线的属性,那系统会去找带下划线的属性去赋值--_name.
4.如果第3步还是没有找到对应的属性赋值,系统就会调setValue:forUndefinedKey
抛出异常,告诉外界找不到该字段去赋值.
根据上面的流程,我们可以通过重写属性的setter方法,拦截数据处理好了再赋值,这样才可以保证字典转模型成功.
- (void)setUsers:(NSArray *)users{
//保存字典数据,这一步是写setter方法的规范写法
_users = users;
//字典转模型
NSMutableArray *arrayM = [NSMutableArray array];
for (NSDictionary *dict in users) {
//前提:在CHUserItem模型中已经实现userItemsWithDict:方法
CHUserItem *item = [CHUserItem userItemsWithDict:dict];
[arrayM addObject:item];
}
//保存模型数据
_users = arrayM;
}
-(void)setUser:(CHUserItem *)user{
_user = user;
NSDictionary *dict = (NSDictionary *)user;
CHUserItem *item = [CHUserItem userItemsWithDict:dict];
_user = item;
}
3) MJExtension的简单实现--runtime的运用
苹果因为不知道你的模型是怎样设计的,所以他只能做到像KVC那样快速赋值.但是我们自己设计好了模型之后,我们可以通过遍历模型的属性,去字典中找对应的字段去赋值,这样不但减少了遍历的次数,而且还排除了找不到未定义的key的错误.MJExtension的实现原理便是这样,下面,我贴出不是那么严谨的MJExtension实现代码来共大家理解.
- (void)test{
// 解析Plist文件
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"category.plist" ofType:nil];
NSDictionary *categoryDict = [NSDictionary dictionaryWithContentsOfFile:filePath];
NSArray *array = categoryDict[@"list"];
NSMutableArray *m = [NSMutableArray array];
for (NSDictionary *dict in array) {
CHCategoryItem *categoryItem = [CHCategoryItem modelWithDictionay:dict];
[m addObject:categoryItem];
NSLog(@"%@,%@,%@",categoryItem.name, categoryItem.users[0].screen_name,categoryItem.user);
}
}
//2016-02-27 23:28:07.931 runtime[12395:6880878] 网红,中二函,<CHUserItem: 0x608000026580>
2016-02-27 23:28:07.932 runtime[12395:6880878] 精品,A号逗比,<CHUserItem: 0x6000000273a0>
2016-02-27 23:28:07.932 runtime[12395:6880878] 搞笑,梁猛搞笑视频,<CHUserItem: 0x6000000276c0>
2016-02-27 23:28:07.933 runtime[12395:6880878] 创意,小越女simida,<CHUserItem: 0x6080000268a0>
2016-02-27 23:28:07.933 runtime[12395:6880878] 视频,说方言的王子涛涛,<CHUserItem: 0x608000026bc0>
2016-02-27 23:28:07.933 runtime[12395:6880878] 图文,阿葩罩爷,<CHUserItem: 0x6000000279e0>
2016-02-27 23:28:07.934 runtime[12395:6880878] 潜力,大哥vip,<CHUserItem: 0x600000027d00>
2016-02-27 23:28:07.934 runtime[12395:6880878] 生活,草莓阿三,<CHUserItem: 0x600000028020>
2016-02-27 23:28:07.934 runtime[12395:6880878] 原创,5毛团队,<CHUserItem: 0x600000028340>
#import <Foundation/Foundation.h>
@protocol ClassNameInItemArray <NSObject>
/**
return 字典,key是模型中的名称,value则是数组所装的模型的名称
*/
+ (NSDictionary *)classNameInItemArray;
@end
@interface NSObject (DictToModel)<ClassNameInItemArray>
/**字典转模型的runtime实现*/
+ (instancetype)modelWithDictionay:(NSDictionary *)dict;
@end
#import "NSObject+DictToModel.h"
#import <objc/runtime.h>
@implementation NSObject (DictToModel)
// 获取类里面所有方法
// class_copyMethodList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)
// 获取类里面的Property
// class_copyPropertyList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)
// Ivar:实例变量 以下划线开头,Property:属性
+ (instancetype)modelWithDictionay:(NSDictionary *)dict
{
id objc = [[self alloc] init];//创建模型对象
unsigned int count = 0; // count:实例变量个数
// 获取实例变量的数组集合
Ivar *ivarLists = class_copyIvarList(self, &count);
// 遍历数组
for (int i = 0; i < count; i++) {
Ivar ivar = ivarLists[i]; // 1.取出单个实例变量
NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)]; // 2.获取实例变量名字(带下划线的)
NSString *key = [name substringFromIndex:1]; // 截串,_name -->name
// 获取实例变量类型,下面需要用到的
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 断点查看可看到type为@\"CHUserItem\" 截串获取:CHUserItem
type = [type stringByReplacingOccurrencesOfString:@"\"" withString:@""];
type = [type stringByReplacingOccurrencesOfString:@"@" withString:@""];
// 根据key去服务器返回的字典中查找对应的value
id value = dict[key];
//取出来的value是字典,并且模型属性类型是自定义的(不是NS开头)
//@property (nonatomic, strong)CHUserItem *user;类型是自定义的类型
if ([value isKindOfClass:[NSDictionary class]] && ![type hasPrefix:@"NS"]) {
// 字典转换成自定义的模型
Class modelClass = NSClassFromString(type);// 使用映射,得到类对象
if (modelClass) {
value = [modelClass modelWithDictionay:value];
}
}
// 判断值是否是数组
if ([value isKindOfClass:[NSArray class]]) {
// 字典数组转换成模型数组.
// 校验self对应的类中,是否实现我的协议方法(该方法的作用请看头文件里的具体说明)
if ([self respondsToSelector:@selector(classNameInItemArray)]) {
id idSelf = self;// 转换成id类型,就能调用任何对象的方法
// 获取数组中字典对应的模型字符串
NSString *type = [idSelf classNameInItemArray][key];
Class classModel = NSClassFromString(type);
NSMutableArray *arrM = [NSMutableArray array];
// 遍历字典数组,生成模型数组
for (NSDictionary *dict in value) {
// 字典转模型
id model = [classModel modelWithDictionay:dict];
[arrM addObject:model];
}
// 把模型数组赋值给value
value = arrM;
}
}
// 给模型中属性赋值
if (value) {
[objc setValue:value forKey:key];
}
}
return objc;
}
@end
plist文件的概览图如下:
如果你英语水平不错,可以点击runtime官网文档进行自己的学习研究.
对文章有什么建议或意见,欢迎纠正.
写文不易,如果觉得本文章对你有用请点个喜欢/关注,谢谢!