iOS 底层原理|Runtime 详解

313 阅读12分钟

一、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 LanguageDeclared 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_classbits & 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 里面的 methodspropertiesprotocols 是二维数组,是可读可写的,包含了类的初始内容、分类的内容,我们拿 methods 举例子:

2.3 class_ro_t 结构

上述的 class_rw_ext_tclass_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 里面的 baseMethodListbaseProtocolsivarsbaseProperties 是一维数组,是只读的,包含了类的初始内容,我们拿 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 表示返回值类型 int24 表示这个方法返回值类型和参数类型,总共需要的字节数。 还有就是方法默认是带有 id 类型和 SEL 类型,types 中的 '@' 和 ':',隐式的。 其他的可以通过下面查询:

CodeMeaningCodeMeaning
cA char*A character string (char *)
iAn int@An object (whether statically typed or typed id)
sA short#A class object (Class)
lA longl is treated as a 32-bit quantity on 64-bit programs.:A method selector (SEL)
qA long long[array type]An array
CAn unsigned char{name=type...}A structure
IAn unsigned int(name=type...)A union
SAn unsigned shortbnumA bit field of num bits
LAn unsigned long^typeA pointer to type
QAn unsigned long long?An unknown type (among other things, this code is used for function pointer)
fA floatdA double
BA C++ bool or a C99 _BoolvA 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.sobjc-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_super2SEL。他直接调用获取父类方法。 这里消息接收者还是子类,只是说从父类开始查找方法实现。

struct objc_super2 {
  id receiver; // receiver 是消息接收者
  Class current_class; // current_class 是 receiver 的 Class 对象
} 

2. Runtime 应用

2.1 Runtime 常见应用场景

Runtime 是做大型框架的利器。它的应用场景非常多,下面就介绍一些常见的应用场景:

  • 查看私有成员变量
  • 字典转模型
  • 替换方法实现
  • 给分类增加属性
2.1.1 查看私有成员变量

如设置 UITextField 占位文字的颜色但是 iOS 13 系统禁止 KVC 对系统 API 私有属性的设置,同理其他私有属性的读写建议也进行修改。

// 已经禁用
[_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 ListProperty List 等,不只可以用来做字典模型转换,还可以做很多工作。 例如:还可以通过 Runtime 实现自动归档和解档,归档和解档通俗来讲就是将数据写入文件和从文件中读取数据,这一块操作在 iOS 中是需要遵循相对应的协议的。 下面我们就来用代码实现一下:用 Runtime 提供的函数遍历 Model 自身所有属性,并对属性进行 encodedecode 操作。

- (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)

参考文档

  1. 《Objective-C Runtime Programming Guide》苹果官方

  2. 《Objective-C Runtime Reference》苹果官方

  3. 《iOS Runtime详解》掘金