IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 深入理解Runtime(一) -> 正文阅读

[移动开发]深入理解Runtime(一)

1.什么是RunTime?

我们都知道,将源代码转换为可执行的程序,通常要经过三个步骤:编译、链接、运行。不同的编译语言,在这三个步骤中所进行的操作又有些不同。

C语言作为一门静态类语言,在编译阶段就已经确定了所有变量的数据类型,同时也确定好了要调用的函数,以及函数的实现。

而Objective-C语言是一门动态语言。在编译阶段并不知道变量的具体数据类型,也不知道所真正调用的哪个函数。只有在运行时间才能检查变量的数据类型,同时在运行时才会根据函数名查找要调用的具体函数。在这样在程序没运行的时候,我们并不知道调用一个方法具体会发生什么。

Objective-C语言把一些决定性的工作从编译阶段、链接阶段推迟到运行时阶段的机制,使得Objective-C变得跟家灵活,我们甚至可以在程序运行的时候,动态的去修改一个方法的实现,这也为大为流行的热更新提供了可能性。

要实现Objective-C语言 运行时机制的一切基础就是Runtime。

Runtime实际上是一个库,这个库使我们可以在程序运行时动态的创建对象、检查对象,修改类和对象的方法。

2.消息机制的基本原理

Objective-C 语言中,对象方法调用都是类似[receiver selector];的形式 ,其本质就是让对象在运行时发生消息的过程。

我们来看看方法调用[receiver selector];在编译阶段和运行阶段分别做了什么?

1.编译阶段:【receiver selector】;方法被编译器转换为:objc_msgSend(receiver, selector)(不带参数) objc_msgSend(receiver, selector, org1,org2, …)(带参数)

2.运行时阶段:消息接受者recever寻找对应的selector。

? 1.通过recevier的isa指针找到receiver的Class(类);

? 2.在Class(类)的method list(方法列表)中找对应的selector;

? 3.如果在Class(类)中没有找到这个selector,就继续在他的superClass(父类)中寻找;

? 4.一旦找到对应的selector,直接执行recever对应selector方法实现的IMP(方法实现)。

? 5.若找不到对应的selector,消息被转发或者临时向recever添加这个selector对应的实现方法,否则就会发生崩溃。

上述这个过程中涉及了好几个新的概念:objc_msgSend、isa指针、Class(类)、IMP(方法实现)等,下面我们来具体讲解一下各个概念的含义。

3.Runtime中的概念解析

objc_msgSend

所有Objective-C方法调用在编译时都会转化为对C函数objc_msgSend的调用。

objc_msgSend(receiver, selector);是[receiver selector];对应的C函数。

3.2 Class(类)

在objc/runtime.h 中,Class(类)被定义为指向objc_class结构体的指针,objc_class结构体的数据结构如下:

  An opaque type that represents an Objective-C class

typedef struct objc_class *Class;

struct objc_class {
	Class _Nonnull isa;
#if !__OBJC2__
	Class _Nullable super_class;
	const char * _Nonnull name;
	long version;
	long info;
	long instance_size;
	struct objc_ivar_list * _Nullable ivars;
	struct objc_method_list *_Nullable * _Nullable methodLists;
	struct objc_cache * _Nonnull cache;
	struct objc_protocol_list *_Nullable protocols;
	#endif
};

从中可以看出,objc_class 结构体 定义了很多变量:自身的所有实例变量(ivars)、所有方法定义(methodLists)、遵守的协议列表(protocols)等。objc_class结构体 存放的数据称为元数据(metadata)。

Objc_class结构体的第一个成员变量isa指针,这里保存的就是objc_class结构体的实例指针,而实例换个名字就是对象。换句话说,Class(类)的本质其实就是一个对象,我们称之为类对象。

Object对象

接下来,我们再来看看objc/objc.h中关于Object(对象)的定义。

Object(对象)被定义为objc_object结构体,其数据结构如下:

/// Represents an instance of a class
struct objc_object {
	Class _Nonnull isa;  // objc_object 结构体的实例指针
}/// Apointer to an instance of a class
typedef struct objc_object *id;

这里的id被定义为一个指向objc_object结构体的指针。从中可以看出objc_object结构体只包含一个Class类型的isa指针。

换句话说,一个Object(对象)唯一保存的就是它所属Class(类)的地址。当我们对一个对象,进行方法调用时,比如[receiver selector];它会通过objc_object结构体的isa指针去找对应的object_class结构体,然后在object_class结构体的methodLists(方法列表)中找到我们调用的方法,然后执行。

Meta Class(元类)

从上边我们看出,对象(objc_object 结构体)的isa指针指向的是对应的类对象(object_class 结构体)。那么类对象(object_class 结构体)的isa指针又指向什么呢?

Object_class 结构体 的isa指针 实际上指向的是类对象自身的Meta Class(元类).

那什么是Meta Class(元类)?

Meta Class(元类)就是一个类对象所属的类。一个对象所属的类叫做类对象,而一个类对象所属的类叫做元类。

Runtime中被类对象所属类型就叫做Meta Class(元类),用于描述类休息本身所具有的特征,而在元类的methodLists中,保存了类的方法链表,即所谓的[类方法]。并且类对象的isa指针指向的就是元类。每个类对象有且只有一个与之相关的元类。

在消息机制的基本原理中我们讲解了对象方法的调用过程,我们是通过对象的isa指针找到对应的Class(类);然后在Class(类)的method list(方法列表)中找对应的selector。

而类方法的调用过程和对象方法调用差不多,流程如下:

1.通过类对象isa指针找到所属的Meta Class(元类);

2.在 Meta Class(元类)的method list(方法列表)中找到对应的selector;

3.执行对应的selector。

下面看一个示例:

NSString *testString = [NSString stringWithFormat:@"%d, %S", 3, "test"];

上边的示例中,stringWithFormat:被发送给了NSString类。NSString类通过isa指针找到NSString元类,然后在该元类的方法列表中找到了对应的stringWithFormat:方法,然后执行该方法。

实例对象、类、元类之间的关系

上面,我们讲解了实例对象(Object)、类(Class)、Meta Class(元类)的基本概念,以及简单的指向关系。下面我们通过一张图来清晰地表示出这种关系。
在这里插入图片描述

我们先来看isa指针:

1.水平方向上,每一级中的实例对象的isa指针指向了对应的类对象,而类对象的isa指针指向了对应的元类。而所有元类的isa指针最终指向了NSObject元类,因此NSObject元类也被称为根源类。

2.垂直方向上,元类的isa指针都指向了根元类。而根源类的isa指针又指向了自己。

我们再来看父类指针:

1.类对象的父类指针指向了父亲的类对象,父亲的类对象又指向了根类的类对象,根类的类对象最终指向了nil。

2.元类的父类指针指向了父类对象的元类,父类对象的元类的父类指针指向了根类对象的元类,也就是根元类。而根元类的父亲指针指向了根类对象,最终指向了nil。

方法(Method)

Object_class结构体的methodLists(方法列表)中存放的元素就是方法(Method)。

先来看下objc/runtimr.h中,表示方法(Method)的objc_method结构体:

/// An opaque type that represents a method in a class definition.
/// 代表类定义中一个方法的不透明类型
typedef struct objc_method *Method;
struct objc_method {
	SEL _Nonnull method_name;	// 方法名
	char * _Nullable method_type; // 方法类型
	IMP _Nonnull method_imp;  // 方法实现
}

1.SEL method_name; // 方法名

/// An opaque type that represents a method selector。
typedef struct objc_selector *SEL;

SEL 是一个指向objc_selector 结构体的指针,但是在runtime相关头文件中并没有找到明确的定义。不过,通过测试我们可以得出:SEL只是一个保存方法名的字符串。

SEL sel = @selector(viewDidLoad);
NSLog("%s", sel);  //. 输出:viewDidLoad
SEL sel1 = @selector(test);
NSLog("%s", sel1);  //  输出:test
// IMP method_imp;  //.方法实现
/// A pointer to the function of a method implementation.
#if  !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ...*/ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif

IMP的实质是一个函数指针,所指向的就是方法的实现。IMP用来找到函数地址,然后执行函数。

1.char * method_types; // 方法类型

方法类型 method_types 是个字符串,用来存储方法的参数类型和返回值类型。

到这里,Method的结构就已经很清楚了,Method将SEL(方法名)和IMP(函数指针)关联起来,当对一个对象发送消息时。会通过给出的SEL(方法名)去找到IMP(函数指针),然后执行。

Runtime消息转发

在消息机制的基本原理最后一步我们提到:若找不到对应的selector,消息被转发或者临时向recever添加这边selector对应的实现方法,否则就会发生崩溃。

当一个方法找不到的时候,Runtime提供了消息动态解析、消息接受者重定向、消息重定向 等三步处理消息,具体流程如下图所示:
在这里插入图片描述

消息动态解析

Objective-C 运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现。我们可以通过重写这两个方法,添加其他函数实现,并返回YES,那运行时系统就会重新启动一次消息发送的过程。

主要用的方法如下:

// 类方法未找到时调起,可以在此添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel;
// 对象方法未找到时调起,可以在此对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel;

/**
	*class_addMethod 向具有给定名称和实现的类中添加新方法
	*@param cls 被添加方法的类
	*@param name selector 方法名
	*@param imp 实现方法的函数指针
	*@param types imp 指向函数的返回值与参数类型
	*@return 如果添加方法成功返回YES,否则返回NO
*/
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char * _Nullable types);

Exp:

#import "ViewController.h"
#include "objc/runtimr.h"

@interface Person:NSObject

- (void)fun;

@end
  
@implementation Persion
  - (void)fun {
  	NSLog(@"fun");
}
@end
  
@interface ViewController ()

  - (void)viewDidLoad {
  [super viewDidLoad];
  
  // 执行 fun 方法
  [self performSelector:@selector(fun)];
}
  
+ (BOOL)resolveInstanceMethod:(SEL)sel {
  return YES;//  为了进行下一步 消息接受者重定向
}

//。消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector {
  if (aSelector == @selector(fun)) {
    return [[Person alloc] init];
    //. 返回Person 对象,让 person对象接收这个消息
  }
  return [super forwardingTargetForSelector:aSelector];
}

@end
  // 打印结果;17:34:05.027800+0800 runtime[19495:8232512] fun

可以看到,虽然当前ViewController没有实现fun方法,+resolveInstanceMethod:也没有添加其他函数实现。但是我们通过 forwardingTargetForSelector 把当前 ViewController 的方法转发给了Person对象去执行了。打印结果也证明我们成功实现了转发。

我们通过 forwardingTargetForSelector 可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象不是nil,也不是self,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步:消息重定向流程。

消息重定向

如果进过消息动态解析、消息接受者重定向,Runtime系统还是找不到相应的方法实现而无法响应消息,Runtime系统会被利用-methodSignatureForSelector:方法获取函数的参数和返回值类型。

如果-methodSignatureForSelector:返回了一个NSMethodSignature对象(函数签名),Runtime系统就会创建一个NSI nvocation对象,并通过-forwardInvocation;消息通知当前对象,给予此次消息发送最后一次寻找IMP的机会。

如果 -methodSignatureForSelector:返回nil。则Runtime系统会发出-doesNotRecognizeSelector:消息,程序也就崩溃了。

所以我们可以在-forwardInvocation:方法中对消息进行转发。

用到的方法:

// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;

exp:

#import "ViewController.h"
#include "objc/runtimr.h"

@interface Persion : NSObject

- (void)fun;

@end

@implementation Persion: NSObject

- (void)fun;

@end

@implementation Person

- (void)fun {
	NSLog(@"fun");
}

@end
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
	[super viewDidLoad];
	
	// 执行fun函数
	[self performSelector:@selector(fun)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
	return YES; //  为了进行下一步 消息接受者重定向
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
	return nil; // 为了进行下一步 消息重定向
}

// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
	if ([NSStringFromSelector(aSelector) isEqualToString:@"fun"]) {
		return [NSMethodSignature signatureWithObjCTypes:"v@:"];
	}
	return [super methodSignatureForSelector:aSelector];
}

// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
	SEL sel = anInvocation.selector;  // 从 anInvocation 中获取消息
	
	Person *p = [[Person alloc] init];
	
	if ([p respondsToSelector:sel]) {	// 判断Person 对象方法是否可以响应 sel
		[anInvocation invokeWithTarget:p];  //. 若可以响应,则将消息转发进行处理
	} else {
		[self doesNotRecognizeSelector:sel];  //. 若仍然无法响应,则报错;找不到响应方法
	}
	
}
@end
  // 打印结果:
  13:23:06.935624+0800 runtime[30032:8724248] fun

可以看到,我们在 -forwardInvocation:方法里面让Person 对象去执行了 fun函数。

既然 - forwardingTargetForSelector:和 - forwardInvocation: 都可以将消息转发给其他对象处理,那么两者的区别在哪?

区别就在于 -forwardingTargetForSelector: 只能将消息转发给一个对象。而 -forwardInvocation: 可以将消息转发给多个对象。

以上就是Runtime消息转发的整个流程。

结合之前讲的 消息机制的基本原理,就构成了整个消息发送以及转发的流程。下面我们来总结一下整个流程。

消息发送以及转发机制总结

调用[receiver selector];后,进行的流程:

1.编译阶段:[receiver selector];方法被编译器转换为:

? objc_msgSend(receiver, selector) (不带参数)

? objc_msgSend(receiver, selector, org1, org2, …)(带参数)

2.运行时阶段:消息接受者recever寻找对应的selector。

? 通过 recevier的isa指针找到recevier的class(类);

在class(类)的method_list(方法列表)中找对应的selector

? 如果在class(类)中没有找到这个selector,就继续在它的superclass(父类)中寻找;

一旦找到对应的selector,直接执行recever对应selector方法实现的IMP(方法实现)。

若找不到对应的selector,Runtime系统进入消息转发机制。

3.运行时消息转发阶段:

? 动态解析:通过重写+resolveInstanceMethod: 或者 +resolveClassMethod:方法,利用 class_addMethod 方法添加其他函数实现;

消息接受者重定向:如果上一步添加其他函数实现,可在当前对象利用 -forwardingTargetForSelector: 方法将消息的接受者转发给其他对象;

消息重定向:如果上一步没有返回值nil,则利用 -methodSignatureForSelector:方法获取函数的参数和返回值类型。

a.如果 -methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime系统就会创建一个NSInvocation 对象,给予此次消息发送最后一次寻找IMP的机会。

b如果 -methodSignatureForSelector: 返回 nil。则Runtime系统就会发出 -doesNotRecognizeSelector:消息,程序也就崩溃了。

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-09-30 12:02:39  更:2021-09-30 12:03:40 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 20:20:48-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码