一、Runtime 简介
Objective-C 语言是一门动态语言。它把一些决策从编译阶段、链接阶段推迟到运行时阶段,实现该机制的基础就是 runtime(又叫作运行时)。
- 静态语言:在编译阶段就已确定所有变量的数据类型,同时也确定要调用的函数,以及函数的实现。常见的静态语言,如:C/C++、Java、C# 等。
- 动态语言:程序在运行时可以改变其结构。也就是说在运行时检查变量数据类型,同时在运行时才会根据函数名查找要调用的具体函数。如 Objective-C。
1、Runtime 是什么
Runtime 提供的接口基本都是 C 语言,源码由 C\C++\汇编语言编写。Runtime API 为 Objective-C 语言的动态属性提供支持,充当一种用于 Objective-C 语言的操作系统,使得该语言正常运转工作。
2、Runtime 的版本和平台
在不同平台上有不同版本的 Objective-C Runtime。runtime开源代码
2.1 Versions
Objective-C 运行时有两个版本 modern(现代版本)
和 legacy(旧版本)
。现代版本是在 Objective-C 2.0 中引入的,其中包括许多新功能。旧版运行时的编程接口在 Objective-C 1.0 运行时参考中有所描述;Objective-C Runtime Reference 中描述了现代版本的运行时的编程接口。
最值得注意的新功能是现代运行时中的实例变量是 non-fragile
(非脆弱的):
- 在旧版运行时中,如果更改类中实例变量的布局,则必须重新编译从其继承的类。
- 在现代运行时中,如果更改类中实例变量的布局,则不必重新编译从其继承的类。
另外,现代的运行时支持声明的属性的实例变量综合(请参见 The Objective-C Programming Language 和 Declared Properties)
2.2 Platforms
OS X v10.5 及更高版本上的 iPhone 应用程序和 64 位程序使用现代版本的 Runtime。
其他程序(OS X 桌面上的32位程序)使用运行时的旧版本。
二、深入 Runtime 的前期铺垫
学习 Runtime,必绕不开 Runtime 底层常用的数据结构,比如 isa 指针,Class 的结构等。
1、isa 详解
- 在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址;
- 从arm64架构开始,对isa进行了优化,变成了一个联合体(union)结构,还使用位域来存储更多的信息。
由
从runtime开源代码中整理源码得:
struct objc_object {
private:
isa_t isa; // 8 bytes
public:
...
}
union isa_t {
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
复制代码
下图是 ISA_BITFIELD
的定义:
上面联合体 isa_t
涉及到一个位域的概念,可以参考《C语言位域(位段)详解》。
因为部分数据在存储时候并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可。正是基于这种考虑,C语言又提供了一种叫做位域的数据结构。
isa_t
使用了位域,这里 ISA_BITFIELD
位域成员通过跟 bits
相与来取对应的值。
在 ISA_BITFIELD
中定义的参数的含义:
2、类(Class) 结构
2.1 objc_class
结构
Objective-C 类是由 Class
类型来表示的,它实际上是一个指向 objc_class
结构体的指针。
typedef struct objc_class *Class;
复制代码
类对象结构体 objc_class
继承 实例对象结构体 objc_object
。
// 类对象结构体 objc_class 继承 实例对象结构体 objc_object
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // 用户获取具体类信息
...
}
复制代码
2.2 class_rw_t
结构
通过 objc_class
的 bits
& FAST_DATA_MASK
获取到 class_rw_t
结构体信息:
struct class_rw_t {
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
// explicit_atomic 是为了安全操作
explicit_atomic<uintptr_t> ro_or_rw_ext;
Class firstSubclass;
Class nextSiblingClass;
...
}
struct class_rw_ext_t { // class_rw_t 扩展
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
char *demangledName;
uint32_t version;
};
复制代码
class_rw_t
里面的 methods
、properties
、protocols
是二维数组,是可读可写的,包含了类的初始内容、分类的内容,我们拿 methods
举例子:
2.3 class_ro_t
结构
上述的 class_rw_ext_t
中 class_ro_t
结构体信息:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
...
}
复制代码
class_ro_t
里面的 baseMethodList
、baseProtocols
、ivars
、baseProperties
是一维数组,是只读的,包含了类的初始内容,我们拿 baseMethodList
举例子:
2.4 method_t
结构
method_t
是对方法、函数的封装,他的结构是:
using MethodListIMP = IMP;
struct method_t {
SEL name; // 方法、函数名
const char *types; // 编码 (返回值类型、参数类型)
MethodListIMP imp; // 指向方法、函数的指针(函数地址)
...
};
复制代码
2.4.1 imp
属性
method_t
结构体中的 imp
代表函数的具体实现,底层源码中的定义:
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
复制代码
2.4.2 SEL
属性
SEL
代表方法、函数名,一般叫做选择器,底层结构跟char *
类似,获取方法:- 可以通过
@selector()
和sel_registerName()
获得; - 可以通过
sel_getName()
和NSStringFromSelector()
转成字符串; - 不同类中相同名字的方法,所对应的方法选择器是相同的。
- 可以通过
底层源码中的定义:
typedef struct objc_selector *SEL;
复制代码
2.4.3 types
属性
types
包含了函数返回值、参数编码的字符串。
我们借助 MJ 封装的 MJClassInfo.h
文件获取底层数据结构 baseMethodList
的第一个参数:
我们得到 types
的值:
// 方法
- (int)test:(int)age height:(float)height;
// types: “i24@0:8i16f20”
复制代码
这个 types
值表示什么呢?iOS 中提供了一个叫做 @encode
的指令,可以将具体的类型表示成字符串编码。
我主要说一下前面 的 i
表示返回值类型 int
,24
表示这个方法返回值类型和参数类型,总共需要的字节数。
还有就是方法默认是带有 id
类型和 SEL
类型,types
中的 ‘@’ 和 ‘:’,隐式的。
其他的可以通过下面查询:
Code | Meaning | Code | Meaning |
---|---|---|---|
c | A char | * | A character string (char * ) |
i | An int | @ | An object (whether statically typed or typed id) |
s | A short | # | A class object (Class) |
l | A longl is treated as a 32-bit quantity on 64-bit programs. | : | A method selector (SEL) |
q | A long long | [array type] | An array |
C | An unsigned char | {name=type…} | A structure |
I | An unsigned int | (name=type…) | A union |
S | An unsigned short | bnum | A bit field of num bits |
L | An unsigned long | ^type | A pointer to type |
Q | An unsigned long long | ? | An unknown type (among other things, this code is used for function pointer) |
f | A float | d | A double |
B | A C++ bool or a C99 _Bool |
v | A void |
2.5 方法缓存
Class 内部结构中有个方法缓存(cache_t
),调用了方法之后会缓存在里面,他用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度。下面是整理出来的重要部分代码:
这里的散列表的原理就是,通过 @selector(methodName)
& _mask
获得一个索引值,通过这个索引就能很快在 buckets
中拿到对应的 bucket_t(key, _imp)
;当然存放也是一样的方式。
- 存放:如果生成的索引在
buckets
下已经存在 data 。那么他会把 index – 1,减到零了还没有空闲位置,它会从数组最大值开始继续往前找位置,直到有位置; - 获取:在拿到
bucket_t
后,会比较一下key
与@selector(methodName)
是否对应,如果不对应,那就回按照存放的那样方式一个一个找。如果存满了,buckets
就会走扩容。
这就是空间换时间。
三、深入探究 Runtime
OC 中方法调用通过 Runtime 实现,Runtime 进行方法调用最重要的底层本质就是Runtime 消息机制,同时我们也会讨论 Runtime 的常见应用。
1. Runtime 消息机制
OC 中的方法调用,编译时候都会转换为 objc_msgSend
函数的调用:
[obj methodName] => objc_msgSend(obj, @selector(methodName))
// 消息接收者:obj
// 消息名称: @selector(methodName)
复制代码
objc_msgSend
的执行流程可以分为 3 大阶段
- 消息发送
- 找不到消息发送方法,就会进入动态方法解析,允许开发者动态创建新方法;
- 如果动态方法解析没有做任何操作,这时候就开始进入消息转发。
如果这三个阶段都没有搞定,也就是说 objc_msgSend
没找到合适的方法调用,就会报一个很经典的错误:
unrecognized selector sent to instance
复制代码
关于消息机制的这块的源码(第一节提供的源码地址下载),主要是在 objc-msg-arm64.s
、objc-runtime-new.mm
以及 Core Foundation 的 forwarding 中(这一块不开源)。
1.1 消息发送
下面是消息发送的流程:
源码自行下载阅读,稍稍费劲点。
1.2 动态方法解析
下面是动态方法解析的流程:
我们通过代码去分析一波:
Person.h
@interface Person : NSObject
- (void)test;
@end
复制代码
Person.m
#import <objc/runtime.h>
@implementation Person
- (void)other {
NSLog(@"%s", __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(test)) {
// 获取 other 方法信息
Method method = class_getInstanceMethod(self, @selector(other));
// 动态添加 test 方法的实现
class_addMethod(self, sel,
method_getImplementation(method),
method_getTypeEncoding(method));
// 返回 YES 代表有动态添加方法
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
复制代码
在 main
文件运行:
Person *person = [[Person alloc] init];
[person test];
复制代码
我们通过打印能看到:
2021-04-14 11:36:52.022282+0800 StudyOC[5105:7228146] -[Person other]
复制代码
我们通过 resolveInstanceMethod
去动态配置 test
,当我们运行 test
实际上调用的是 other
方法。上面分析了实例方法,类方法操作也是一样的,只是使用的方法不一样。这里需要注意的是类方法的动态解析中 class_addMethod
第一个参数传的不是 self
而是 object_getClass(self)
。
动态解析过后,会重新走“消息发送”的流程,从 receiverClass 的 cache 中查找方法这一步开始执行。
1.3 消息转发
下面是动态方法解析的流程:
还是拿上述的 Person
类举例子:
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
// objc_msgSend([[Student alloc] init], aSelector);
return [[Student alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
复制代码
这时候在 main
调用 Person
的对象方法 test
,实际执行的是 Student
的对象方法 test
;
如果 forwardingTargetForSelector
返回值是空的;那么就会继续走 methodSignatureForSelector
方法,下面:
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
return nil;
}
return [super forwardingTargetForSelector:aSelector];
}
// 方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
}
return [super methodSignatureForSelector:aSelector];
}
// NSInvocation 封装了一个方法调用,包括:方法调用者、方法名、方法参数
// 方法调用者:anInvocation.target
// 方法名:anInvocation.selector
// 方法参数:[anInvocation getArgument:NULL atIndex:0];
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 等同与 [anInvocation invokeWithTarget:[[Student alloc] init]];
anInvocation.target = [[Student alloc] init];
[anInvocation invoke];
}
复制代码
开发者可以在 forwardInvocation:
方法中自定义任何逻辑,以上方法都有对象方法、类方法。
1.4 super
super
调用,底层会转换为 objc_msgSendSuper2
函数的调用,接收2个参数:struct objc_super2
、SEL
。他直接调用获取父类方法。
这里消息接收者还是子类,只是说从父类开始查找方法实现。
struct objc_super2 {
id receiver; // receiver 是消息接收者
Class current_class; // current_class 是 receiver 的 Class 对象
}
复制代码
2. Runtime 应用
2.1 Runtime 常见应用场景
Runtime 是做大型框架的利器。它的应用场景非常多,下面就介绍一些常见的应用场景:
- 查看私有成员变量
- 字典转模型
- 替换方法实现
- 给分类增加属性
2.1.1 查看私有成员变量
如设置 ,但是 iOS 13 系统禁止 KVC 对系统 API 私有属性的设置,同理其他私有属性的读写建议也进行修改。UITextField
占位文字的颜色
// 已经禁用
[_passwordTextField setValue:RGBCOLOR(176, 176, 176) forKeyPath:@"_placeholderLabel.textColor"];
复制代码
2.1.2 字典转模型
字典转模型重要的两个点:
- 利用 Runtime 遍历所有的属性或者成员变量;
- 利用 KVC 设值。
下面简单实现了一个字典转模型的代码,通过 Runtime 遍历属性列表,并根据属性名取出字典中的对象,然后通过 KVC 进行赋值操作。调用方式和 MJExtension、YYModel 类似,直接通过模型类调用类方法即可。
- (instancetype)initWithDict:(NSDictionary *)dict {
self = [super init];
if (self) {
unsigned int count = 0;
objc_property_t *propertys = class_copyPropertyList([self class], &count);
for (int i = 0; i < count; i++) {
objc_property_t property = propertys[i];
//通过 property_getName 函数获得属性的名称
const char *name = property_getName(property);
NSString *nameStr = [[NSString alloc] initWithUTF8String:name];
id value = [dict objectForKey:nameStr];
[self setValue:value forKey:nameStr];
}
free(propertys);
}
return self;
}
复制代码
有兴趣可以看看第三方模型转换库的横向对比(Mantle、MJExtension、YYModel 等):《iOS JSON 模型转换库评测》。
2.1.3 替换方法实现
替换方法实现常用的两个方法:
// 方法替换
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
// 方法交换
void method_exchangeImplementations(Method m1, Method m2)
复制代码
例子:
// class_replaceMethod 替换成 imp_implementationWithBlock 中的内容
Person *person = [[Person alloc] init];
class_replaceMethod([Person class], @selector(test), imp_implementationWithBlock(^{
NSlog(@"11");
}), "v");
[person test];
// run 和 test 相互替换
Method runMethod = class_getInstanceMethod([Person class], @selector(run));
Method testMethod = class_getInstanceMethod([Person class], @selector(test));
method_exchangeImplementations(runMethod, testMethod);
复制代码
2.1.4 对象自动归档解档
通过 Runtime 可以获取到对象的 Method List
、Property List
等,不只可以用来做字典模型转换,还可以做很多工作。
例如:还可以通过 Runtime 实现自动归档和解档,归档和解档通俗来讲就是将数据写入文件和从文件中读取数据,这一块操作在 iOS 中是需要遵循相对应的协议的。
下面我们就来用代码实现一下:用 Runtime 提供的函数遍历 Model 自身所有属性,并对属性进行 encode
和 decode
操作。
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}
复制代码
2.1.5 给分类增加属性
这里可以看我的博客 《iOS 底层原理|Category 本质》 中
五、 Caregory 通过关联对象添加成员变量
2.2 Runtime 常用 API
2.2.1 Runtime 关于类的 API
//动态创建一个类(参数:父类,类名,额外的内存空间)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
// 注册一个类(要在类注册之前添加成员变量)
void objc_registerClassPair(Class cls)
// 销毁一个类
void objc_disposeClassPair(Class cls)
// 获取isa指向的Class
Class object_getClass(id obj)
// 设置isa指向的Class
Class object_setClass(id obj, Class cls)
// 判断一个OC对象是否为Class
BOOL object_isClass(id obj)
// 判断一个Class是否为元类
BOOL class_isMetaClass(Class cls)
// 获取父类
Class class_getSuperclass(Class cls)
复制代码
2.2.2 Runtime 关于成员变量的 API
// 获取一个实例变量信息
Ivar class_getInstanceVariable(Class cls, const char *name)
// 拷贝实例变量列表(最后需要调用free释放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
// 设置和获取成员变量的值
void object_setIvar(id obj, Ivar ivar, id value)
id object_getIvar(id obj, Ivar ivar)
// 动态添加成员变量(已经注册的类是不能动态添加成员变量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)
// 获取成员变量的相关信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)
复制代码
2.2.3 Runtime 关于属性的 API
// 获取一个属性
objc_property_t class_getProperty(Class cls, const char *name)
// 拷贝属性列表(最后需要调用free释放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
// 动态添加属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
unsigned int attributeCount)
// 动态替换属性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
unsigned int attributeCount)
// 获取属性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)
复制代码
2.2.4 Runtime 关于方法的 API
// 获得一个实例方法、类方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)
// 方法实现相关操作
IMP class_getMethodImplementation(Class cls, SEL name)
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2)
// 拷贝方法列表(最后需要调用free释放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)
// 动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
// 动态替换方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
// 获取方法的相关信息(带有copy的需要调用free去释放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_copyArgumentType(Method m, unsigned int index)
// 选择器相关
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)
// 用block作为方法实现
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)
复制代码