美文网首页
Objective-C类的定义和Runtime机制

Objective-C类的定义和Runtime机制

作者: 月咏蝴蝶 | 来源:发表于2016-03-24 16:40 被阅读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结构体的定义
    • objc_class里面也有一个isa指针,这个指针是指向meteClass(元类)
      元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。元类也是类,也是对象,最终指向根元类(root meteClass),根元类isa指针指向自己,形成闭环。
    • Class super_class 父类,如果是最顶层的根类它为NULL。
    • const char *name 类名
    • long version/info 版本信息,默认是0/标识
    • long instance_size 该类实例变量大小
    • struct objc_ivar_list *ivars 和 struct objc_method_list **methodLists
    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结构体存储了类的某个方法的信息。

    • struct objc_cache *cache 存储被调用过的方法,提高效率
    • struct objc_protocol_list *protocols 指向该类的协议
    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会改变全局状态,所以我们需要在运行时采取一些预防措施

    相关文章

      网友评论

          本文标题:Objective-C类的定义和Runtime机制

          本文链接:https://www.haomeiwen.com/subject/wkerlttx.html