美文网首页
iOS进阶-详细介绍runtime

iOS进阶-详细介绍runtime

作者: Gaizka | 来源:发表于2019-05-08 17:40 被阅读0次

    参考:

    1. https://www.jianshu.com/p/1a75373d5ba6
    2. https://blog.csdn.net/WflytoC/article/details/49926345

    目录

    • runtime 概念
    • runtime的成员组成以及结构
    • runtime的消息转发机制流程
    • runtime 常用API 总结
    • runtime使用场景

    一、runtime 概念

    一套C语言标准库,oc 的运行时环境,它将程序类的类型确定、和函数调用逻辑从编译时移动到了运行,是oc成为一种动态语言。

    二、runtime的成员组成以及结构

    描述Objective-C对象所有的数据结构定义都在Runtime的头文件里,下面我们逐一分析。

    1.id

    运行期系统如何知道某个对象的类型呢?对象类型并不是在编译期就知道了,而是要在运行期查找。Objective-C有个特殊的类型id,它可以表示Objective-C的任意对象类型,id类型定义在Runtime的头文件中:

    struct objc_object {
        Class isa;
    } *id;
    
    

    由此可见,每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类,通常称为isa指针。

    objc_object

    objc_object是表示一个类的实例的结构体
    它的定义如下(objc/objc.h):

    struct objc_object{
         Class isa OBJC_ISA_AVAILABILITY;
    };
    typedef struct objc_object *id;
    
    

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

    2.Class

    Class对象也定义在Runtime的头文件中,查看objc/runtime.h中的objc_class结构体:
    Objective-C中,类是由Class类型来表示的,它实际上是一个指
    向objc_class结构体的指针。

    typedef struct objc_class *Class;
    
    struct objc_class { 
        Class isa                                 OBJC_ISA_AVAILABILITY; 
    #if !__OBJC2__
        Class super_class                         OBJC2_UNAVAILABLE;   // 父类
        const char *name                          OBJC2_UNAVAILABLE;   // 类名
        long version                              OBJC2_UNAVAILABLE;   // 类的版本信息,默认为0
        long info                                 OBJC2_UNAVAILABLE;   // 类信息,供运行期使用的一些位标识
        long instance_size                        OBJC2_UNAVAILABLE;   // 该类的实例变量大小
        struct objc_ivar_list *ivars              OBJC2_UNAVAILABLE;   // 该类的成员变量链表
        struct objc_method_list **methodLists     OBJC2_UNAVAILABLE;   // 方法定义的链表
        struct objc_cache *cache                  OBJC2_UNAVAILABLE;   // 方法缓存
        struct objc_protocol_list *protocols      OBJC2_UNAVAILABLE;   // 协议链表
    #endif
    }
    
    

    下面说下Class的结构体中的几个主要变量:

    • 1.isa:
      结构体的首个变量也是isa指针,这说明Class本身也是Objective-C中的对象。isa指针非常重要, 对象需要通过isa指针找到它的类, 类需要通过isa找到它的元类. 这在调用实例方法和类方法的时候起到重要的作用.
    • 2.super_class:
      结构体里还有个变量是super_class,它定义了本类的超类。类对象所属类型(isa指针所指向的类型)是另外一个类,叫做“元类”。
    • 3.ivars:
      成员变量列表,类的成员变量都在ivars里面。
    • 4.methodLists:
      方法列表,类的实例方法都在methodLists里,类方法在元类的methodLists里面。methodLists是一个指针的指针,通过修改该指针指向指针的值,就可以动态的为某一个类添加成员方法。这也就是Category实现的原理,同时也说明了Category只可以为对象添加成员方法,不能添加成员变量。
    • 5.cache:
      方法缓存列表,objc_msgSend(下文详解)每调用一次方法后,就会把该方法缓存到cache列表中,下次调用的时候,会优先从cache列表中寻找,如果cache没有,才从methodLists中查找方法。提高效率。

    元类(Meta Class)

    meta-class是一个类对象的类。
    在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。既然是对象,那么它也是一个objc_object指针,它包含一个指向其类的一个isa指针。那么,这个isa指针指向什么呢?
    为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,meta-class中存储着一个类的所有类方法。
    所以,调用类方法的这个类对象的isa指针指向的就是meta-class
    当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。

    再深入一下,meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。

    即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。

    通过上面的描述,再加上对objc_class结构体中super_class指针的分析,我们就可以描绘出类及相应meta-class类的一个继承体系了,如下代码

    image

    看图说话:
    上图中:superclass指针代表继承关系,isa指针代表实例所属的类。
    类也是一个对象,它是另外一个类的实例,这个就是“元类”,元类里面保存了类方法的列表,类里面保存了实例方法的列表。实例对象的isa指向类,类对象的isa指向元类,元类对象的isa指针指向一个“根元类”(root metaclass)。所有子类的元类都继承父类的元类,换而言之,类对象和元类对象有着同样的继承关系。

    1.Class是一个指向objc_class结构体的指针,而id是一个指向objc_object结构体的指针,其中的isa是一个指向objc_class结构体的指针。其中的id就是我们所说的对象,Class就是我们所说的类。
    2.isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用isKindOfClass:方法来确定实例对象的类。因为KVO的实现机制就是将被观察对象的isa指针指向一个中间类而不是真实的类。

    Category

    Category是表示一个指向分类的结构体的指针,其定义如下:

    typedef struct objc_category *Category
    struct objc_category{
         char *category_name                         OBJC2_UNAVAILABLE; // 分类名
         char *class_name                            OBJC2_UNAVAILABLE;  // 分类所属的类名
         struct objc_method_list *instance_methods   OBJC2_UNAVAILABLE;  // 实例方法列表
         struct objc_method_list *class_methods      OBJC2_UNAVAILABLE; // 类方法列表
         struct objc_protocol_list *protocols        OBJC2_UNAVAILABLE; // 分类所实现的协议列表
    }
    
    

    这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods列表是objc_class中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。
    可发现,类别中没有ivar成员变量指针,也就意味着:类别中不能够添加实例变量和属性

    struct objc_ivar_list *ivars             OBJC2_UNAVAILABLE;  // 该类的成员变量链表
    
    

    3.SEL

    //// http://www.jianshu.com/p/3e050ec3b759
    SEL是选择子的类型,选择子指的就是方法的名字。在Runtime的头文件中的定义如下:

    typedef struct objc_selector *SEL;
    
    

    它就是个映射到方法的C字符串,SEL类型代表着方法的签名,在类对象的方法列表中存储着该签名与方法代码的对应关系,每个方法都有一个与之对应的SEL类型的对象,根据一个SEL对象就可以找到方法的地址,进而调用方法。
    ////http://www.jianshu.com/p/adf0d566c887
    SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:
    方法的selector用于表示运行时方法的名字。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。
    两个类之间,只要方法名相同,那么方法的SEL就是一样的,每一个方法都对应着一个SEL。所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行
    如在某一个类中定义以下两个方法: 错误

    - (void)setWidth:(int)width;
    - (void)setWidth:(double)width;
    
    

    当然,不同的类可以拥有相同的selector,这个没有问题。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。
    工程中的所有的SEL组成一个Set集合,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度上无语伦比!
    本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。
    @selector()就是取类方法的编号
    通过下面三种方法可以获取SEL:
    a、sel_registerName函数
    b、Objective-C编译器提供的@selector()
    c、NSSelectorFromString()方法

    4.Method

    Method代表类中的某个方法的类型,在Runtime的头文件中的定义如下:

    typedef struct objc_method *Method;
    
    

    objc_method的结构体定义如下:

    struct objc_method{
        SEL method_name      OBJC2_UNAVAILABLE; // 方法名
        char *method_types   OBJC2_UNAVAILABLE;
        IMP method_imp       OBJC2_UNAVAILABLE; // 方法实现
    }
    
    
    • 1.method_name:方法名。
    • 2.method_types:方法类型,主要存储着方法的参数类型和返回值类型。
    • 3.IMP:方法的实现,函数指针。(下文详解)
      class_copyMethodList(Class cls, unsigned int *outCount)可以使用这个方法获取某个类的成员方法列表。

    ////
    Method用于表示类定义中的方法
    我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。

    5.Ivar

    Ivar代表类中实例变量的类型,在Runtime的头文件中的定义如下:

    typedef struct objc_ivar *Ivar;
    
    

    objc_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
    }
    
    

    class_copyIvarList(Class cls, unsigned int *outCount) 可以使用这个方法获取某个类的成员变量列表。

    6.objc_property_t

    objc_property_t是属性,在Runtime的头文件中的的定义如下:

    typedef struct objc_property *objc_property_t;
    
    

    class_copyPropertyList(Class cls, unsigned int *outCount) 可以使用这个方法获取某个类的属性列表。

    7.IMP

    IMP在Runtime的头文件中的的定义如下:

    typedef id (*IMP)(id, SEL, ...);
    
    

    IMP是一个函数指针,它是由编译器生成的。当你发起一个消息后,这个函数指针决定了最终执行哪段代码。
    ////
    IMP实际上是一个函数指针,指向方法实现的地址。
    其定义如下:

    id (*IMP)(id, SEL,...)
    
    

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

    前面介绍过的SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的IMP,查找过程将在下面讨论。取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针了。

    8.Cache

    Cache在Runtime的头文件中的的定义如下:

    typedef struct objc_cache *Cache
    
    

    objc_cache的定义如下:

    struct objc_cache {
        unsigned int mask                   OBJC2_UNAVAILABLE;
        unsigned int occupied               OBJC2_UNAVAILABLE;
        Method buckets[1]                   OBJC2_UNAVAILABLE;
    };
    
    

    每调用一次方法后,不会直接在isa指向的类的方法列表(methodLists)中遍历查找能够响应消息的方法,因为这样效率太低。它会把该方法缓存到cache列表中,下次的时候,就直接优先从cache列表中寻找,如果cache没有,才从isa指向的类的方法列表(methodLists)中查找方法。提高效率。

    三、runtime的消息转发机制流程

    • [someObject msg_send([someObject class],SEL)]步骤:
    第一步:检测selector是否可以被忽略,Mac OS X 系统有垃圾回收机制,不会理会retain ,release;
    第二步: 检测selctor 对应的Target是否为nill,Rumtime 允许我们对一个nill对象执行任何方法,不会crash;
    第三步:当前someObject对象通过isa指针找到对应的objc_Class(调用实例方法)或者objc_metaClass(调用类方法时);
    第四步:在objc_Class内部的cache里通过SEL选择子进行匹配,如果找到对应的objc_Mehod,就用objc_Mehod内部的method_IMP找到对应的c函数执行,没有进入下一步;
    第五步:在objc_Class内部的method_list里通过SEL选择子进行匹配,如果找到对应的objc_Mehod,就用objc_Mehod内部的method_IMP找到对应的c函数执行,没有进入下一步;
    第六步: 通过objc_Class内部的super_class只找到父类,分类去对应的cache、method list寻找,找到就直接执行;没有继续下一步;
    第七步:通过父类的root类去找,分类去对应的cache、method list寻找,找到就直接执行;没有继续下一步;
    第八步: 如果没有找到,就会执行消息转发(message forwarding);
    
    • 详细介绍消息转发步骤:


      image.png

    rumtime 在发送消息 没有找对用对应目标对象需要执行的任务时,允许我们进行3次修正:

    1. 方法动态解析: 目标通过自己是想新的IMP函数和resolveInstenceMethod或者resolveClassMethod ; 如果这两个方法参数都是没有找到对应地址的SEL变量,如果实现类存在对应的方法,首先runtime 为当前的SEL变量重新设置IMP指针,并且返回Yes,rumtime会重新执行消息发送;
    void fooMethod(id obj, SEL _cmd)  
    {
        NSLog(@"Doing foo");
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)aSEL
    {
        if(aSEL == @selector(foo:)){
            class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:");
            return YES;
        }
        return [super resolveInstanceMethod];
    }
    
    
    1. 快速转发 : 找到实现 forwaringTargetingSelector ,参数是没有执行的SEL ,forwaringTargetingSelector内部如果如果自己存在SEL一样的函数,就会将当前的对象返回出去,runtime会重新想当前新的目标对象发送消息;
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        if(aSelector == @selector(foo:)){
            return [[BackupClass alloc] init];
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    
    1. 正常转发: 同快速转发都是想新的目标对象发送消息,但是可以代替快速转发做更多的事。
      forwardInvocation: 方法就是一个不能识别消息的分发中心,将这些不能识别的消息转发给不同的消息对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的“吃掉”某些消息,因此没有响应也不会报错。例如:我们可以为了避免直接闪退,可以当消息没法处理时在这个方法中给用户一个提示,也不失为一种友好的用户体验。
      其中,参数invocation是从哪来的?在forwardInvocation:消息发送前,runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。所以重写forwardInvocation:的同时也要重写methodSignatureForSelector:方法,否则会抛出异常。当一个对象由于没有相应的方法实现而无法响应某个消息时,运行时系统将通过forwardInvocation:消息通知该对象。每个对象都继承了forwardInvocation:方法,我们可以将消息转发给其它的对象。
    - (void)forwardInvocation:(NSInvocation *)invocation {
        SEL sel = invocation.selector;
        if([alternateObject respondsToSelector:sel]) {
            [invocation invokeWithTarget:alternateObject];
        } else {
            [self doesNotRecognizeSelector:sel];
        }
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
        if (!methodSignature) {
            methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
        }
        return methodSignature;
    }
    
    

    这里附加NSObject+CrashHandle代码

    #import "NSObject+CrashLogHandle.h"
    
    @implementation NSObject (CrashLogHandle)
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        //方法签名
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        NSLog(@"NSObject+CrashLogHandle---在类:%@中 未实现该方法:%@",NSStringFromClass([anInvocation.target class]),NSStringFromSelector(anInvocation.selector));
    }
    
    @end
    
    

    四、runtime 常用API 总结

    第一类 : 为对象工作的API

    object_getIvar : 获取对象的某个实例变量的值;
    object_setIvar : 设置对象的某个实例变量的值;
    object_getClassName : 获取当前对象的isa所指的类的名字;NSStringFromClass(Class )
    object_getClass : 获取当前对象的isa所指的类;
    object_setClass : 设置对象的isa所指的类;

    第二类: 为Class工作的API

    class_getName: 获取Class变量的名字; 相当于NSStringFromClass(Class )
    class_getSupperClass : 获取父类;
    class_isMetaClass : 判断是否是元类;
    class_addIvar: 为class添加成员变量; 这个步骤要在 alloc之后register之前才有效果;
    class_add_Method : 为当前的类添加实例方法
    class_getInstanceMethod 获取当前类的实例方法
    class_getClassMethod 获取当前类的实例方法
    class——copyMethodList 获取当前类的所有的方法
    class_replaceMethod 替换某个类的方法签名对应的实现
    class_respondsToSelector 判断某个类是否存在SEL对应的函数

    第三类 : 添加类相关

    objc_allocateClassPair 创建一个类 里面要穿入你要继承的类Class,新的类名c字符串,大小
    objc_disposeClassPair 销毁一个类和它对应的是元类
    objc_registerClassPair 将创建的类假如内存,一般在添加完方法、成员变量、属性、协议、分类、扩展后使用

    实例化一个类相关

    class_createInstance 默认在malloc memory zone.

    objc_destructInstance 销毁一个实例对象

    成员变量相关

    ivar_getName: 获取 Ivar的变量名字
    ivar_getTypeEncoding: 获取 Ivar的变量类型

    观象对象相关

    objc_setAssociatedObject : 设置一个对象的关联对象,让源对象持有关联对象 参数 1 : 源对象 2、唯一的关键一是一个void的指针变量 3、关联对象 4关联规则
    objc_getAssociatedObject : 通过关键字获取一个对象它所有持有对象 参数: 1、目标对象 2、关联对象的关键字
    objc_removeAssociatedObjects 移除目标对象所有关联的对象 参数: 目标对象

    发送消息相关 ****比较关键的

    objc_msgSend : 向目标对象发送函数调用消息 参数:1、目标对象 2、SEL函数唯一签名 有关调用过程上面介绍过

    runtime 内部相关的结构对象

    An opaque type that represents an Objective-C class.

    Method

    An opaque type that represents a method in a class definition.

    Ivar

    An opaque type that represents an instance variable.

    Category

    An opaque type that represents a category.

    objc_property_t

    An opaque type that represents an Objective-C declared property.

    IMP

    A pointer to the start of a method implementation.

    SEL

    Defines an opaque type that represents a method selector.

    objc_method_description

    Defines an Objective-C method.

    objc_method_list

    Contains an array of method definitions.

    Deprecated

    objc_cache

    Performance optimization for method calls. Contains pointers to recently used methods.

    objc_protocol_list

    Represents a list of formal protocols.

    objc_property_attribute_t

    Defines a property attribute.

    五、runtime使用场景

    5.1. kvo

    介于传统的kvo实现方式过于的死板,只能通过代理方法实现监听回调,因此,我自己用 runtime 写了一个NSOBject+ggzKVO的分类,主要实现 : 一层属性监听和多层属性监听 2、监听回调方法定义函数回调、block实现回调
    实现原理:
    1、创建目标类的派生类,将派生类的isa指针指向目标类;
    2、将观察关系的信息保存在一个字典里,字典放在集成在NSObject的对象里;
    3、重写父类的setter方法,在改变父类的值之前获取旧值,之后获取新的值,然后获取观察信息,获取当前key对应的对调函数或者block进行回调
    说着简单,不如自己手动写一下

    github-custom-runtime-kvo 连接 : https://github.com/ge123/test-runtime-kvo

    5.2. kvc :
    setValue forKey:(NSString*) key 查找顺序: 去查找对应的set<key>方法,如果没有找到如果本类的accessInstanceVariablesDirectly属性返回YES,则按 _<key>、_is<key> 、key、is<key>的顺序去查找,如果最后还是没找到,就回调setValue:forUndefinedKey方法;我们可以重写setValue:forUndefinedKey方法进行防止报错;
    github-custom-runtime-kvo 连接 : https://github.com/ge123/runtime-kvc

    5.3. JSON转Model
    github-custom-runtime-kvo 连接 : https://github.com/ge123/runtime--

    5.4. categary 实现添加属性成员变量
    github-custom-runtime-kvo 连接 : https://github.com/ge123/runtime-categary--

    相关文章

      网友评论

          本文标题:iOS进阶-详细介绍runtime

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