objc让C语言面向对象的方式
简介:objc使用结构体让C语言支持面向对象。本文主要对这些结构体的功能和实际运行时的机制做一个概括,有了这些底层机制会更加轻松地理解一些高级的机制,比如类别为什么可以添加方法而不能添加属性、动态绑定变量、类别天加的方法会“覆盖”原有方法等。
Class和Object
- 在<objc/objc.h>中定义了objc_object。
- 在<objc/runtime.h>中定义了objc_class。
- 其他相关的结构体也都在这两个文件中可以找到。
- 如下(在objc/runtime.h中):
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
- 定义了一种objc_class的结构体,并使用Class代替结构体指针,具体如下(在objc/objc.h中):
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
- 使用的[self Class]就可以获取这种结构体指针,这个结构体指针指向的是self类的结构体,也就是实例的isa指针。
- 在objc_class结构体中定义的主要内容有:isa指针、super指针、char*类型的类名、long型的实例大小、变量列表、方法列表、缓存、遵守的协议列表。
- 依次介绍这些必要元素的作用:
- isa意思是这个class是个什么,通常实例的isa指针是指向类,而类(类对象)的isa指针指向类的原类,对于实例、类、原类,我个人是以他们的功能区分的,他们分别负责存储一个完整类的三个抽象层次的东西:属性、对象方法、类方法:在实例中主要存储着属性、在类中存储着实例方法(开头为-()的方法)、在原类中存储着类方法(开头为+()的方法),通常我们在h文件中声明的时候也都是声明这三种东西,而他们在抽象层次上是存储在三个位置的。
- super指针就是指向自身父类(对应的类结构体)的指针。
- name就是这个类的名字(当我们手动创建一个和KVO派生出的子类名字相同的类时编译不会报错,而运行时会报错,因此OC在区分类时就是靠名字区分的)。
- instance_size,就是一个对象占内存的大小,当我第一次看到实例结构体的定义时(见下面代码)让我大跌眼镜,为什么一个实例的结构体只有一个变量还是一个必不可少的isa指针,那我们定义的属性的值都装载哪里?肯定不会装载class结构体里面,因为class结构体是唯一的,而之后看到struct objc_ivar结构体的定义时才恍然大悟(见下下面代码),定义变量的结构体中有变量名、变量类型、offset和space,offset定义了这个实例的某个变量的地址相对于这个实例地址的偏移量,比如说一个实例的地址是10000,他的第一个变量是NSInteger,那么这个变量的offset就是0,space就是8,那么第二个变量不管是什么,他的offset都是8、他的space根据自己的类型大小不同。需要找第一个变量时就去10000+0的地址去找,需要找第二个变量时就去10000+8的地址去找。因此,一个objc_object结构体指针只会透露两个信息,一个是他的地址、一个是他的父类,当需要访问里面详细的信息时都需要根据class的规定和自身地址去寻址,从而查找变量。因此这个instance_size也就相当于varlist中所有变量的space之和了。
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
struct objc_ivar {
char * _Nullable ivar_name OBJC2_UNAVAILABLE;
char * _Nullable ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
- var_list,就是上面所说一个objc_ivar结构体的列表,包含了所有变量的信息。
- method_list,类似变量列表,这个是方法列表,包含所有方法的信息,方法结构体的定义如下(见下面代码)。每个结构体包含:方法名(SEL)、返回类型、和方法实现(IMP)。SEL和IMP都是typedef定义的缩写,找到其定义(见下下面代码),更深一层没有找到对于objc_selector结构体的定义,可以姑且就把他看成是一个字符串用来标记方法的名字,而对于IMP可以清晰看它就是一个C的函数指针,这样就可以把一个名字和一个指针对应起来的,只要我给出名字,就可以得到这个名字对应实现的指针,然后执行那里相应的代码。(这里忽然想到categary相关的一些理解,我们在categary中添加方法的时候总是在load时把方法添加到这个list中,如果有了同名方法,并不会把之前的覆盖掉,而是直接添加到list的最前面,因此每次使用selector查指针的时候都是先查到前面那个,因此就好像把原有方法覆盖掉了一样,但是OC是不推荐这样做的,因此这样会导致我们即使不想引入类别,也会不得不调用类别中的同名方法,从而使原有方法没法调用;另一方面,类别中之所以不能添加属性是因为我们上面一条所说的instance_size是类一开始load的时候就会根据varlist中的变量个数和大小确定好了,之后再添加属性会可能会出现访问越界的问题,因此不能添加属性,而动态绑定的变量不会出现在varlist中也不会改变一个实例的大小,因此可以用这种方法实现“假装”添加属性,绑定变量的绑定关系保存在全局的一个哈希表中,而不是实例里面)。
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
/// 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
- cache缓存结构体,cache装的什么?cache的定义(见下方代码),里面有个Method数组,Method就是我们之前看到的方法结构体,因此Cache是用来存储Method的。事实上当我们创建一个类的时候会写很多方法在里面,而runtime在查找这些方法的时候是根据SEL(方法名)一个一个对照着去查找的,而有些不常用的方法总是需要经过遍历,很浪费时间,因此就有了这个缓存,里面装的是那些常用的方法,每次只需要遍历一小部分方法名就可以快速找到对应的函数指针。
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method _Nullable buckets[1] OBJC2_UNAVAILABLE;
};
typedef struct objc_method *Method;
- protocol_list即是我们平时用尖括号围起来的协议了,其定义如下(见下方代码),可以看到protocollist中装的Protocol其实就是objc_object,这个没有查到特别具体的解释,我自己的理解是这些object指针指向的是类对象,从这样的类对象中也可以读到name,这些name被当做协议名而不是类名,而在方法列表中只能读取到方法名SEL,不能读取到具体的IMP实现指针,因此protocol和class是相同的存储形式,根据不同的读取规则产生了不同的作用。
#ifdef __OBJC__
@class Protocol;
#else
typedef struct objc_object Protocol;
#endif
- 最终,根据以上的这些结构体,OC成功实现了让C语言面向对象。
- 以上为查看源码和膜拜大神解释后自己的理解,不能保证完全正确,但整体上来看这样理解是基本正确的。
结束
网友评论