iOS开发Runtime基础知识

iOS开发-Runtime基础知识

前言

Runtime又简称为运行时。其提供了对Objective-C语言动态属性的支持。Runtime的特性主要是消息(方法)传递,如果消息(方法)在对象中找不到,就进行转发。下面我们就从数据结构类对象与元类对象消息传递方法缓存消息转发Method-Swizzling动态添加方法动态方法解析这些方面来探寻一下。

数据结构

主要存在一下结构:

  • objc_object
  • objc_class
  • isa指针
  • method_t

objc_object

  • id = objc_object:OC中的id类型在runtime中就是对应objc_object这样的数据结构

objc_class

  • Class = objc_class:Class代表一个类,在runtime中就是对应objc_class这样的数据结构
  • objc_class继承自objc_object

  • cache_t:表达了方法缓存的结构,在消息传递时会使用到
  • class_data_bits_t:关于一个类,定义的变量、属性、方法都在class_data_bits_t结构当中

isa指针

  • 实际上是C++中的共用体isa_t结构
    • 指针型isa:isa的代表Class的地址
    • 非指针型isa:isa的值的部分代表Class的地址
  • isa指向
  • 关于对象,其指向类对象
  • 关于类对象,其指向元类对象(MetaClass)。 (当我们在进行方法调用的时候, 若调用对象的实例方法,实际上是通过对象的isa指针到类对象当中去进行方法查找; 若我们调用的是类方法,则是通过类对象的isa指针到元类对象中去进行方法查找;)

cache_t

  • 用于快速查找方法执行函数
  • 是可增量扩展哈希表结构(在结构中随着存储内容增大,也会增量扩大内存空间)
  • 局部性原理的最佳应用 (简单说明局部性原理: 可能我们在调用的方法就集中那么几个方法,也就是这几个方法调用的频次是最高的,我们就可以把这些方法放到缓存当中,可能下次命中就更高一些)

  • cache_t可以理解为一个数组实现的,里面每一个对象都是bucket_t这样的结构体。
  • bucket_t由key和IMP组成;key对应OC中的选择器SEL,IMP可以理解为一个无类型的函数指针,即方法的实现

class_data_bits_t

  • class_data_bits_t主要是对class_rw_t的封装
  • class_rw_t代表了类相关的读写信息(比如给类的分类添加的方法、属性、协议),是对class_ro_t的封装(rw:可读可写;ro:只读)
  • class_ro_t代表了类相关的只读信息

class_rw_t

(以methods举例,methods装载了类的不同分类的方法集合;)

class_ro_t

method_t

  • 首先回顾下函数的四要素:返回值、名称、参数、函数体。
  • struct method_t:实际上是一个结构体类型,包含以下数据结构
    • SEL name:代表方法的名称
    • const char* types:对应的是方法的返回值和参数的组合
    • IMP imp:实际上是一个无类型的函数指针,所对应的就是函数体

下面讲解下const char* types,那么OC中是怎么实现的呢,这就要涉及苹果开发中Type Encodings技术。

  • Type Encodings
    • const char* types;

  • v 代表返回值是void类型的
  • @ 代表第一个参数类型是id,即self
  • : 代表第二个参数是SEL类型的

整体数据结构

对象、类对象、元类对象

  • 类对象存储实例方法列表等信息;
  • 元类对象存储类方法列表等信息;

消息传递

我们先看下这段代码的结果如何

@implementation Son

- (instancetype)init
{
    self = [super init];
    if (self) {
        NSLog(@"class name01 = %@", NSStringFromClass([self class]));
        NSLog(@"class name02 = %@", NSStringFromClass([super class]));
    }
    return self;
}
@end

答案:
class name01 = Son
class name02 = Son

方法调用会编译器转为objc_msgSend、objc_msgSendSuper

  • void objc_msgSend(void /* id self, SEL op, … */ )
  • void objc_msgSendSuper(void /* struct objc_super *super, SEL op, … */ )
objc_super的结构:
struct objc_super {
	/// Specifies an instance of a class.
	__sunsafe_unretained id receiver; // 就是当前对象
}

(其实super只是编译器关键字,最后还是struct objc_super中的receiver接收消息)

  • objc_msgSend和objc_msgSendSuper的区别
    • objc_msgSendSuper从父类的方法列表中开始查找

消息转发的流程:

下面我们继续分析三个判断条件里的实现

缓存查找

  • 例:给定值是SEL,目标值是对应bucket_t中的IMP
  • 通过Hash查找,cache_key_t–f(key)—>bucket_t

当前类中查找

  • 对于已排序好的列表,采用二分查找算法查找方法对应执行函数
  • 对于没有排序的列表,采用一般遍历查找方法对应执行函数

父类逐级查找

消息转发流程

  • 转发流程,相关的函数:

  • resolveInstanceMethod
  • forwardingTargetForSelector
  • methodSignatureForSelector

Method-Swizzling

  • Method Swizzling是一种改变一个selector的实际实现的技术。通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现。

  • 举例,代码如下:

@implementation NSMutableDictionary (HYLSafe)

+ (void)load 
{
    Class dictCls = NSClassFromString(@"__NSDictionaryM");
    Method originalMethod = class_getInstanceMethod(dictCls, @selector(setObject:forKey:));
    Method swizzledMethod = class_getInstanceMethod(dictCls, @selector(hyl_setObject:forKey:));
    // 确保不插入nil对象
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)hyl_setObject:(id)anObject forKey:(id<NSCopying>)aKey 
{
    if (!anObject)
        return;
    [self hyl_setObject:anObject forKey:aKey];
}

@end
  • 使用场景:
    • 替换系统方法,如viewDidLoad,统一处理
    • NSMutableDictionary的setObject方法,保证程序不崩溃

动态添加方法

  • 应用场景:可能有人会问,你有用过performSelector方法么?(隐式询问是否用过动态添加方法,因为类可能在编译时未实现某个方法,运行时才添加了方法)
  • 具体添加实现:
void testImp(void)
{
    NSLog(@"test invoke");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(test)) {
        // 动态添加 test方法的实现
        class_addMethod(self, @selector(test), testImp, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

动态方法解析

  • 应用场景:可能有人会问,你有使用过@dynamic这个编译器关键字?(隐式考察编译时语言与动态运行时语言的区别)
    • 当我们声明的属性,在实现中标志为@dynamic的时候,相当于属性的get、set方法是在运行时添加的,而不是编译时自动添加实现的

Q:编译时语言与动态运行时语言的区别?

  • 动态运行时语言将函数决议推迟到运行时;
  • 编译时语言在编译期进行函数决议;

总结

  • [obj foo]和objc_msgSend()函数之间有什么关系?
  • runtime如何通过Selector找到对应的IMP地址的?(消息传递的流程)
  • 能否向编译后的类中增加实例变量?(不能,可为动态添加的类增加实例变量)

参考文章

最后

如果对大家有帮助,请github上follow和star,本文发布在戴超的技术博客,转载请注明出处

Loading Disqus comments...
Table of Contents