分享

iOS 读懂runtime基础(一)

 印度阿三17 2020-03-06

目录

前言

本文会详细描述Objective-C运行时的各对象数底层据结构、类和原类、消息传递与转发、动态方法等技术方案. 文中底层代码实现均来自Apple open source; 本文篇幅较长, 文中描述加之有个人的一点理解, 主要用作记录和学习之用, 文笔粗陋, 技术菜鸡, 如有错误或不妥之处, 万望各位大佬不吝指教, 不胜感激!

runtime是什么

Objective-C runtime是一个动态运行库, 它给Objective-C语言的动态性提供了支撑. 所有的应用都会链接到该运行时库.

The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps. Objective-C runtime library support functions are implemented in the shared library found at /usr/lib/libobjc.A.dylib.

三个重要概念

在下述讲述过程中, 你应该注意三个非常重要的概念.即Class、SEL、IMP, 在这里我先把他们列出来, 后面我们会一一的深入讲到其内部结构和之间的关系.

typedef struct objc_class *Class;
 
typedef struct objc_object {
 
    Class isa;
 
} *id;
 
typedef struct objc_selector   *SEL;   
 
typedef id (*IMP)(id, SEL, ...);

一 各主要对象数据结构

1 objc_object

objc_object表示实例对象底层是结构体, 内部有一个私有的isa指针, 该指针指向了其类对象

struct objc_object {
private:
    isa_t isa;
...
// isa相关操作
// 弱引用, 关联对象, 内存管理等等相关的操作
// 都是在此结构体中, 篇幅太长, 不再全部贴出
}

2 objc_class

objc_class继承自objc_object(所以肯定有isa指针), 表示类对象, 底层仍然是结构体, 其内部的isa指针, 指向了该类的元类对象. 同时, 内部的superclass指向了自身的父类对象, NSObject对象superclass指向了nil, cache是一个方法缓存结构体, bits是存储变量、属性、方法等的结构体

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
    class_rw_t *data() { 
        return bits.data();
    }
    ...
	// 类相关的数据操作都是在此结构体中, 不再全部贴出
}

2.1 cache_t

缓存方法, 消息传递时, 会先通过哈希查找算法, 在此数据结构中查询是否有要执行的方法缓存, 如果有则快速执行该方法函数, 这样提高了消息传递的效率;

方法缓存策略, 是局部性原理的最佳应用;

本质是一个可增量的哈希表, 其内部维护了一个由bucket_t组成的结构体列表

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
public:
    struct bucket_t *buckets();
    ...
};

bucket_t内部存储了方法缓存key和无类型函数指针地址的映射关系, 在查找缓存时, 通知指定的key查找到具体的bucket_t, 再从bucket_t中查询到函数IMP地址, 进而去执行函数

struct bucket_t {
private:
    cache_key_t _key;
    IMP _imp;

public:
    inline cache_key_t key() const { return _key; }
    inline IMP imp() const { return (IMP)_imp; }
    inline void setKey(cache_key_t newKey) { _key = newKey; }
    inline void setImp(IMP newImp) { _imp = newImp; }

    void set(cache_key_t newKey, IMP newImp);
};

cache内存结构示意图

2.2 class_data_bits_t

  • class_data_bits_t结构主要是对class_rw_r的封装
  • class_rw_r又是对class_ro_r的封装
struct class_rw_t {
	// class_rw_t部分代码
    uint32_t flags;
    uint32_t version;
	// 指向只读的结构体, 存储类初始内容
    const class_ro_t *ro;
	/*
	三个可读写二维数组, 存储了类的初始化信息, 内容
	*/
    method_array_t methods;			// 方法列表
    property_array_t properties;	// 属性列表
    protocol_array_t protocols;		// 协议列表
	// 第一个子类
    Class firstSubclass;
    // 下一个同级类
    Class nextSiblingClass;
};

class_ro_t结构

struct class_ro_t {
   	// class_ro_t部分代码
    const char * name;
    // class_ro_t存储的是类在编译期就确定的方法, 属性, 协议等
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

需要注意的是, class_ro_t存储的是类在编译期就确定的内容信息, 而class_rw_t不仅包含了类在编译期的内容信息(其实是把class_ro_t的内容合并), 还包含了在运行时动态添加的类内容, 如分类添加的方法, 属性, 协议等内容; 一张图来表示上述结构之间的关系:
class_rw_t和class_ro_t

3 isa

在arm64为架构之前, isa指针存储了类或元类对象的地址信息, 从arm64架构开始对isa指针(非指针型指针)进行了优化, 用位域存储了除类或元类地址信息以外的其他信息, 如has_assoc表示是否设置关联对象

union isa_t  {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
    struct {
    	// 标记位, 0 代表指针型isa, 1代表非指针型isa
        uintptr_t indexed           : 1;
        // 是否有关联对象
        uintptr_t has_assoc         : 1;
        // 是否有C  析构函数
        uintptr_t has_cxx_dtor      : 1;
        // 存储当前对象的类或元类的内存地址
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        // 判断对象是否已经初始化
        uintptr_t magic             : 6;
        // 对象是否有弱引用指针
        uintptr_t weakly_referenced : 1;
        // 当前对象是否有dealloc操作
        uintptr_t deallocating      : 1;
        // 当前isa指针是否有外挂引用表
        // 引用计数值大于isa所能存储最大值时
        // 就会绑定一个sidetable散列表属性, 来存储更多的引用计数信息
        uintptr_t has_sidetable_rc  : 1;
        // 额外的引用计数值
        uintptr_t extra_rc          : 19;
    };
}

这里需要注意

  • isa所属对象是实例对象, 则其指向实例对象的类对象
  • isa所属对象是类对象, 则其指向类对象的元类对象
    在这里插入图片描述

4 method_t

method_t是函数的底层数据结构, 是对函数的封装, Apple对函数的介绍在这里

struct method_t {
    SEL name;			// 函数名称
    const char *types;	// 函数返回值和参数
    IMP imp;			// 无类型函数指针指向函数体
    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool> {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

4.1 函数的四要素

  • 名称
  • 返回值
  • 参数
  • 函数体

4.2 types

Apple使用了Type Encodings技术, 来实现类型编码, Objective-C 运行时库内部利用类型编码帮助加快消息分发.
结构是个列表, 包含了函数的返回值, 参数1, 参数2, 参数3 …, 其中函数的返回值存储在第0个位置, 因为函数只有一个返回值(Go支持多返回值), 而参数可以有多个.

对于一个无类型无参数的函数, 其types值为 “V@:”

- (void)method {
    // 其中
    // V对应返回值, 代表返回值类型为void
    // @对应第一个参数, id类型代表一个对象, 默认第一个参数是对象本身(self), 且该参数是固定的
    // :对应SEL, 代表该参数是个方法选择器, 且该参数是默认的第二个固定参数
}

5 一张图表明各数据结构之间的关系

runtime基础数据结构

二 实例对象、类对象和元类对象

一大佬(膜拜)画的一张图, 足以说明三者之间的关系.( Apple官网也有类似的描述,但是个人感觉没有下面这张图更精彩)
实例、类与元类

  • 实例对象的isa指针指向其类对象
  • 类对象的isa指针指向其元类对象
  • 任何元类对象的isa指针都指向根元类对象
  • 类对象的superclass指针指向其父类对象, 根类对象指向nil
  • 元类对象的superclass指针指向其父元类对象, 根元类对象指向根类

其中, 根类在Objective-C中即为NSObject. 实例对象其实就是objc_object(), 类对象就是objc_class(); 上面讲到, objc_class()是继承自objc_object(), 因此类对象中也有isa指针

typedef struct objc_object {
    Class isa;
} *id;

从底层数据结构可以看出, 类对象中存储了实例对象方法列表, 成员变量等内容; 同时, 元类对象中存储了类对象的类方法列表等内容;

1 实例方法调用时是如何查找的

当一个实例对象调用一个实例方法时

  • 首先会根据该对象的isa指针, 查到到其类对象, 在类对象方法列表中查询是否有所调用方法的实现;
  • 如果没有, 则类对象会根据自身的superclass指针查找其父类对象, 在父类对象方法列表中查询是否有所调用方法的同名方法实现;
  • 递归调用直至根类对象, 如果中间有任何一步查询到了具体的方法实现, 就去执行具体的函数调用;
  • 如果直至根类, 仍然没有找到方法实现, 则会调用系统两个方法, 然后走系统调用流程;
  (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
  (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

具体的消息传递流程请往下看

2 self和super

self是当前类的因此参数, 指向类的实例对象, 进行方法调用时, 代表从当前类开始进行查找

OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
    __OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

super本质是个编译器标识符, 仅代表方法查找时从当前对象所属父类开始查找方法实现

OBJC_EXPORT id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
    __OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

三 消息传递与消息转发

在开发中, 我们经常会碰见这样子一个错误 unrecognized selector sent to instance xx
大致意思是, 你调用了一个并不存在的方法. 现在, 我们会深入的探究一下为什么会出现这个异常.
其实上面这个异常会正好就是我们要讲的, 在Objective-C的消息机制中, 用OC消息机制来说: 如果消息在传递的过程中找不到具体的IMP, 内部就触发了消息转发机制, 而系统的消息转发机制默认实现是抛出上述的异常. 接下来, 我们分别讲述消息的传递和转发.

我们知道Objective-C是动态语言, 方法的调用并不像C的静态绑定一样, 在编译的时候就确定了程序运行时该调用哪个函数(C中没有方法实现会报错), 而是在运行时基于runtime这个动态运行时库通过一系列的查找才决定调用哪个函数, 这样的调用方式更加灵活, 我们甚至可以在运行时动态的修改某个方法的实现, 与当下流行的"热更新"技术有些类似. 而这个查找过程就是Objective-C的消息机制.

1 消息传递流程

在Objective-C中, 方法调用其实就是给某个对象发送消息, 在编译后的文件中我们发现, 底层都转变为函数调用

// 返回值, 参数1: 固定self, 参数2: 固定SEL, 后面是参数3, 参数4....
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
    __OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

从代码中可以看到, 消息发送函数有两个默认的参数, 第一个是消息接受者receiver, 默认就是当前对象self; 第二个默认参数是SEL, SEL的本质的方法选择器selector; (阅读运行时的文档你会发现, 几乎所有方法调用都和selector有关系)! 所以, 我们的方法调用可以这样表示** [receiver selector] **, 那么这个selector究竟是何方神圣, 遗憾的是我在Apple和GNU提供的runtime代码中,都只找到了这一行代码.

typedef struct objc_selector *SEL;

不过Apple给了说明, 方法选择器selector就是个映射到C中的字符串. 根据我翻阅的各种资料都显示, selector就是个C字符串类型的方法名称.

Method selectors are used to represent the name of a method at runtime. A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.

说了半天, 跟我们的运行时有什么关系(objc_msgSend()是[receiver selector]编译阶段实现的)?那么, objc_msgSend()函数在运行时是如何进一步调用的呢?

  • 首先, 通过 recevier的isa指针寻找到recevier的class(类);
  • 其次, 先在class中的cache list(缓存列表)查找是否有对应的缓存selector;
  • 如果在缓存列表中查找到, 那么就根据selector(key)直接执行方法对应的IMP(value);
  • 否则, 继续在 class的method list(方法列表)中查找对应的 selector;
  • 如果没有找到对应的selector, 就继续在它的 superclass(父类)中寻找;
  • 最后, 如果找到对应的 selector, 直接执行 recever 对应 selector 方法实现的 IMP(方法实现)
  • 否则, 系统进入默认消息转发机制.

我们用一张图来表示上述流程消息传递流程
有时候, 我们会通过super调用, 其实道理是一样的, 编译后会生成objc_msgSendSuper()函数

OBJC_EXPORT id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
    __OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

而objc_super结构体内部, 消息接受者仍然是receiver当前实例对象, 与上面不唯一不同的是, self是从当前对象的类对象中开始查找对应实现, 而super则是跨过当前对象的类对象直接从类对象的父类对象开始查找方法实现;

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained id receiver;
};

2 消息转发流程

在消息的传递过程中, 我们讲到, 如果receiver 找不到对应的selector的IMP实现, 则会进入系统的默认消息转发流程. 而系统默认处理消息转发的机制就会抛出unrecognized selector sent to instance xx异常, 然后结束整个消息转发. 如果想要避免这种情况的发生, 我们就需要在如果selector找不到的情况下在运行时动态的给receiver添加实现.
幸运的是虽然系统默认默认流程是抛异常, 但是在抛异常的方法调用过程中, 系统给我们开了口子, 让我们可以通过 动态解析、receiver重定向、消息重定向等对消息进行处理, 流程如下图:
消息转发流程

2.1 消息动态解析

在系统处理消息转发的过程中, 首先会根据调用对象类型不同分别调用如下两个api, 我们可以通过重载在这两个方法内部动态添加方法, 进而避免crash

// 找不到类方法, 重载此类方法添加类方法实现
  (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
// 找不到实例方法, 重载此类方法添加实例方法实现
  (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

我们以实例方法为例举个🌰

// 此处实例方法test没有方法实现
  (BOOL)resolveInstanceMethod:(SEL)sel {
    // 判断是否是test方法
    if (sel == @selector(test)) {
        NSLog(@"resolveInstanceMethod:");
        // 动态添加test方法的实现
        class_addMethod(self, @selector(test), testImp, "v@:");
    }
    
    return [super resolveInstanceMethod:sel];
}
void testImp (void) {
    NSLog(@"test invoke");
}

2.2 消息接受者重定向

如果在resolveInstanceMethod:SEL中没有处理消息(即返回NO), 则系统会给我们第二次机会, 调用forwardingTargetForSelector:SEL! 方法返回值是个id类型, 告诉系统这个实例方法调用转由哪个对象(如果是类方法调用则返回类对象; 如果是实例方法调用, 则返回实例对象)来接受处理, 如果我们指定了新的receiver, 就把消息重新交给新的receiver处理.
同样的, 我们举个🌰

 - (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        NSLog(@"forwardingTargetForSelector:");
        // 重定向, 让ForwardObj对象作为receiver, 接收处理这个消息
        return [[ForwardObj alloc] init];
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

2.3 消息重定向

如果系统给我们第二次机会时, 我们返回的对象是nil, 或者self, 那系统会最后一次给我们避免crash的机会, 即消息重定向流程, 调用methodSignatureForSelector方法, 返回值是个方法签名
继续举个🌰

// 定义函数参数和返回值类型, 并返回函数签名
 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        NSLog(@"methodSignatureForSelector:");
        // v代表方法签名的返回值void, @ id类型代表self
        // : SEL类型, 代表方法选择器, 其实就是@selector(test)
        return [NSMethodSignature signatureWithObjCTypes:"@:"];
    }
    
    return [super methodSignatureForSelector:aSelector];
}
// 消息重定向
 - (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"forwardInvocation:");
    ForwardObj *obj = [[ForwardObj alloc] init];
    if ([obj performSelector:anInvocation.selector]) {
    	// 如果obj对象可以响应, 则消息转发给obj对象处理
        [anInvocation invokeWithTarget: obj];
    } else {
    	// 否则, 抛异常找不到方法对应的实现
        [self doesNotRecognizeSelector:anInvocation.selector];
    }
}

四 动态方法

1 动态添加方法

我们在消息转发的过程中已经用到了动态添加方法

// 动态添加底层实现
// cls: 为哪个动态添加方法
// name: 要添加的方法名称(方法选择器selector)
// IMP: 无类型函数指针地址
// types: Type Encodings 函数参数和返回值
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) {
    if (!cls) return NO;

    rwlock_writer_t lock(runtimeLock);
    return ! addMethod(cls, name, imp, types ?: "", NO);
}

2 动态方法解析

同样的, 在消息转发过程中, 其实就是对方法的动态解析. 现在我们要讲述另一个方法动态解析的类型, @dynamic
dynamic: 这个词中文意思是动态. 什么动态? 动态运行时的动态, 动态方法的动态, 动态解析的动态, 动态语言的动态!
被@dynamic标记的属性, 在编译时并没有对其getter和setter方法做实现, 而是

  • 动态运行时, 把其实现推迟到了运行时, 即将函数决议推迟到运行时
  • 而静态语言, 是在编译期就进行了函数决议

转载请注明作者和链接哦!
参考资料:
GNU
NS类型编码
运行时编程指南
Objective-C 运行时
Objective-C 编程
iOS 开发:『Runtime』详解(一)基础知识

来源:https://www./content-4-650651.html

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多