转载

浅谈Runtime

转自南华coder的空间

Objective-C利用Runtime库(底层的 C 语言 API)赋予了语言的动态特性。

一、Runtime基石:Objective-C对象模型

1、对象

  • 每一个对象都是 类的实例 , 类中保存 对象的方法列表 ;当一个对象方法被调用时,类会首先查找它本身是否有该方法的实现,如果没有,则会向它的父类查找该方法,直到NSObject(根类);

  • 类是 元类 (metaclass) 的实例 ;元类保存 类方法列表 ;当一个类方法被调用时,元类会首先查找它本身是否有该类方法的实现,如果没有,则会向它的父类查找该方法,直到NSObject(根类);

2、isa指针

  • 对象的 isa指针 指向所属的类,类的 isa指针 指向所属的元类;所有的元类的  isa指针 都会指向一个根元类 (root metaclass)。根元类的  isa指针 指向自己,行成了一个闭环。

  • 在64 位 CPU 下,isa 的内部结构有变化。具体查看用  isa 承载对象的类信息

  • 对象、isa指针、类、元类、根元类的关系如下图:

浅谈Runtime

3、对象布局

  • 实例变量(包括父类)都保存在对象本身的存储空间内;实例方法保存在 中,类方法保存在 元类 中;父类的实例方法保存在各级 super class 中,父类的类方法保存在各级 super meta class;

    //对象组成 --start--
    isa pointer
    rootClass's vars
    penultimate superClass's vars
    ...
    superClass's vars
    Class's vars
    //对象组成 --end--
    
    typedef struct objc_class *Class;
     //类的结构
     struct objc_class{
      struct objc_class* isa;                              //指向元类
      struct objc_class* super_class;                //指向父类
      const char* name;
      long version;
      long info;
      long instance_size;
      struct objc_ivar_list* ivars;                         //实例变量列表
      struct objc_method_list** methodLists;      //方法列表
      struct objc_cache* cache;
      struct objc_protocol_list* protocols;           //协议列表
    };
    
    //实例变量的结构
    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
    }

    说明1:对象中保存指向类的isa指针 以及 各级的 实例变量(ivar),这个内存结构在编译时就确定下来了,不能在编译时给对象增加实例变量。

    说明2:类的内存布局有isa指针、super_class指针、实例变量列表、方法列表和协议列表,其中实例变量(var)包含了变量的名称、类型、偏移等。

二、Runtime核心:消息发送和转发

Runtime赋予了OC了诸多动态特性,使其可以在运行时可以做一些事情;主要表现为: 动态类型 (在运行时才检查对象类型)和  动态绑定 (接到消息后,由运行环境决定执行哪部分代码)

1、消息发送(Message)

  • Objective-C 中的方法调用,实质上是在底层用objc_msgSend()实现 消息发送 ,其核心在于: 根据SEL(选择器)开始找到IMP ;其中SEL是实例方法的指针,可以看做方法名字符串;IMP是函数指针,指向方法实现的地址。

    //调用方法  
    [obj doSomething];
    //在编译时候转换
    objc_msgSend(obj,@selector(doSomething))
  • objc_msgSend的定义如下:

    // self是接收者,接收该消息的类的实例
        // _cmd是选择器,要处理的消息的selector
         // ... 是需传入的参数,参数个数不定
        objc_msgSend(id self, SEL _cmd, ...)
  • objc_msgSend的发送流程:先在Class中的缓存查找imp(没缓存则初始化缓存),如果没找到,则向父类的Class查找。如果一直查找到根类仍旧没有实现,就走消息转发(_objc_msgForward)了。

  • 给nil发送消息不会有什么作用,但是返回值有些区别,具体如下:

    a) 如果方法返回值是 对象,返回nil
    b) 如果方法返回值是 指针类型,其指针大小为小于或者等于sizeof(void*),float,double,long double 或者 long long 的整型标量
    c) 如果方法返回值是 结构体,发送给 nil 的消息将返回0。结构体中各个字段的值将都是0。
    d) 如果方法返回值不是 上述提到的几种情况,那么发送给 nil 的消息的返回值将是未定义的。

2-1、消息转发(Message Forwarding)

消息转发解决的是:查找IMP(方法实现)失败后的处理;经历 动态方法解析 、  备用接收者 和  完整的消息转发 三个过程,其流程如下图:

浅谈Runtime

  • 动态方法解析:接收到未知消息时,Runtime向当前类发送+resolveInstanceMethod:或+resolveClassMethod:消息,在这里可以 添加缺失的方法 ,返回YES,重新发送消息,否则继续下一步;

  • 备用接收者: 动态方法解析 中没能处理,Runtime会向forwardingTargetForSelector:发消息,如果该方法返回了一个非nil或非self对象,恰好该对象实现了这个方法,那么该对象就成了消息的接收者,消息就被分发到该对象。

  • 完整消息转发:前两个都没能处理好,Runtime发送methodSignatureForSelector:消息,获取selector对应方法的签名;如果有方法签名返回,则根据方法签名创建描述消息的NSInvocation,向当前对象发送forwardInvocation:消息;如果没有方法签名返回,返回nil,向当前对象发送doesNotRecognizeSelector:消息,应用Crash退出。

2-2、避免消息转发的办法

在消息转发三个过程中,未知消息的处理过程越往后,代价越大;一般我们可以这么做 尽可能避免消息转发,可以这么做:

  • 调用delegate 方法前检查方法是否实现(respondsToSelector:), 只有实现了(respondsToSelector:返回YES) ,才去真正调用delegate 方法。

    if([self.delegate respondsToSelector: @selector(sayHello)]) {
        [self.delegate sayHello];
    }
  • 直接调用方法,少用performSelector:;因为在直接调用方法时,编译自动校验,如果方法不存在,编译器会直接报错;而使用performSelector:的话一定是在运行时候才能发现,如果此方法不存在就会崩溃。

    //直接使用方法调用,少使用performSelector
    [dog sayHello];
    // [dog performSelector:@selector(sayHello) withObject:nil];
  • 使用performSelector:,最好先判断方法是否实现(respondsToSelector:),只有实现了(respondsToSelector:返回YES) ,才去调用performSelector:方法。

    //respondsToSelector:和performSelector:组合使用
        if ([dog respondsToSelector:@selector(sayHello)])         {
        [dog performSelector:@selector(sayHello)];
     }
  • 强制类型转换,先判断对象是否属于强制转换后的类

    if([data isKindOfClass:[NSDictionary class]]){
      //
    }

三、Runtime特性和应用

1、分类(Category)

  • 原理:对象的 方法 定义都保存在类的可变区域中,修改methodLists指针指向的指针的值,就可以实现动态地为某一个类增加 成员方法 。( 但是对象布局在编译时候就固定了,结构体的大小并不能动态变化,在运行时不能增加实例变量 )。

  • 通过关联objc_setAssociatedObject 和 objc_getAssociatedObject方法可以变相地给对象增加实例变量,并不会真正改变了对象的内存结构。

  • 通过Category新增的方法,会插入到方法列表的前部;如果有和原来方法重名,在运行时,顺序查找时,一旦找到对应名字的方法,就不再查找,导致 原来方法 得不到机会, 这是Category新增的方法和原方法重名,原有方法失效的原因

  • 作用:给现有的类添加方法;将一个类的实现拆分成多个独立的源文件;声明私有的方法。

2、关联对象(Associated Objects)

  • 原理:Category不能给一个已有类添加实例变量,但是可以通过 关联对象 添加属性;但是关联对象不会改变对象的内存布局,新增的属性是添加到和对象地址关联的哈希表中;

  • Associated Objects 相关的三个方法

    objc_setAssociatedObject    //添加关联对象
    objc_getAssociatedObject    //获取关联对象
    objc_removeAssociatedObjects  // 删除所有关联对象
  • 作用:为现有的类添加私有变量以帮助实现细节;为现有的类添加公有属性;为 KVO 创建一个关联的观察者

    具体参考: Objective-C Associated Objects 的实现原理

3、方法混写(Method Swizzling)

  • 原理:在运行时交换方法实现(IMP)

  • 作用:可以利用它hook原有的方法,插入自己的业务需求,

    详细参考我写的 Method Swizzling小记

4、键值观察(KVO)

观察者模式在Objective-C的应用之一,借助Runtime特性,实现自动键值观察;使用了isa swizzling机制。具体描述如下:

  • 当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个 子类 ,在这个子类中 重写 基类中被观察属性的 setter 方法,实现真正的通知机制;

  • 派生类还重写了 class 方法以“欺骗”外部调用者,系统将对象的 isa 指针指向这个 新诞生的子类 ,实质上这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。

  • 此外,派生类还重写了 dealloc 方法来释放资源。

说明:KVC(键值编码)是不通过存取方法,而通过属性名称字符串间接访问属性的机制,没有用到isa swizzling机制。

原文  http://www.cocoachina.com/ios/20180418/23067.html
正文到此结束
Loading...