RunTime原理之一:如何让Objective-C支持面向对象
一、一切从面向对象开始说起
OC是一门基于C的面向对象的语言,这是如何做到的呢?下面我们简单分析窥探下这背后的密码。
「面向对象」有哪些特性呢?
- 最基本的概念有,类、对象(类实例)、成员变量、方法。
- OC特有的一些性质,属性、协议、类别/扩展、
二、循序渐进深入「面向对象」各种「概念」的封装
1、类(关键字Class
)
/// 类
typedef struct objc_class *Class;
struct objc_class {
Class isa; // 实现方法调用的关键
Class super_class; // 父类
const char * name; // 类名
long version; // 类的版本信息,默认为0
long info; // 类信息,供运行期使用的一些位标识
long instance_size; // 该类的实例变量大小
struct objc_ivar_list * ivars; // 该类的成员变量链表
struct objc_method_list ** methodLists; // 方法定义的链表
struct objc_cache * cache; // 方法缓存
struct objc_protocol_list * protocols; // 协议链表
};
其中关键字段:ivars
(属性链表)、methodLists
(「类实例方法」链表)、cache
(方法缓存)、protocols
(协议链表)、isa
(指向类的元类)。
OC设计者,将类
设计成一个对象!从struct objc_class
里面有个isa
指针,可以推断出这点来。那么类
又是谁的对象呢?其实是「元类」,关于它将在第二章中说明。
2、对象(关键字id
)
/// 对象
struct objc_object {
Class isa;
};
/// id指针
typedef struct objc_object *id;
struct objc_object
的 isa
指向的是「对象」的「类」;id
实质 struct objc_object *
指针,因此其可以指向任何对象。
例如NSObject *obj = [[NSObject alloc] init];
,对象 obj
离的 isa
指针 指向就是NSObject
。
3、方法(关键字SEL
IMP
)
OC利用 @ selector
可以获取方法,其返回的是SEL
,如SEL func = @selector(viewWillAppear:);
那么Runtime又是怎么封装的呢?
还是 struct objc_class
里面的方法列表 struct objc_method_list ** methodLists;
说起,看看结构体struct objc_method_list
是怎么定义的。
// 方法列表
struct objc_method_list {
struct objc_method_list *obsolete ;
int method_count;
/* variable length structure */
struct objc_method method_list[1];
}
// Method
typedef struct objc_method *Method;
struct objc_method {
SEL method_name;
char * method_types;
IMP method_imp;
};
// SEL
typedef struct objc_selector *SEL;
// IMP
typedef id (*IMP)(id, SEL, ...);
-
SEL
是一个指向objc_selector
结构体的指针,而Runtime中没有详细的objc_selector
的定义。
// 通过打印测试
SEL sel = @selector(viewWillAppear:);
NSLog(@"%s", sel); // 输出:viewWillAppear:
推断而知,SEL
其实就是字符串表示方法的名称。
-
IMP
,从定义而知,其实质是一个函数指针,所指向的就是方法的实现。
IMP = id + SEL + ...(即参数列表)
。其中SEL
就是方法名;参数列表就是方法参数;id
指向是self
,对于实例方法来说, self 保存了当前对象的地址;对于类方法来说, self 保存了当前对应类对象的地址。
也可以从NSObject
提供的方法- (IMP)methodForSelector:(SEL)aSelector;
和 + (IMP)instanceMethodForSelector:(SEL)aSelector;
获取到「方法」的具体「实现IMP
」。
-
method_types
,方法类型,用来存储方法的参数类型和返回值类型。
4、成员变量(关键字Ivar
)
类结构体 struct objc_class
里面有一个成员变量列表 struct objc_ivar_list * ivars;
,保存「类」的所有成员变量,它对应关键字是Ivar
,它的结构体是struct objc_ivar
。
// 成员变量列表
struct objc_ivar_list {
int ivar_count;
/* variable length structure */
struct objc_ivar ivar_list[1];
};
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name; // 变量名称
char *ivar_type; // 变量类型名
int ivar_offset; // 基地址偏移字节
}
结构体 struct objc_ivar
保存了「成员变量」的名称、类型,以及对应的偏移地址。
5、协议,Category
typedef struct category_t *Category;
struct category_t {
const char *name; // 类名
classref_t cls; // 类,在运行时阶段通过 clasee_name(类名)对应到类对象
struct method_list_t *instanceMethods; // Category 中所有添加的对象方法列表
struct method_list_t *classMethods; // Category 中所有添加的类方法列表
struct protocol_list_t *protocols; // Category 中实现的所有协议列表
struct property_list_t *instanceProperties; // Category 中添加的所有属性
};
从 Category
的结构体定义中也可以看出, Category
可以为类添加对象方法、类方法、协议、属性。但是 Category
无法添加「成员变量」。
6、属性和属性特性(关键字Property
)
// 属性
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;
// 属性特性
typedef struct {
const char * _Nonnull name; /**< The name of the attribute */
const char * _Nonnull value; /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;
通过 clang
将OC代码转写为.cpp的相关代码可知,本质上 结构体struct objc_property
其实就是 结构体struct _prop_t
,其大致结构如下:
struct _prop_t {
const char *name; //名称
const char *attributes; // readonly、assign的修饰属性
};
成员变量与属性的联系
本质上,一个属性一定对应一个成员变量。但是属性又不仅仅是一个成员变量,属性还会根据自己对应的属性特性的定义来对这个成员变量进行一系列的封装:提供 Getter/Setter 方法、内存管理策略、线程安全机制等等。
7、协议(关键字Protocol
)
struct objc_protocol_list {
struct objc_protocol_list * _Nullable next;
long count;
__unsafe_unretained Protocol * _Nullable list[1];
};
// 宏__OBJC__保证只有OC文件可以调用.h里面的头文件,一些非OC语言不能调用
#ifdef __OBJC__
@class Protocol; // OC当做类
#else
typedef struct objc_object Protocol;
#endif
实质上 Protocol
对应的结构体 struct objc_object
,内部只有一个isa
指针,指向对应的类。
8、cache
typedef struct objc_cache *Cache
struct objc_cache {
unsigned int mask /* total = mask + 1 */ ;
unsigned int occupied;
Method buckets[1];
};
方法调用的时候,会现在缓存中查找,找到返回,否则再去方法列表中找。这样设计的目的在于提高方法调用效率。
三、总结
上面学习了Runtime对于面向对象概念的一些封装,其实质用C语言的结构体、指针、函数指针等;大量使用链表、map等作为存储;设计算法职称,如缓存,让Objective-C具有面向对象的能力。
Runtime针对上面的每一个结构体,都提供丰富API接口,允许对其进行操作。也正是如此设计,让Objective-C能在运行时,获取、创建、修改类相关的数据,具备强大的动态能力。
后面章节,我们将继续学习,Runtime是如何利用这些结构体实现面向对象的能力的。