OC原理-方法缓存机制&OC消息机制

2,242 阅读6分钟

方法的本质

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_tmethod_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;方法后,拿到方法的@selecorimp生成一个bucket_t,再利用位运算@selector(test)&_mask生成一个索引index,按照生成的索引index把生成的bucket_t存放到cache_t中_buckets散列表中。
  • 读取过程: 再次调用-(void)test;方法时,先利用位运算@selector(test)&_mask生成一个索引index,拿索引indexcache_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,说明这个方法还没有被缓存过,就需要查找方法,然后进行缓存。
  • 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:将消息转发给能处理消息的对象
  • methodSignatureForSelectorforwardInvocation:第一个方法生成方法签名NSMethodSignature对象,然后创建anInvocation对象座位参数传给第二个方法,然后在第二个方法中可以尽情的处理,只要在第二个方法里面不执行父类的方法,即使不处理也不会崩溃.