文章目录
- 前言
- 一、Runtime简介
- 二、NSObject库起源
-
- isa
- isa_t结构体
- cache_t的具体实现
- class_data_bits_t的具体实现
- 三、[self class] 与 [super class]
- 四、消息发送与转发
- 五、Runtime应用场景
前言
之前分part学习了Runtime的内容,但是没有系统的总结,这篇博客用来总结学过的所有Runtime知识
一、Runtime简介
Runtime
又叫运行时,是一套底层的C语言API
,是iOS系统的核心之一
在编码阶段中,当我们向一个对象发送消息时,编译阶段
只是确定了我们需要向接收者发送消息,但是接收者如何响应与处理这条消息是运行时
决定的,我们来看一个例子
首先,让我们定义这些类:
bash
#import <Foundation/Foundation.h>
// 基类 Animal
@interface Animal : NSObject
- (void)speak;
@end
@implementation Animal
- (void)speak {
NSLog(@"Some generic animal sound");
}
@end
// Dog 类继承自 Animal
@interface Dog : Animal
@end
@implementation Dog
- (void)speak {
NSLog(@"Woof!");
}
@end
// Cat 类继承自 Animal
@interface Cat : Animal
@end
@implementation Cat
- (void)speak {
NSLog(@"Meow!");
}
@end
现在,我们编写一个主函数来创建不同的动物对象,并对它们调用 speak 方法:
bash
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 创建 Animal 类型的数组
NSArray *animals = @[[[Dog alloc] init], [[Cat alloc] init], [[Animal alloc] init]];
// 遍历数组中的每一个动物,并调用 speak 方法
for (Animal *animal in animals) {
[animal speak];
}
}
return 0;
}
可以看到我们animal接受了speak这个方法,但是运行时会查找animal的实际类,并且动态地查找这个类或其父类中的 speak 方法实现。
同时OC也是一门动态语言,这意味着它不仅需要一个编译器,更需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。
Objc 在三种层面 上与 Runtime
系统进行交互:
- 层面一:通过OC源代码
我们只需要编写OC代码,Runtime系统会自动将我们写的代码在编译阶段转换为运行时代码 - 层面二:通过Foudation框架的NSObject的类自定义方法
在NSObject协议中有五种方法可以从Runtime
中获取信息,并且让对象进行自我检查
bash
- (Class)class OBJC_SWIFT_UNAVAILABLE("use 'anObject.dynamicType' instead");
- (BOOL)isKindOfClass:(Class)aClass;
- (BOOL)isMemberOfClass:(Class)aClass;
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
- (BOOL)respondsToSelector:(SEL)aSelector;
-class
方法返回对象的类;
-isKindOfClass:
和 -isMemberOfClass:
方法检查对象是否存在于指定的类的继承体系中;
-respondsToSelector:
检查对象能否响应指定的消息;
-conformsToProtocol:
检查对象是否实现了指定协议类的方法;
在NSObject类中还有一个方法会返回SEL的IMP
bash
- (IMP)methodForSelector:(SEL)aSelector;
- 层面三:通过对 Runtime 库函数的直接调用
bash
1. Class and Metaclass Functions
• objc_getClass(const char *name): 获取指定名称的类。
• objc_getMetaClass(const char *name): 获取指定名称的元类。
• objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes): 动态创建一个新的类。
• objc_registerClassPair(Class cls): 注册一个动态创建的类。
2. Method Functions
• class_addMethod(Class cls, SEL name, IMP imp, const char *types): 向类中添加一个方法。
• class_replaceMethod(Class cls, SEL name, IMP imp, const char *types): 替换类中的一个方法。
• class_getInstanceMethod(Class cls, SEL name): 获取实例方法。
• class_getClassMethod(Class cls, SEL name): 获取类方法。
• method_getName(Method m): 获取方法的选择器。
• method_getImplementation(Method m): 获取方法的实现。
3. Property and Ivar Functions
• class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types): 向类中添加一个实例变量。
• class_getInstanceVariable(Class cls, const char *name): 获取类中的实例变量。
• class_getProperty(Class cls, const char *name): 获取类中的属性。
• class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount): 向类中添加属性。
4. Selector Functions
• sel_registerName(const char *str): 注册一个选择器。
• sel_getUid(const char *str): 获取一个选择器。
5. Protocol Functions
• objc_getProtocol(const char *name): 获取指定名称的协议。
• objc_allocateProtocol(const char *name): 动态创建一个新的协议。
• objc_registerProtocol(Protocol *proto): 注册一个动态创建的协议。
• protocol_addMethodDescription(Protocol *proto, SEL name, const char *types, BOOL isRequiredMethod, BOOL isInstanceMethod): 向协议中添加方法描述。
• protocol_addProperty(Protocol *proto, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount, BOOL isRequiredProperty, BOOL isInstanceProperty): 向协议中添加属性。
6. Object and Messaging Functions
• objc_msgSend(id self, SEL op, ... ): 发送消息。
• objc_msgSendSuper(struct objc_super *super, SEL op, ... ): 发送消息给父类。
• object_getClass(id obj): 获取对象的类。
• object_setClass(id obj, Class cls): 设置对象的类。
二、NSObject库起源
刚才说了我们有三种方式可以和Runtime进行交互,前两种方式都与NSObject有关,我们就从NSObject基类开始说起
我们通过源码可以得知NSObject
的定义如下:
bash
typedef struct objc_class *Class;
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
其内部只包含了一个名为isa的Class指针,同时Class指针实际上就是一个objc_class
结构体,如何理解这个结构体呢,我们来看一下这个结构体的源码:
在Objc2.0之前,objc_class
源码如下:
bash
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
可以看到在一个类中,有超类的指针,类名,版本的信息,同时还有指向成员变量列表的指针,指向方法列表的指针
我们可以通过动态的修改方法列表来达到使用分类向类中添加方法
关于分类的文章之前写过,现在发现一篇更好的,大家可以读一下
深入理解Objective-C:Category
同时在先前说过Category的底层结构体中是有属性列表的,但是为什么不能添加属性呢,这是因为当我们使用@property声明属性时,会自动添加实例变量,但是Category
的底层结构体中没有实例变量列表,因此无法实现,同时还有一个原因是编译器不会为分类自动合成set与get方法,但最最主要的原因是rw中没有成员变量列表,不允许修改成员变量
在objc2.0
之后,objc_class的定义就变了:
bash
typedef struct objc_class *Class;
typedef struct objc_object *id;
@interface Object {
Class isa;
}
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
struct objc_object {
private:
isa_t isa;
}
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
}
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
}
将源码转换为类图就变成了下面这样子:
在源码中我们可以看出来所有的对象都包含一个isa_t类型的结构体,这是如何看出来的呢
bash
struct objc_object {
private:
isa_t isa;
}
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
}
从这两段代码我们可以看出来,objc_class
是objc_object
的子类,我们理解一下这两个结构体名字:
objc_object
的意思是对象,也就是在OC中所有对象都有一个isa_t
变量,objc_class
的意思是类,但是他却继承于对象,那么说明我们的类实际上也是一个对象,也就是类对象
这也就说明了上面的结论:所有的对象都会包含一个isa_t类型的结构体。
objc_object
被源码typedef成了id
类型,这也说明了为什么任何类型都可以用id
来表示,这是因为id
类型是所有对象的父类
我们一步步来分析这里面的成员变量,首先是object
类和NSObject
类里面分别都包含一个objc_class
类型的isa
isa
首先我们通过学习消息流程可以知道,当一个对象的方法被调用时,首先会根据isa指针找到相应的类,然后在该类的class_data_bits_t中去查找方法。class_data_bits_t是指向了类对象的数据区域
。在该数据区域内查找相应方法的对应实现
同时当调用类方法是也会通过isa
查找方法,此时isa
指向的是元类(Meta Class)
,这里有问题可以看先前的博客,不再赘述
同时元类与类对象是唯一的
isa_t结构体
isa_t
是现代Objective-C
运行时中的一个重要优化,它通过位域结构封装了 isa
指针,使得它不仅仅是一个指向类的指针,还携带了大量运行时所需的附加信息。通过这种设计,Objective-C
运行时能够在保持高效内存使用的同时,提供丰富的对象管理功能。
总结就是isa_t比较抽象,笔者也讲不懂,但是里面用到了Tagged Pointer技术,大家可以去了解
深入理解 Tagged Pointer
cache_t的具体实现
cache_t出现objc_class中,我们来通过源码分析一下
bash
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
typedef unsigned int uint32_t;
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
typedef unsigned long uintptr_t;
typedef uintptr_t cache_key_t;
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
}
通过源码我们知道了cache_t
中存储了一个bucket_t
的结构体,和两个unsigned int的变量。
- mask:分配用来缓存bucket的总数。
- occupied:表明目前实际占用的缓存bucket的个数。
同时我们看一下bucket_t
结构体,他里面只有两个元素,一个是key,一个是IMP
cache_t
中的bucket_t *_buckets
其实就是一个散列表,用来存储Method
的链表。
当我们使用方法后,编译器会自动将方法的SEL存为Key,其实现IMP存进bucket_t
中的Key对应的IMP中,这样就优化了方法调用的性能,不用每次调用方法时都去方法列表中查找
Cache的作用主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。
class_data_bits_t的具体实现
bash
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
}
在objc2.0
之前我们的objc_class
结构体中有十分多的元素,但是更新后就变得十分简洁,这些元素并没有消失,其实都存在了数据区域class_data_bits_t
中
同样来看源码:
bash
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;
}
struct 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;
char *demangledName;
}
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;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
在 objc_class
结构体中的注释写到 :
class_data_bits_t
相当于 class_rw_t
指针加上 rr/alloc
的标志
也就是说先前的属性、方法以及遵循的协议在obj 2.0
的版本之后都放在class_rw_t
中,那么ro
是用来干什么的呢?
我们知道OC作为一门动态语言运行阶段分为编译器与运行期,在编译期类的结构中的 class_data_bits_t *data
指向的是一个 class_ro_t *
指针:
在Objc运行时会调用realizeClass
方法:
- 从
class_data_bits_t
调用data
方法,将结果从class_rw_t
强制转换为class_ro_t
指针,这一步是为了class_rw_t
的ro
能被正确赋值 - 初始化一个
class_rw_t
结构体 - 设置结构体
ro
的值以及flag
- 最后设置正确的
data
,也就是返回最后的rw
结构体(因为原本data
指向的是ro
)
我们来看一下更改后的图片
此时realizeClass
方法运行后我们的rw结构体已经被初始化,同时ro已经被赋值,但是此时的方法,属性以及协议列表均为空,这时需要 realizeClass 调用 methodizeClass 方法
来将类自己实现的方法(包括分类)、属性和遵循的协议加载到 methods、 properties 和 protocols 列表中。
bash
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; }
};
};
同时我们可以再通过这里讲讲我们的消息查找,如果动态修改了方法会生成rw_e
结构体,查找方法时会优先去rw_e
中查找,否则去ro
中查找
三、[self class] 与 [super class]
我们来看一道题目
下面代码输出什么?
bash
@implementation Son : Father
- (id)init
{
self = [super init];
if (self)
{
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
self和super
的区别:
self是类一个隐藏参数,每个方法的实现的第一个参数为self
super则负责告诉编译器,调用方法时,去调用父类的方法,而不是本类中的方法
也就是说[super class]
调用了objc_msgSendSuper
方法,而不是objc_msgSend
bash
OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained Class class;
#else
__unsafe_unretained Class super_class;
#endif
/* super_class is the first class to search */
};
在objc_msgSendSuper
方法中,我们会从父类的方法列表开始查找selector
,找到后以objc->receiver
去调用父类的这个selector
。注意,最后的调用者是objc->receiver
,而不是super_class
!
那么objc_msgSendSuper
最后就转变成
bash
// 注意这里是从父类开始msgSend,而不是从本类开始,谢谢@Josscii 和他同事共同指点出此处描述的不妥。
objc_msgSend(objc_super->receiver, @selector(class))
/// Specifies an instance of a class. 这是类的一个实例
__unsafe_unretained id receiver;
// 由于是实例调用,所以是减号方法
- (Class)class {
return object_getClass(self);
}
由于找到了父类NSObject
里面的class
方法的IMP
,又因为传入的入参objc_super->receiver = self
。self
就是son
,调用class
,所以父类的方法class
执行IMP
之后,输出还是son,最后输出两个都一样,都是输出son
。
四、消息发送与转发
这部分内容之前已经学的十分详细了,可以直接看之前写的博客
【iOS】消息流程分析
五、Runtime应用场景
同时我们讲完了Runtime,我们自然要知道如何应用Runtime,我们来看一下Runtime的一些应用
- (1) 实现多继承Multiple Inheritance
- (2) Method Swizzling
- (3) Aspect Oriented Programming
- (4) Isa Swizzling
- (5) Associated Object关联对象
- (6) 动态的增加方法
- (7) NSCoding的自动归档和自动解档
- (8) 字典和模型互相转换
其中大多数应用之前博客都有讲大家可以自行查找,同时Isa Swizzling
对应的应用是KVO
的原理,至于字典模型相互转换之后在学习JsonModel源码中会讲
参考博客:
神经病院 Objective-C Runtime 入院第一天------ isa 和 Class神经病院 Objective-C Runtime 入院第一天------ isa 和 Class
深入解析 ObjC 中方法的结构
深入理解Objective-C:Category