方法的本质
在OC原理--对象、类的本质我们看到类和分类的所有方法存储在class_rw_ext_t->methods的二维数组中。
struct class_rw_ext_t {
const class_ro_t *ro;
method_array_t methods; //二维的方法数组
property_array_t properties; //二维的属性数组
protocol_array_t protocols; //二维的协议数组
char *demangledName;
uint32_t version;
};
method_array_t中存储这一个个的method_list_t,method_list_t中又存储这一个个的method_t。
//method_t就是方法的本质
struct method_t {
SEL name; //函数名 也可以叫选择器
const char *types; //编码(返回值类型、参数类型)
MethodListIMP imp; //函数地址
};
不同类中相同名字的方法,所对应的方法选择器是相同的 types生成规则如下
iOS中提供了一个叫@encode的指令,可以将具体的类型转化成字符串编码
编码 意义
c ---> A char
i ---> An int
s ---> A short
l ---> A longl is treated as a 32-bit quantity on 64-bit programs.
q ---> A long long
C ---> An unsigned char
I ---> An unsigned int
S ---> An unsigned short
L ---> An unsigned long
Q ---> An unsigned long long
f ---> A float
d ---> A double
B ---> A C++ bool or a C99 _Bool
v ---> A void
* ---> A character string (char *)
@ ---> An object (whether statically typed or typed id)
# ---> A class object (Class)
: ---> A method selector (SEL)
[array type] ---> An array
{name=type...} ---> A structure
(name=type...) ---> A union
bnum ---> A bit field of num bits
^type ---> A pointer to type
? ---> An unknown type (among other things, this code is used for function pointers)
举个🌰
//types 是 v16@0:8 v:返回类型viod 16:全部参数占用16字节 @:函数的第一个参数是self,类型是id 0:第一个参数从0开始 :标示第二个参数是_cmd,SEL类型 8:第二个参数从8开始
-(void)test;
方法缓存机制
调用方法,OC中方法查找的过程是比较曲折的,先根据isa指针找到Class对象或者Meta-Class对象,查找无果后,在根据Class对象或者Meta-Class对象的superclass指针找到其父类,在父类中接着查找,如果还没有找到,接着查找父类。。。。。
这么辛苦才找到的方法,假如我没过一会又调用一次,再来一遍!!!岂不是要疯。。。
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // 方法缓存
class_data_bits_t bits; // 用户获取类的具体信息
class_rw_t *data() const {
return bits.data();
}
}
所以OC有个方法缓存机制:我们看到Class内部结构中有个方法cache(cache_t类型),这个就是用来做方法缓存的。cache用散列表
来缓存曾经调用过的方法,可以提高方法查找速度。
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets; //方法会缓存到这里 散列表相当于数组 存储着一个个的bucket_t
explicit_atomic<mask_t> _mask; //散列表的长度-1
}
struct bucket_t {
explicit_atomic<uintptr_t> _imp; //函数地址
explicit_atomic<SEL> _sel; //函数名
}
散列表可以提高查找速度,其实就是一种空间换时间
的手段。
大致流程如下:
- 存放过程:
调用
-(void)test;
方法后,拿到方法的@selecor
和imp
生成一个bucket_t
,再利用位运算@selector(test)&_mask
生成一个索引index
,按照生成的索引index
把生成的bucket_t
存放到cache_t中_buckets散列表
中。 - 读取过程:
再次调用
-(void)test;
方法时,先利用位运算@selector(test)&_mask
生成一个索引index
,拿索引index
去cache_t中_buckets散列表
取到bucket_t
,然后直接拿到函数地址IMP
调用方法。
直接拿索引值取值,肯定比挨个遍历要效率高,但是位运算生成的索引值不会依次增加,会导致散列表中有的空间没有被利用,所以说空间换时间
。缓存完的散列表有可能张这个样子。
- Q1:创建的散列表的空间被用完咋办?
- A1:一开始会创建一个空间为4的散列表,当存储方法量达到当前散列表的3/4时,就会重新创建一个新的散列表,存储空间是原来的2倍,原来的散列表会被清除掉,方法也得重新缓存。
- Q2: @selector(XXX)&_mask?
- A2:这样生成的index<=_mask。
- Q3:
@selector(XXX)&_mask
算出来的索引index,去存储发现里面已经被别的方法生成的bucket_t
占用了咋办?- A3:生成新的索引,规则是
index = index?index-1:mask
,就是索引不为0:新的索引就是index-1,索引为0:新的索引就是散列表的长度-1。如果新生成的索引还是被占用,那就递归再回去新的索引。所以取到的缓存有可能不是本方法生成的缓存,递归生成新索引再去获取,直到取到本方法的缓存,当然当拿着一开始的索引取到的值为nil,说明这个方法还没有被缓存过,就需要查找方法,然后进行缓存。
- A3:生成新的索引,规则是
- Q4:调用父类的方法,方法会被缓存到哪里?类方法又会被缓存到哪里?
- Q4:方法会被缓存进方法调用者的类对象中或者元类对象中,区分是实例方法缓存到类对象中,类方法缓存到元类对象中。
消息机制
OC中的方法调用,都会转化成objc_msgSend函数调用,就是给方法调用者发送消息,所以方法调用者又称为消息接收者(receiver),方法名称又称消息名称。
objc_msgSend执行流程又可以分为3大阶段
- 消息发送
- 动态方法解析
- 消息转发
1.消息发送流程
receiver就是方法调用者。receiver通过isa指针找到receiverClass,receiverClass通过superclass指针找到superClass
2.动态方法解析
-(void)other{
NSLog(@"other");
}
//实例方法动态解析
+(BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(test)) {
//动态添加 动态给test方法添加一个方法实现 这个方法会被添加到类对象的class_rw_t的方法列表中
Method method = class_getInstanceMethod(self, @selector(other));
//这里是的self是类对象 注意这里西部给类对象添加方法
class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
return YES;
}
return [super resolveInstanceMethod:sel];
}
//类方法动态解析
+(BOOL)resolveClassMethod:(SEL)sel{
//动态添加
if (sel == @selector(test)) {
//动态添加 动态给test方法添加一个方法实现 这个方法会被添加到元类对象的class_rw_t的方法列表中
Method method = class_getInstanceMethod(self, @selector(other));
// 这里是的self是类对象 获取到元类对象 这里一定要给元类对象添加方法
class_addMethod(object_getClass(self), sel, method_getImplementation(method), method_getTypeEncoding(method));
return YES;
}
return [super resolveClassMethod:sel];
}
注意:我们看到实例方法和类方法的动态方法解析最后都可以去执行-(void)other{}方法,说明实例方法和类方法本质是一样的,+ - 只是OC的语法,目的是把方法存到类对象或者实例对象,只要拿到函数指针就可以直接调用,不用在乎方法存放在哪里。
动态方法解析完,会
重走消息发送
的流程,从在receiverClass的cache中查找方法
这一步开始。所以实例方法的动态解析必须把方法添加到class对象
,类方法的动态解析必须把方法添加到meta-class对象
,否者动态解析就会失败。
3.消息转发:将消息转发给别人
以上转发使用的方法都有对象方法、类方法2个版本。
@implementation Cat
//Cat类中实现test方法
-(void)test:(int)age{
NSLog(@"cat-test");
}
@end
//消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(test:)) {
return [Cat new]; //将test消息转发给Cat对象
}
return [super forwardingTargetForSelector:aSelector];
}
//这里也侧面说明Person类中的@selector(test:)和Cat类中的@selector(test:)相同
//返回方法签名 包括:返回值类型、参数类型
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(test:)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:i"];
}
return [super methodSignatureForSelector:aSelector];
}
//anInvocation封装了一个方法调用 包括:方法调用者,方法名,方法参数
-(void)forwardInvocation:(NSInvocation *)anInvocation{
//尽情处理一:也可以什么也不做 不调用父类这样程序也不会奔溃
NSLog(@"fdf");
//尽情处理二:把消息转发给别动对象 修改方法调用者
if (anInvocation.selector == @selector(test:)) {
//anInvocation 一开始的方法调用者是person对象 这里可以改变方法调用者为cat对象,再去执行方法 Cat类中要有实现test方法
//写法1
// anInvocation.target = [[Cat alloc] init];
// [anInvocation invoke];
//写法2
// [anInvocation invokeWithTarget:[[Cat alloc] init]];
}
[super forwardInvocation:anInvocation];
//尽情处理三:修改方法名,
anInvocation.selector = @selector(XXX);
[anInvocation invoke];
//尽情处理四:修改参数
// if (anInvocation.selector == @selector(test:)) {
// int age;
//这里为啥是2 因为前面还有2个参数self和_cmd
// [anInvocation getArgument:&age atIndex:2];
// NSLog(@"%d",age);
// NString *name = @"zht";
// [anInvocation setArgument:&name atIndex:2];
// }
}
forwardingTargetForSelector
:将消息转发给能处理消息的对象methodSignatureForSelector
和forwardInvocation
:第一个方法生成方法签名NSMethodSignature
对象,然后创建anInvocation
对象座位参数传给第二个方法,然后在第二个方法中可以尽情的处理
,只要在第二个方法里面不执行父类的方法,即使不处理也不会崩溃.