包罗万象的runtime(三):Method

个人认为,method是runtime中最重要的一部分了吧,方法的定义很简单:返回值+方法名+入参+方法体。当我们调用一个方法的时候,到底是经过哪些步骤呢,当遇到同名函数的时候,调用的究竟是哪个方法?runtime的Swizzling黑魔法是怎么做到替换系统方法的呢?

ok,带着这些疑问,开始我们的旅程吧!

1. 先来看下runtime里面关于method的定义吧

struct objc_method {
    SEL method_name     //方法id                                    OBJC2_UNAVAILABLE;
    char *method_types  //各参数和返回值类型的typeEncode                                     OBJC2_UNAVAILABLE;
    IMP method_imp      //方法实现                                   OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}
1.1 先看一下objc_method_list

看源码:objc_method_list 顾名思义,就是对象的方法列表了,看下结构体里面的内容:

  • objc_method_list *obsolete:不明白,先不管
  • method_count :方法数量
  • space:继续跳过先
  • objc_method method_list[1]:这个我感觉应该是指向方法列表的第一个方法,看这样式,方法列表应该是个链式存储结构,搜一下有没有next方法什么的:
    class_nextMethodList(Class _Nullable, void * _Nullable * _Nullable)
    果然搜到了,那应该是链式存储无疑了哈(要是我错了一定要告诉我)
1.2 继续看objc_method结构体

objc_method里面有三个参数,我们来看下

  • method_name
    method_name:一看就明白啦,方法名,但是SEL是什么,看起来很面熟对不对,想到一个方法:performSelector:(SEL)aSelector withObject...,我们这样调用[self performSelector:@selector(report:) withObject:nil afterDelay:2];,所以很明显SEL=@selector

SEL 类成员方法的指针,可以理解 @selector()就是取类方法的编号,他的行为基本可以等同C语言中的函数指针,只不过C语言中,可以把函数名直接赋给一个函数指针,而Object-C的类不能直接应用函数指针,这样只能做一个@selector语法来取。它的结果是一个SEL类型。这个类型本质是类方法的编号

精简一下:SEL是类成员方法的指针,本质是类方法的编号。

  • method_types
    各参数和返回值类型的typeEncode
  • IMP method_imp
    方法实现
    题外话:imp忽然想到了《权力的游戏》里面的小恶魔,超喜欢他!
    言归正传,这个IMP也老重要了,基本面试必问的有没有!

IMP是Implement缩写,IMP 是一个函数指针,这个被指向的函数包含一个接收消息的对象id(self 指针), 调用方法的选标 SEL (方法名),以及不定个数的方法参数,并返回一个id。也就是说 IMP 是消息最终调用的执行代码,是方法真正的实现代码 。我们可以像在C语言里面一样使用这个函数指针。

总之IMP是一个函数指针,保存了方法的地址,是方法的真正实现

下面来写个例子看一下方法列表吧
show the code

/// 定义一个Person类
@interface Person : NSObject
{
    NSString * _address;
    NSString * _idNo;
}

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *sex;
@property (atomic, assign) int age;

- (instancetype)initWithID:(NSString *)idNo address:(NSString *)address;

- (void)instanceDescription;

+ (void)staticDescription;

@end

/// 定义一个关于方法操作的方法
- (void)methodOperation:(id)obj {
    NSLog(@"%s的方法操作:",object_getClassName(obj));
    Class cls = object_getClass(obj);
    unsigned int outCount = 0;
    /// 获取方法列表
    Method *methods = class_copyMethodList(cls, &outCount);
    for (int i = 0; i < outCount; i++) {
        Method method = methods[i];
        SEL sel = method_getName(method);
        NSLog(@"%@",NSStringFromSelector(sel));
    }
    free(methods);
}
///调用
Person *person = [[Person alloc] initWithID:@"3715251993098767567" address:@"山东聊城冠县"];
person.name = @"Elaine";
person.sex = @"F";
person.age = 25;
[self methodOperation:person];
/// 输出结果
2018-06-03 04:33:31.876527+0800 RuntimeDemo[2124:294878] Person的方法操作:
2018-06-03 04:33:31.876875+0800 RuntimeDemo[2124:294878] initWithID:address:
2018-06-03 04:33:31.877025+0800 RuntimeDemo[2124:294878] setSex:
2018-06-03 04:33:31.877136+0800 RuntimeDemo[2124:294878] instanceDescription
2018-06-03 04:33:31.877317+0800 RuntimeDemo[2124:294878] sex
2018-06-03 04:33:31.877472+0800 RuntimeDemo[2124:294878] .cxx_destruct
2018-06-03 04:33:31.877615+0800 RuntimeDemo[2124:294878] name
2018-06-03 04:33:31.878932+0800 RuntimeDemo[2124:294878] setName:
2018-06-03 04:33:31.879089+0800 RuntimeDemo[2124:294878] setAge:
2018-06-03 04:33:31.879254+0800 RuntimeDemo[2124:294878] age

SEL 和 IMP的关系

每一个继承于NSObject的类都能自动获得runtime的支持。在这样的一个类中,有一个isa指针,指向该类定义的数据结构体,这个结构体是由编译器编译时为类(需继承于NSObject)创建的.在这个结构体中有包括了指向其父类类定义的指针以及 Dispatch table. Dispatch table是一张SEL和IMP的对应表。

2. runtime的黑魔法

我们用一个例子来说明一下吧,我们经常使用UITableView,UITableView会显示空的,比如网不好或者本来就没有数据什么的,这种情况我们一般都会显示一张空界面,当然可以在调用reload之后加个判断,但是UITableView使用超级频繁,这样加起来太麻烦了,有童鞋说我们可以自己写个新的newReload方法。确实,这比加判断简单多了,不过我们一般都是多人开发,你加的这个方法你知道如何调用,其他人呢,或者新来的员工呢?所以这时候就用到我们的黑魔法了!!!
代码实现:

@implementation UITableView (Runtime)

+(void)load{
    Class class = [self class];
    SEL originalSelector = @selector(reloadData);
    SEL swizzledSelector = @selector(swizzle_reloadData);
    /// 获取我们自定义的swizzle_reloadData方法
    Method swizzledMethod = class_getInstanceMethod (class, swizzledSelector);
    /// 获取系统的reloadData方法
    Method originalMethod = class_getInstanceMethod (class, originalSelector);
    /// 交换两个方法的实现
    method_exchangeImplementations(originalMethod, originalMethod);
}

-(void)swizzle_reloadData {
    //执行系统的reloadData方法
    [self swizzle_reloadData];
    if (self.visibleCells.count == 0) {
        NSLog(@"处理空table");
    }
}
@end

这样当我们调用系统的reloadData方法时,实际调用的是swizzle_reloadData的实现,当然在swizzle_reloadData里面调用swizzle_reloadData实际上是调用的reloadData的实现,运行一下,没有问题,perfect! really?


再来看另外一种情况,这次不在Category里面实现了,定义一个UITableView的子类MyTableView,在MyTableView里面写入上面的两个方法,调用一下试试,发现并没有执行我们的swizzle_reloadData方法,这是因为MyTableView并没有重写reloadData方法,实际上是调用父类的reloadData,ok,来看下最完整的写法吧

+ (void)load {
    static dispatch_once_t once_Token;
    /// dispatch_once这里不是“单例”,是保证方法替换只执行一次.
    dispatch_once(&once_Token, ^ {
        Class class = [self class];
        SEL originalSelector = @selector(reloadData);
        SEL swizzledSelector = @selector(swizzle_reloadData);
        /// 获取我们自定义的swizzle_reloadData方法
        Method swizzledMethod = class_getInstanceMethod (class, swizzledSelector);
        /// 获取系统的reloadData方法
        Method originalMethod = class_getInstanceMethod (class, originalSelector);
        /// 交换两个方法的实现
        method_exchangeImplementations(originalMethod, originalMethod);
        
        /*
         当 class_addMethod 返回 NO 时,说明主类本身就实现了需要被替换的方法,
         * 这种情况比较简单,我们直接交换两个方法的实现就可以了。
         * 当 class_addMethod 返回 YES 时,说明主类本身没有实现需要被替换的方法,而是继承了父类的实现。
         * 这时 class_getInstanceMethod 函数获取到的 originalSelector 指向的就是父类的方法。
         * 然后我们再通过 class_replaceMethod 把父类的实现替换到我们自定义的 swizzle_reloadData 中,
         * 这样就达到了在 swizzle_reloadData 方法中调用父类实现的目的。
         */
        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            /// 交换两个方法的实现
            method_exchangeImplementations(originalMethod, originalMethod);
        }
        
    });
}


-(void)swizzle_reloadData {
    //执行系统的reloadData方法
    [self swizzle_reloadData];
    if (self.visibleCells.count == 0) {
        NSLog(@"处理空table");
    }
}
/// 运行一下
2018-06-03 10:42:16.801534+0800 RuntimeDemo[1813:185046] 处理空table

这次没问题了

为什么runtime可以在运行时更改方法的实现?
其实这块我是不怎么明白的,我的理解是在编译期把方法编译成了runtime中的obj_send()方法,绑定了方法编号SEL,在运行期,通过方法编号在对应的table里面查找方法实现IMP,所以我们能通过修改table里面的对应关系来实现动态更改方法实现,水平有限,自己确实不太明白,希望各位能够跟我讲明白一点,不胜感激!!


附上method相关操作方法:

//判断类中是否包含某个方法的实现
  BOOL class_respondsToSelector(Class cls, SEL sel)
  //获取类中的方法列表
  Method *class_copyMethodList(Class cls, unsigned int *outCount) 
  //为类添加新的方法,如果方法该方法已存在则返回NO
  BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
  //替换类中已有方法的实现,如果该方法不存在添加该方法
  IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types) 
  //获取类中的某个实例方法(减号方法)
  Method class_getInstanceMethod(Class cls, SEL name)
  //获取类中的某个类方法(加号方法)
  Method class_getClassMethod(Class cls, SEL name)
  //获取类中的方法实现
  IMP class_getMethodImplementation(Class cls, SEL name)
  //获取类中的方法的实现,该方法的返回值类型为struct
  IMP class_getMethodImplementation_stret(Class cls, SEL name) 

  //获取Method中的SEL
  SEL method_getName(Method m) 
  //获取Method中的IMP
  IMP method_getImplementation(Method m)
  //获取方法的Type字符串(包含参数类型和返回值类型)
  const char *method_getTypeEncoding(Method m) 
  //获取参数个数
  unsigned int method_getNumberOfArguments(Method m)
  //获取返回值类型字符串
  char *method_copyReturnType(Method m)
  //获取方法中第n个参数的Type
  char *method_copyArgumentType(Method m, unsigned int index)
  //获取Method的描述
  struct objc_method_description *method_getDescription(Method m)
  //设置Method的IMP
  IMP method_setImplementation(Method m, IMP imp) 
  //替换Method
  void method_exchangeImplementations(Method m1, Method m2)

  //获取SEL的名称
  const char *sel_getName(SEL sel)
  //注册一个SEL
  SEL sel_registerName(const char *str)
  //判断两个SEL对象是否相同
  BOOL sel_isEqual(SEL lhs, SEL rhs) 

  //通过块创建函数指针,block的形式为^ReturnType(id self,参数,...)
  IMP imp_implementationWithBlock(id block)
  //获取IMP中的block
  id imp_getBlock(IMP anImp)
  //移出IMP中的block
  BOOL imp_removeBlock(IMP anImp)

  //调用target对象的sel方法
  id objc_msgSend(id target, SEL sel, 参数列表...)

参考链接:(排名不分先后)
https://www.jianshu.com/p/23836159aa49
https://www.jianshu.com/p/eac6ed137e06
https://blog.csdn.net/qq_30513483/article/details/52326035
https://www.jianshu.com/p/eac6ed137e06
https://www.jianshu.com/p/4a09d5ebdc2c

你可能感兴趣的:(包罗万象的runtime(三):Method)