一:基本概念
Runtime基本是用C和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。Objective-C 从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码,通过 Foundation 框架的NSObject类定义的方法,通过对 runtime 函数的直接调用。大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。
- RunTime简称运行时,就是系统在运行的时候的一些机制,其中最主要的是消息机制。
- 对于C语言,函数的调用在编译的时候会决定调用哪个函数,编译完成之后直接顺序执行,无任何二义性。
- OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编 译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。
- 只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。
二:runtime的具体实现
我们写的oc代码,它在运行的时候也是转换成了runtime方式运行的,更好的理解runtime,也能帮我们更深的掌握oc语言。
每一个oc的方法,底层必然有一个与之对应的runtime方法。
-
当我们用OC写下这样一段代码
[tableView cellForRowAtIndexPath:indexPath];
-
在编译时RunTime会将上述代码转化成[发送消息]
objc_msgSend(tableView, @selector(cellForRowAtIndexPath:),indexPath);
三:常见方法
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]);
}
- 获取方法列表
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i<count; i++) {
Method method = methodList[I];
NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}
-
获取成员变量列表
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]); }
-
获取协议列表
__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两个方法
- 获得类方法
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);
- 添加方法
BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
- 替换原方法实现
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
- 交换两个方法
method_exchangeImplementations(oriMethod, cusMethod);
四:常见作用
- 动态的添加对象的成员变量和方法
- 动态交换两个方法的实现
- 拦截并替换方法
- 在方法上增加额外功能
- 实现NSCoding的自动归档和解档
- 实现字典转模型的自动转换
五:代码实现
要使用runtime,要先引入头文件#import <objc/runtime.h>
这些代码的实例有浅入深逐步讲解,最后附上一个我在公司项目中遇到的一个实际问题。
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
这样的方式实现,不管有多少个属性,写这几行代码就搞定了。怎么,还嫌麻烦,下面看看更加简便的方法:两句代码搞定。
我们把encodeWithCoder
和 initWithCoder
这两个方法抽成宏
#import "Movie.h"
#import <objc/runtime.h>
#define encodeRuntime(A) \
\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; 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);\
\
#define initCoderRuntime(A) \
\
if (self = [super init]) {\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; 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;\
\
@implementation Movie
- (void)encodeWithCoder:(NSCoder *)encoder
{
encodeRuntime(Movie)
}
- (id)initWithCoder:(NSCoder *)decoder
{
initCoderRuntime(Movie)
}
@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;
}
}
六.几个参数概念
以上的几种方法应该算是runtime在实际场景中所应用的大部分的情况了,平常的编码中差不多足够用了。
如果从头仔细看到尾,相信你基本的用法应该会了,虽然会用是主要的目的,有几个基本的参数概念还是要了解一下的。
1.objc_msgSend
/* Basic Messaging Primitives
*
* On some architectures, use objc_msgSend_stret for some struct return types.
* On some architectures, use objc_msgSend_fpret for some float return types.
* On some architectures, use objc_msgSend_fp2ret for some float return types.
*
* These functions must be cast to an appropriate function pointer type
* before being called.
*/
这是官方的声明,从这个函数的注释可以看出来了,这是个最基本的用于发送消息的函数。另外,这个函数并不能发送所有类型的消息,只能发送基本的消息。比如,在一些处理器上,我们必须使用objc_msgSend_stret
来发送返回值类型为结构体的消息,使用objc_msgSend_fpret
来发送返回值类型为浮点类型的消息,而又在一些处理器上,还得使用objc_msgSend_fp2ret来发送返回值类型为浮点类型的消息。
最关键的一点:无论何时,要调用objc_msgSend
函数,必须要将函数强制转换成合适的函数指针类型才能调用。
从objc_msgSend
函数的声明来看,它应该是不带返回值的,但是我们在使用中却可以强制转换类型,以便接收返回值。另外,它的参数列表是可以任意多个的,前提也是要强制函数指针类型。
其实编译器会根据情况在objc_msgSend
, objc_msgSend_stret
, objc_msgSendSuper
, 或 objc_msgSendSuper_stret
四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有”Super”
的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”
的函数。
2.SEL
objc_msgSend
函数第二个参数类型为SEL,它是selector在Objc
中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:
typedef struct objc_selector *SEL
;
其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()
或者 Runtime 系统的sel_registerName
函数来获得一个SEL类型的方法选择器。
不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是 Objc 中方法命名有时会带上参数类型(NSNumber一堆抽象工厂方法),Cocoa 中有好多长长的方法哦。
3.id
objc_msgSend
第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:
typedef struct objc_object *id
;
那objc_object又是啥呢:
struct objc_object { Class isa; }
;
objc_object
结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。
PS:isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的isa指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术,详见官方文档.
4.Class
之所以说isa是指针是因为Class其实是一个指向objc_class结构体的指针:
typedef struct objc_class *Class
;
objc_class里面的东西多着呢:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。
在objc_class
结构体中:ivars是objc_ivar_list
指针;methodLists
是指向objc_method_list
指针的指针。也就是说可以动态修改 *methodLists
的值来添加成员方法,这也是Category实现的原理.
/********面试题*******/
<article class="_2rhmJa">
OC中方法的调用 其实都是转化为 objc_msgSend函数的调用
objc_msgSend函数的执行流程可以分为3大阶段
一、消息发送阶段
首先判断消息接收者是否为nil 如果为空直接退出
1、去自己类的方法缓存列表中查找该方法 如果找到则执行该方法,否则执行下一步
2、去自己类的方法列表中查找该方法 如果找到先缓存该方法然后再执行,否则执行下一步
3、去自己父类的方法缓存列表中查找该方法 如果找到先缓存该方法到自己类中,然后执行该方法,否则执行下一步
4、去自己父类的方法列表中查找该方法 如果找到先缓存该方法到自己类中,然后执行该方法,否则 到第二阶段
第一阶段流程图如下所示
image二、动态方法解析阶段
1、首先判断曾经有过动态解析 如果没有则调用 +resolveInstanceMethod:或则+resoveClassMethod:方法来动态解析 然后标记为已经动态解析 最后重新走“消息发送”的流程
2、如果曾经有过动态解析 则直接到第三个阶段 消息转发阶段
第二阶段流程图如下所示:
image三、消息转发阶段
顾名思义就是 自己没有能力处理这个方法 他需要将此方法转交给别人类处理。
1、首先调用 forwardingTargetForSelector:方法 在此方法中:
如果返回值不为空 则直接给返回值转发消息
如果返回值为空否则进入第2步 调用方法签名函数
2、调用methodSignatureForSelector:方法(方法签名函数)在此方法中:
如果返回值为空 则直接调用 调用doesNotRecognizeSelector:方法 然后程序报错:unrecognized selector sent to instance 经典错误
如果返回值不为空 则直接进入第3步 调用 forwardInvocation方法
3、forwardInvocation:方法
开发人员可以在此方法中处理调用的方法
第三阶段流程图如下所示
image举例:
1、首先我们创建一个类(Person 类) 然后在 Person 类中声明一个对象方法(test)且不去实现
Person.h 声明一个test方法
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
- (void)test;
@end
NS_ASSUME_NONNULL_END
2、在其他地方调用 Person 类对象方法(test)
ViewController.m 调用 Person 类对象方法(test)
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
Person *person = [[Person alloc]init];
[person test];
}
如果我们不做任何处理 则程序就会奔溃 报如下错误:
2021-01-19 10:18:44.480279+0800 LLLL[40671:839012] -[Person test]: unrecognized selector sent to instance 0x600000e20000
2021-01-19 10:18:44.485940+0800 LLLL[40671:839012] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person test]: unrecognized selector sent to instance 0x600000e20000'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff23e3de6e __exceptionPreprocess + 350
1 libobjc.A.dylib 0x00007fff512a19b2 objc_exception_throw + 48
2 CoreFoundation 0x00007fff23e5eb94 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
3 CoreFoundation 0x00007fff23e4286c ___forwarding___ + 1436
4 CoreFoundation 0x00007fff23e44b58 _CF_forwarding_prep_0 + 120
5 LLLL 0x0000000102a9ff2e -[ViewController viewDidLoad] + 206
6 UIKitCore 0x00007fff48cc294e -[UIViewController _sendViewDidLoadWithAppearanceProxyObjectTaggingEnabled] + 83
7 UIKitCore 0x00007fff48cc786c -[UIViewController loadViewIfRequired] + 1084
8 UIKitCore 0x00007fff48cc7c89 -[UIViewController view] + 27
9 UIKitCore 0x00007fff493ab2d5 -[UIWindow addRootViewControllerViewIfPossible] + 326
10 UIKitCore 0x00007fff493aa8fe -[UIWindow _updateLayerOrderingAndSetLayerHidden:actionBlock:] + 219
11 UIKitCore 0x00007fff493ab989 -[UIWindow _setHidden:forced:] + 362
12 UIKit 0x0000000102fa4dc4 -[UIWindowAccessibility _orderFrontWithoutMakingKey] + 84
13 UIKitCore 0x00007fff493bedc5 -[UIWindow _mainQueue_makeKeyAndVisible] + 42
14 UIKitCore 0x00007fff495e0cdb -[UIWindowScene _makeKeyAndVisibleIfNeeded] + 202
15 UIKitCore 0x00007fff488cec30 +[UIScene _sceneForFBSScene:create:withSession:connectionOptions:] + 1405
16 UIKitCore 0x00007fff4936eca5 -[UIApplication _connectUISceneFromFBSScene:transitionContext:] + 1019
17 UIKitCore 0x00007fff4936efdc -[UIApplication workspace:didCreateScene:withTransitionContext:completion:] + 291
18 UIKitCore 0x00007fff48ec177c -[UIApplicationSceneClientAgent scene:didInitializeWithEvent:completion:] + 361
19 FrontBoardServices 0x00007fff36d03d2e -[FBSSceneImpl _callOutQueue_agent_didCreateWithTransitionContext:completion:] + 419
20 FrontBoardServices 0x00007fff36d29dc1 __86-[FBSWorkspaceScenesClient sceneID:createWithParameters:transitionContext:completion:]_block_invoke.154 + 102
21 FrontBoardServices 0x00007fff36d0e757 -[FBSWorkspace _calloutQueue_executeCalloutFromSource:withBlock:] + 220
22 FrontBoardServices 0x00007fff36d29a52 __86-[FBSWorkspaceScenesClient sceneID:createWithParameters:transitionContext:completion:]_block_invoke + 355
23 libdispatch.dylib 0x0000000102d0ae8e _dispatch_client_callout + 8
24 libdispatch.dylib 0x0000000102d0dda2 _dispatch_block_invoke_direct + 300
25 FrontBoardServices 0x00007fff36d4f6e9 __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 30
26 FrontBoardServices 0x00007fff36d4f3d7 -[FBSSerialQueue _queue_performNextIfPossible] + 441
27 FrontBoardServices 0x00007fff36d4f8e6 -[FBSSerialQueue _performNextFromRunLoopSource] + 22
28 CoreFoundation 0x00007fff23da1c91 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
29 CoreFoundation 0x00007fff23da1bbc __CFRunLoopDoSource0 + 76
30 CoreFoundation 0x00007fff23da13ec __CFRunLoopDoSources0 + 268
31 CoreFoundation 0x00007fff23d9bf8e __CFRunLoopRun + 974
32 CoreFoundation 0x00007fff23d9b8a4 CFRunLoopRunSpecific + 404
33 GraphicsServices 0x00007fff38c05bbe GSEventRunModal + 139
34 UIKitCore 0x00007fff49372964 UIApplicationMain + 1605
35 LLLL 0x0000000102aa01d2 main + 114
36 libdyld.dylib 0x00007fff5211c1fd start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
接下来使用runtime运行时 来处理这个崩溃
第一阶段:消息发送阶段
此阶段为方法查找阶段 找到了则调用
找不到则 进行第二步 方法动态解析阶段
第二阶段:动态方法解析阶段
在此阶段 我们可以添加一个新的方法来 otherTest 来代替 test
Person.m
#import "Person.h"
#import <objc/runtime.h>
@implementation Person
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(test)) {
Method method = class_getInstanceMethod(self, @selector(otherTest));
class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
return YES;
}
return [super resolveInstanceMethod:sel];
}
- (void)otherTest{
NSLog(@"我是代替方法 ---- %s",__func__);
}
@end
运行程序结果打印如下:
2021-01-19 10:41:28.143085+0800 LLLL[41023:856445] 我是代替方法 ---- -[Person otherTest]
这是第一补救阶段,如果 +resolveInstanceMethod:函数内部不做任何处理或则直接不实现 则进入 第三阶段
第三阶段:消息转发阶段
准备:
首先再创建一个类(Student类) 然后在这个类中创建一个对象方法test 这个对象方法一定要与Person对象中的一样 这样才能找到 方法
Student.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Student : NSObject
- (void)test;
@end
NS_ASSUME_NONNULL_END
Student.m
#import "Student.h"
@implementation Student
- (void)test{
NSLog(@"%s",__func__);
}
@end
实现:
进入第三阶段 首先会调用 forwardingTargetForSelector:方法 在此方法中我们可以把方法转发给别的类来处理
Person.m
#import "Person.h"
#import <objc/runtime.h>
#import "Student.h"
@implementation Person
//动态解析方法阶段
+ (BOOL)resolveInstanceMethod:(SEL)sel{
// //注意⚠️ 如果不注释则无法进行第三阶段
// if (sel == @selector(test)) {
// Method method = class_getInstanceMethod(self, @selector(otherTest));
// class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
// return YES;
// }
return [super resolveInstanceMethod:sel];
}
- (void)otherTest{
NSLog(@"我是代替方法 ---- %s",__func__);
}
//消息转发阶段
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(test)) {
return [[Student alloc]init];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
此时运行代码 控制台打印如下:
2021-01-19 10:59:01.971909+0800 LLLL[41176:868929] -[Student test]
这一步则 完成了方法转发。
如果 forwardingTargetForSelector: 方法中未返回其他类 则会调用以下两个方法
methodSignatureForSelector:aSelector
forwardInvocation:anInvocation
Person.m
#import "Person.h"
#import <objc/runtime.h>
#import "Student.h"
@implementation Person
//动态解析方法阶段
+ (BOOL)resolveInstanceMethod:(SEL)sel{
// //注意⚠️ 如果不注释则无法进行第三阶段
// if (sel == @selector(test)) {
// Method method = class_getInstanceMethod(self, @selector(otherTest));
// class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
// return YES;
// }
return [super resolveInstanceMethod:sel];
}
- (void)otherTest{
NSLog(@"我是代替方法 ---- %s",__func__);
}
//消息转发阶段
- (id)forwardingTargetForSelector:(SEL)aSelector{
// //如果此方法不注释 则不会走下一步
// if (aSelector == @selector(test)) {
// return [[Student alloc]init];
// }
return [super forwardingTargetForSelector:aSelector];
}
/*
方法签名:返回值、返回类型
*/
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(test)) {
/*
v16@0:8
第一个参数 返回值类型 如 v代表 void
第二个参数 16 代表16个字这个节
第三个参数 @ 代表一个对象
第四个参数 0 从第0个字节开始
第五个参数 : 代表SEL
第六个参数 8代表从第八个字节开始
可以简写成: v@:
此处填写请参考:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW1
举例1:
- (BOOL)ifSuccess:(NSString *)tag
其ObjCTypes为:"B@:@",其中:
"B":代表BOOL。 // NSLog(@"%s",@encode(BOOL))的结果为B
"@":一个id类型的对象,第一个参数类型,也就是objc _ msgSend的第一个参数
":":代表对应的SEL,第二个参数
"@":一个id类型的对象,也就是tag。
举例2;
- (void)goToSchoolWithPerson:(Person *)person;
[zhangsan goToSchoolWithPerson:lisi];
其ObjCTypes为: "v@:@" 那究竟是如何得来该字符串呢?其实我们有两种方式:
1、 直接查表。在Type Encodings里面列出了对应关系。 链接如上
2、使用 @encode()计算。(如: NSLog(@"%s",@encode(BOOL))的结果为B )
我们都知道消息发送会被转换成objc _ msgSend(id reciever,SEL sel,prarams1,params2,....)。所以上面的方法会被转换成:
void objc_msgSend(zhangsan,@selector(goToSchoolWithPerson:),lisi); //包含两个隐藏参数
这里的 “v@:@”就代表:
"v":代表返回值void
"@":代表一个对象,这里指代的id类型zhangsan,也就是消息的receiver
":":代表SEL
"@":代表参数lisi
*/
// return [NSMethodSignature signatureWithObjCTypes:"v@:"];
return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
}
return [super methodSignatureForSelector:aSelector];
}
/*
NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
anInvocation.target 方法调用者
anInvocation.selector 方法名
[anInvocation getArgument:NULL atIndex:0]
你可以在这个函数里面干你想干的事
*/
- (void)forwardInvocation:(NSInvocation *)anInvocation{
//1、你可以转发消息
// anInvocation.target = [[MJCat alloc] init];
// [anInvocation invoke];
// 或
// [anInvocation invokeWithTarget:[[Student alloc] init]];
//2、你可以干你想干的事。如:我就想打印
NSLog(@"%s",__func__);
}
@end
由此可知:我们可以在三个地方来实现test方法
第一步:
+resolveInstanceMethod:(对象方法)
+resoveClassMethod:(类方法)
第二步:
forwardingTargetForSelector:
第三步:
methodSignatureForSelector:aSelector
forwardInvocation:anInvocation
面试题 :什么是Runtime?平时项目中有用过么?
1、OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行
2、OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数
3、平时编写的OC代码,底层都是转换成了Runtime API进行调用
具体应用
1、利用关联对象(AssociatedObject)给分类添加属性
2、遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
3、交换方法实现(交换系统的方法)
4、利用消息转发机制解决方法找不到的异常问题
举例1:
1、窥探 系统自带对象的私有属性:如窥探UITextField 的属性
我们可以根据需求找到你想要的东西 然后去重写它等等
UITextField * textField = [[UITextField alloc]init];
// 成员变量的数量
unsigned int count;
Ivar *ivars = class_copyIvarList([UITextField class], &count);
for (int i = 0; i < count; i++) {
// 取出i位置的成员变量
Ivar ivar = ivars[I];
NSLog(@"%s %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
}
free(ivars);
打印结果如下:
2021-01-20 10:06:44.646507+0800 LLLL[51081:1190161] _borderStyle q
2021-01-20 10:06:44.646605+0800 LLLL[51081:1190161] _minimumFontSize d
2021-01-20 10:06:44.646655+0800 LLLL[51081:1190161] _delegate @
2021-01-20 10:06:44.646713+0800 LLLL[51081:1190161] _background @"UIImage"
2021-01-20 10:06:44.646786+0800 LLLL[51081:1190161] _disabledBackground @"UIImage"
2021-01-20 10:06:44.646871+0800 LLLL[51081:1190161] _clearButtonMode q
2021-01-20 10:06:44.646929+0800 LLLL[51081:1190161] _leftView @"UIView"
2021-01-20 10:06:44.647006+0800 LLLL[51081:1190161] _leftViewMode q
2021-01-20 10:06:44.647095+0800 LLLL[51081:1190161] _rightView @"UIView"
2021-01-20 10:06:44.647180+0800 LLLL[51081:1190161] _rightViewMode q
2021-01-20 10:06:44.647274+0800 LLLL[51081:1190161] _contentCoverView @"UIView"
2021-01-20 10:06:44.647478+0800 LLLL[51081:1190161] _contentCoverViewMode q
2021-01-20 10:06:44.647650+0800 LLLL[51081:1190161] _backgroundCoverView @"UIView"
2021-01-20 10:06:44.647801+0800 LLLL[51081:1190161] _backgroundCoverViewMode q
2021-01-20 10:06:44.647988+0800 LLLL[51081:1190161] _traits @"UITextInputTraits"
2021-01-20 10:06:44.648146+0800 LLLL[51081:1190161] _nonAtomTraits @"UITextInputTraits"
2021-01-20 10:06:44.648569+0800 LLLL[51081:1190161] _fullFontSize @"_UIFullFontSize"
2021-01-20 10:06:44.648924+0800 LLLL[51081:1190161] _padding {UIEdgeInsets="top"d"left"d"bottom"d"right"d}
2021-01-20 10:06:44.649360+0800 LLLL[51081:1190161] _progress f
2021-01-20 10:06:44.649453+0800 LLLL[51081:1190161] _clearButton @"_UITextFieldClearButton"
2021-01-20 10:06:44.649528+0800 LLLL[51081:1190161] _clearButtonOffset {CGSize="width"d"height"d}
2021-01-20 10:06:44.649603+0800 LLLL[51081:1190161] _leftViewOffset {CGSize="width"d"height"d}
2021-01-20 10:06:44.649669+0800 LLLL[51081:1190161] _rightViewOffset {CGSize="width"d"height"d}
2021-01-20 10:06:44.665169+0800 LLLL[51081:1190161] _backgroundView @"UITextFieldBorderView"
2021-01-20 10:06:44.665276+0800 LLLL[51081:1190161] _disabledBackgroundView @"UITextFieldBorderView"
2021-01-20 10:06:44.665369+0800 LLLL[51081:1190161] _systemBackgroundView @"UITextFieldBackgroundView"
2021-01-20 10:06:44.665453+0800 LLLL[51081:1190161] _textContentView @"_UITextFieldCanvasView"
2021-01-20 10:06:44.665945+0800 LLLL[51081:1190161] _floatingContentView @"_UIFloatingContentView"
2021-01-20 10:06:44.666024+0800 LLLL[51081:1190161] _contentBackdropView @"UIVisualEffectView"
2021-01-20 10:06:44.666107+0800 LLLL[51081:1190161] _fieldEditorBackgroundView @"_UIDetachedFieldEditorBackgroundView"
2021-01-20 10:06:44.666174+0800 LLLL[51081:1190161] _fieldEditorEffectView @"UIVisualEffectView"
2021-01-20 10:06:44.666241+0800 LLLL[51081:1190161] _placeholderLabel @"UITextFieldLabel"
2021-01-20 10:06:44.666313+0800 LLLL[51081:1190161] _suffixLabel @"UITextFieldLabel"
2021-01-20 10:06:44.666406+0800 LLLL[51081:1190161] _prefixLabel @"UITextFieldLabel"
2021-01-20 10:06:44.666497+0800 LLLL[51081:1190161] _iconView @"UIImageView"
2021-01-20 10:06:44.666576+0800 LLLL[51081:1190161] _label @"UILabel"
2021-01-20 10:06:44.666659+0800 LLLL[51081:1190161] _labelOffset d
2021-01-20 10:06:44.666745+0800 LLLL[51081:1190161] _overriddenPlaceholder @"NSAttributedString"
2021-01-20 10:06:44.666833+0800 LLLL[51081:1190161] _overriddenPlaceholderAlignment q
2021-01-20 10:06:44.666997+0800 LLLL[51081:1190161] _interactionAssistant @"UITextInteractionAssistant"
2021-01-20 10:06:44.667143+0800 LLLL[51081:1190161] _selectGestureRecognizer @"UITapGestureRecognizer"
2021-01-20 10:06:44.667323+0800 LLLL[51081:1190161] _fieldEditor @"UIFieldEditor"
2021-01-20 10:06:44.667865+0800 LLLL[51081:1190161] __textContainer @"NSTextContainer"
2021-01-20 10:06:44.667974+0800 LLLL[51081:1190161] __layoutManager @"_UIFieldEditorLayoutManager"
2021-01-20 10:06:44.668068+0800 LLLL[51081:1190161] _textStorage @"_UICascadingTextStorage"
2021-01-20 10:06:44.668145+0800 LLLL[51081:1190161] _linkTextAttributes @"NSDictionary"
2021-01-20 10:06:44.668221+0800 LLLL[51081:1190161] _pasteController @"UITextPasteController"
2021-01-20 10:06:44.668360+0800 LLLL[51081:1190161] _inputView @"UIView"
2021-01-20 10:06:44.668574+0800 LLLL[51081:1190161] _inputAccessoryView @"UIView"
2021-01-20 10:06:44.668765+0800 LLLL[51081:1190161] _recentsAccessoryView @"UIView"
2021-01-20 10:06:44.669371+0800 LLLL[51081:1190161] _systemInputViewController @"UISystemInputViewController"
2021-01-20 10:06:44.669459+0800 LLLL[51081:1190161] _atomBackgroundView @"UITextFieldAtomBackgroundView"
2021-01-20 10:06:44.669525+0800 LLLL[51081:1190161] _textDragDropSupport @"<UITextDragDropSupport>"
2021-01-20 10:06:44.669603+0800 LLLL[51081:1190161] _textItemDiscoverer @"_UITextItemDiscoverer"
2021-01-20 10:06:44.669681+0800 LLLL[51081:1190161] _textFieldFlags {?="verticallyCenterText"b1"isAnimating"b4"inactiveHasDimAppearance"b1"becomesFirstResponderOnClearButtonTap"b1"clearsPlaceholderOnBeginEditing"b1"adjustsFontSizeToFitWidth"b1"fieldEditorAttached"b1"canBecomeFirstResponder"b1"shouldSuppressShouldBeginEditing"b1"inResignFirstResponder"b1"undoDisabled"b1"explicitAlignment"b1"implementsCustomDrawing"b1"needsClearing"b1"suppressContentChangedNotification"b1"allowsEditingTextAttributes"b1"usesAttributedText"b1"backgroundViewState"b2"clearingBehavior"b2"overridePasscodeStyle"b1"shouldResignWithoutUpdate"b1"blurEnabled"b1"visualEffectViewEnabled"b1"disableFocus"b1"disableRemoteTextEditing"b1"allowsAttachments"b1"isReceivingDrop"b1"contentCoverUnsecuresText"b1"forcesClearButtonHighContrastAppearance"b1"contentInsetsFromFontsValid"b1}
2021-01-20 10:06:44.669868+0800 LLLL[51081:1190161] _deferringBecomeFirstResponder B
2021-01-20 10:06:44.670089+0800 LLLL[51081:1190161] _animateNextHighlightChange B
2021-01-20 10:06:44.670723+0800 LLLL[51081:1190161] _cuiCatalog @"CUICatalog"
2021-01-20 10:06:44.671168+0800 LLLL[51081:1190161] _cuiStyleEffectConfiguration @"CUIStyleEffectConfiguration"
2021-01-20 10:06:44.671270+0800 LLLL[51081:1190161] _roundedRectBackgroundCornerRadius d
2021-01-20 10:06:44.671352+0800 LLLL[51081:1190161] _overriddenAttributesForEditing @"NSArray"
2021-01-20 10:06:44.671407+0800 LLLL[51081:1190161] _adjustsFontForContentSizeCategory B
2021-01-20 10:06:44.671486+0800 LLLL[51081:1190161] _tvUseVibrancy B
2021-01-20 10:06:44.671550+0800 LLLL[51081:1190161] _disableTextColorUpdateOnTraitCollectionChange B
2021-01-20 10:06:44.671659+0800 LLLL[51081:1190161] _pasteDelegate @"<UITextPasteDelegate>"
2021-01-20 10:06:44.671865+0800 LLLL[51081:1190161] _baselineLayoutConstraint @"NSLayoutConstraint"
2021-01-20 10:06:44.672058+0800 LLLL[51081:1190161] _baselineLayoutLabel @"_UIBaselineLayoutStrut"
2021-01-20 10:06:44.672269+0800 LLLL[51081:1190161] _tvCustomTextColor @"UIColor"
2021-01-20 10:06:44.672410+0800 LLLL[51081:1190161] _tvCustomFocusedTextColor @"UIColor"
2021-01-20 10:06:44.672573+0800 LLLL[51081:1190161] _textDragOptions q
2021-01-20 10:06:44.672777+0800 LLLL[51081:1190161] _textDragDelegate @"<UITextDragDelegate>"
2021-01-20 10:06:44.672989+0800 LLLL[51081:1190161] _textDropDelegate @"<UITextDropDelegate>"
2021-01-20 10:06:44.673218+0800 LLLL[51081:1190161] _visualStyle @"_UITextFieldVisualStyle"
举例2、字典转模型
好多第三方字典转模型 都会用到runtime 一般都是便利取出 模型(类)中的属性获取属性名称 然后经过一系列的复杂处理 得到一个字符串,最后经过 KVC来设置属性值 (可自行查看 三方字典转模型的内部实现)
1.讲一下 OC 的消息机制
OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)
objc_msgSend底层有3大阶段
消息发送(当前类、父类中查找)、动态方法解析、消息转发
- 什么是Runtime?平时项目中有用过么?
OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行
OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数
平时编写的OC代码,底层都是转换成了Runtime API进行调用
3.runtime具体应用
利用关联对象(AssociatedObject)给分类添加属性
遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
交换方法实现(交换系统的方法)
利用消息转发机制解决方法找不到的异常问题
网友评论