iOS的方法

292 阅读5分钟

类的结构

类分成了三部分

与我们调用方法相关的东西有三个,

  • objc_calsscache_t cache 方法缓存
  • class_rw_tmethod_array_t methods 二维数组的方法列表
  • class_ro_tmethod_list_t 一维数组的方法列表

cache_t cache

objc_calsscache_t cache ,用散列表(哈希表)数组来缓存曾经调用过的方法,提高查找方法的速度。

cache_t 里面就三个成员,后两个是int类型,肯定不是存储的方法了,那就只有存储 _buckets 这个散列表中。
bucket_t 就两个值,一个key一个imp,key的话就是SEL 方法名,而imp就是方法的内存地址。调用一个方法,到缓存里来找的时候,通过匹配比对方法名是不是一致。

找方法的具体实现,可以查看 runtime 源码中的,objc-cache.mm 文件。

method_array_t methods

class_rw_tmethod_array_t methods ,包含原始类和分类的方法的二维数组,可读可写,动态添加方法的时候,是对这个二维数组进行的操作。

method_list_t

class_ro_tmethod_list_t ,是只包含原始类的方法的数组,不包含分类的方法。只读不可写。

method_t

方法的数据结构如下

struct method_t {
    SEL name;
    const char *types;
    MethodListIMP imp;
}
  • name 方法的名字,可以叫做选择器。可以通过 @selector()sel_registerName() 获得。
  • types 方法的编码,即返回值、参数的类型。由:返回值 参数1 参数2...参数N,格式编码拼接而成的字符串。@encode 指令可以获取类型的编码字符。
  • imp 方法的具体实现,是函数指针(函数的地址)

IMP定义

第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector)

方法的调用

方法的调用顺序:系统是先从方法缓存中找有没有对应的方法,有的话就直接调用缓存里的方法,根据imp去调用方法,没有的话,就再去方法数组中遍历查找,找到后调用并保存到方法缓存里。

当我们调用一个方法是,其实底层是将这个方法转换成了 objc_msgSend 函数来进行调用,objc_msgSend 的执行流程可以分为3大阶段:

  • 消息的发送
  • 动态的方法解析
  • 消息转发

消息的发送流程

当方法经过图中的一系列寻找工作后,如果还没找到方法,则会进入到动态方法解析

动态的方法解析

主要涉及到了两个API

+resolveInstanceMethod://添加对象方法  也就是-开头的方法
+resolveClassMethod://添加类方法  也就是+开头的方法

当我们需要动态方法解析式,需要实现这两个方法,返回 YES

通过 class_getInstanceMethod 获取到一个 Method 的实现

通过 class_addMethod 把方法添加到类对象或者元类对象 class_rw_t中。

说明 : 动态添加方法,实质是找到某个方法的实现 imp,添加到类对象或者元类对象中,这个方法的是什么性质(类方法或者对象方法)无关紧要,重要的是 class_addMethod 的时候,希望这个方法成为对象方法,则添加到类对象的 class_rw_t 的方法中;希望它成为类方法,则添加到元类对象的 class_rw_t 的方法中。

消息转发

如果方法在消息发送阶段没有找到相关方法,也没有进行动态方法解析,这个时候就会走到消息转发阶段了。
所谓消息转发,就是你这个消息处理不了后可以找其他人或者其他方法来代替。

可以看到,转发的方式有两种,通过 forwardingTargetForSelector 快速转发。或者通过 methodSignatureForSelectorforwardInvocation 进行标准转发。

  • 通过 forwardingTargetForSelector 快速转发简单方便,缺点就是能做的事情有限,只能转发消息调用者
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        //return [[SomeClass alloc] init];
        return [[NSObject alloc] init];
    }
    return  [super forwardingTargetForSelector:aSelector];
}
  • 通过 methodSignatureForSelectorforwardInvocation 进行标准转发,写起来麻烦点,需要写方法签名等信息,但是可以很大程度的自定义方法的转发,可以在找不到方法imp的时候做任何逻辑。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        //注意:这里返回的是我们要转发的方法的签名,比如我们现在是转发test方法 那就是返回的就是test方法的签名
        
        //1.可以使用methodSignatureForSelector:方法从实例中请求实例方法签名,或者从类中请求类方法签名。
        //2.也可以使用instanceMethodSignatureForSelector:方法从一个类中获取实例方法签名
        //这里使用self的话会进入死循环 所以不可以使用 如果其他方法中有同名方法可以将self换成其他类
        //return [self methodSignatureForSelector:aSelector];
        //return [NSMethodSignature instanceMethodSignatureForSelector:aSelector];
        //3.直接输入字符串
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
     return [super methodSignatureForSelector:aSelector];
}

//当返回方法签名后 就会转发到这个方法  所以我们可以在这里做想要实现的功能  可操作空间很大
//这个anInvocation里面有转发方法的信息,比如方法调用者/SEL/types/参数等等信息
- (void)forwardInvocation:(NSInvocation *)anInvocation{
//    SomeClass *obj = [[SomeClass alloc] init];
    //指定target
//    anInvocation.target = obj;
    //对anInvocation做出修改后要执行invoke方法保存修改
//    [anInvocation invoke];
    
    //或者干脆一行代码搞定
//    [anInvocation invokeWithTarget:[[SomeClass alloc] init]];
    
    //转发到这里的话可操作性更大  也可以什么都不写
    //相当于转发到的这个方法是个空方法  也不会报方法找不到的错误
    //也可以在这里将报错信息提交给后台统计 
    //比如说某个方法找不到提交给后台 方便线上错误收集..等等
}

整体的流程图

方法交换

method swizzling,开发中偶尔会用的,将自定义方法替换掉系统的方法,或者将两个方法交换。

  • 利用 class_replaceMethod 替换方法的实现
  • 利用 method_setImplementation 来直接设置某个方法的IMP
  • 利用 method_exchangeImplementations 交换两个方法的实现,交换的是 Method 里的 imp