Objective-C类的定义和Runtime机制

2016-03-24  本文已影响103人  月咏蝴蝶

之前看各种Runtime机制的详解等一系列诸如此类的文章,对文章中各种术语都一知半解,这几天看了Block原理的详解,又遇到类似相关的知识,要深入理解Runtime机制,必须得从Objective-C类的定义理解开始。

此文章大部分内容来源于以下两个博客,本人仅记录个人所需要的知识点。
Objective-C Runtime
Objective-C isa 指针 与 runtime 机制
Objective-C Runtime 运行时之四:Method Swizzling

1. Objective-C对类的定义

Xcode中objc.h中对类的定义
typedef struct objc_selector *SEL;

方法选择器,可以理解为区分方法的ID,此ID的数据结构是SEL
方法的selector用于表示运行时方法的名字。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL
两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么方法的SEL就是一样的。每一个方法都对应着一个SEL。所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。

typedef struct objc_object *id;

id是一个 objc_object 结构类型的指针,是一个指向类实例的指针
它的存在可以让我们实现类似于C++中泛型的一些操作。该类型的对象可以转换为任何一种对象,有点类似于C语言中void *指针类型的作用。

struct objc_object { Class isa OBJC_ISA_AVAILABILITY; };

objc_object 结构体包含一个isa指针,可以通过这个指针找到对象所属的类
当我们向一个Objective-C对象发送消息时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类的方法列表及父类的方法列表中去寻找与消息对应的selector指向的方法。找到后即运行这个方法。

typedef struct objc_class *Class;

(本质上是Class 是一个指向objc_class结构体的指针,于是我们就可以在这个结构体里面找到相应的信息)

objc_class结构体的定义
struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            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;
}

可以理解为objc_ivar_list结构体存储着objc_ivar数组列表(变量数组),而objc_ivar结构体存储了类的单个成员变量的信息;同理objc_method_list结构体存储着objc_method数组列表(方法数组),而objc_method结构体存储了类的某个方法的信息。

typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
// 方法名类型为SEL,相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
// 方法类型method_types是个char指针,其实存储着方法的参数类型和返回值类型。
// method_imp指向了方法的实现,本质上是一个函数指针。
typedef struct objc_ivar *Ivar;
struct objc_ivar {
    char *ivar_name           变量名字  OBJC2_UNAVAILABLE;
    char *ivar_type           变量类型  OBJC2_UNAVAILABLE;
    int ivar_offset                    OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                          OBJC2_UNAVAILABLE;
#endif
}                                      OBJC2_UNAVAILABLE;
typedef id (*IMP)(id, SEL, ...);

它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。
你会发现IMP指向的方法与objc_msgSend函数类型相同,参数都包含id和SEL类型。每个方法名都对应一个SEL类型的方法选择器,而每个实例对象中的SEL对应的方法实现肯定是唯一的,通过一组id和SEL参数就能确定唯一的方法实现地址;反之亦然。

2. Runtime机制

当看懂第一部分类的定义之后,接下来Runtime机制都是在该基础上进行操作。

Runtime的应用:
1.动态创建一个类(比如KVO的底层实现)
2.动态地为某个类添加属性\方法, 修改属性值\方法
3.遍历一个类的所有成员变量(属性)\所有方法
实质上,以上的是通过相关方法来获取对象或者类的isa指针来实现的。

相关函数

  1. 增加
    增加函数:class_addMethod
    增加实例变量:class_addIvar
    增加属性:@dynamic标签,或者class_addMethod,因为属性其实就是由getter和setter函数组成
    增加Protocol:class_addProtocol
  2. 获取
    获取函数列表及每个函数的信息(函数指针、函数名等等):class_getClassMethod method_getName ...
    获取属性列表及每个属性的信息:class_copyPropertyList property_getName
    获取类本身的信息,如类名等:class_getName class_getInstanceSize
    获取变量列表及变量信息:class_copyIvarList
    获取变量的值
  3. 替换
    将实例替换成另一个类:object_setClass
    替换类方法的定义:class_replaceMethod
  4. 其他常用方法:
    交换两个方法的实现:method_exchangeImplementations.
    设置一个方法的实现:method_setImplementation.

举例获取所有方法名

    u_int count;
//  获取一个类的所有方法名
    Method *methods = class_copyMethodList([self class], &count);
    for (int i = 0; i < count; i++) {
        SEL name = method_getName(methods[i]);
        NSString *strName = [NSString stringWithCString:sel_getName(name) encoding:NSUTF8StringEncoding];
        NSLog(@"方法名:%@", strName);
    }
获取所有方法名例子结果

举例获取该类某一成员变量类型和变量名字

    NSLog(@"选择的变量类名是什么:%@", [self nameWithInstance:self.color]);

- (NSString *)nameWithInstance:(id)instance {
    unsigned int numIvars = 0;
    NSString *key=nil;
    Ivar * ivars = class_copyIvarList([self class], &numIvars);
    for(int i = 0; i < numIvars; i++) {
        Ivar thisIvar = ivars[i];
        const char *type = ivar_getTypeEncoding(thisIvar);
        NSString *stringType =  [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
        NSLog(@"方法类型:%@", stringType);
        if (![stringType hasPrefix:@"@"]) {
            continue;
        }
        if ((object_getIvar(self, thisIvar) == instance)) {//此处若 crash 不要慌!
            key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
            break;
        }
    }
    free(ivars);
    return key;
}
举例获取该类某一成员变量类型和变量名字

举例修改方法的实现(替换方法的实现)
这里我们用UIViewController的viewWillAppear方法举例
首先新建一个UIViewController的Category

#import <objc/runtime.h>
#import "UIViewController+Tracking.h"

@implementation UIViewController (Tracking)

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 获取当前class
        Class class = [self class];
//        Class class = object_getClass((id)self);
        // 获取SEL
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(swizzl_viewWillAppear:);
        
        // 获取Method
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // 是否添加方法
        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        // 这里如果originalSelector已经被实现了的话,则返回NO,如果还未被实现,则添加方法(swizzled的IMP)
        if (didAddMethod) {
            // 返回YES,说明已经把originalSelector和swizzledIMP绑定在一起,现在只需要把swizzledSelector和originalIMP绑定在一起,就实现了交换
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        }
        else{
            // 这里返回NO,说定originalIMP已经被实现,这里需要交换IMP
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling
- (void)swizzl_viewWillAppear:(BOOL)animated{
    // 以下这个方法已经制定给UIViewController的viewWillAppear方法,不会产生无限循环
    // 如果这里调用[self viewWillAppear]的话,则会陷入无限循环
    [self swizzl_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}

@end
2016-04-08 14:09:26.977 WebSocket[2060:124911] viewWillAppear: UINavigationController
2016-04-08 14:09:28.551 WebSocket[2060:124911] viewWillAppear: WSViewController
2016-04-08 14:09:31.623 WebSocket[2060:124911] viewWillAppear: UIInputWindowController

代码的注释可以帮助了解这个过程,在这里还需要提一下的是:
Swizzling应该总是在+load中执行
在Objective-C中,运行时会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用
Swizzling应该总是在dispatch_once中执行
因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施

上一篇下一篇

猜你喜欢

热点阅读